This commit is contained in:
Qing 2023-11-23 22:01:06 +08:00
parent 43433c50eb
commit 973987dfbb
7 changed files with 619 additions and 41 deletions

View File

@ -11,6 +11,7 @@
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
@ -27,6 +28,7 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"flexsearch": "^0.7.21", "flexsearch": "^0.7.21",
"immer": "^10.0.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.292.0", "lucide-react": "^0.292.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
@ -39,7 +41,8 @@
"react-zoom-pan-pinch": "^3.3.0", "react-zoom-pan-pinch": "^3.3.0",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zustand": "^4.4.6"
}, },
"devDependencies": { "devDependencies": {
"@types/flexsearch": "^0.7.3", "@types/flexsearch": "^0.7.3",
@ -1564,6 +1567,35 @@
} }
} }
}, },
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz",
"integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-menu": "2.0.6",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-controllable-state": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": { "node_modules/@radix-ui/react-focus-guards": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
@ -1655,6 +1687,46 @@
} }
} }
}, },
"node_modules/@radix-ui/react-menu": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz",
"integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": { "node_modules/@radix-ui/react-popover": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
@ -4023,6 +4095,15 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immer": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
"integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -5652,6 +5733,14 @@
} }
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -5770,6 +5859,33 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zustand": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz",
"integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
} }
} }
} }

View File

@ -13,6 +13,7 @@
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
@ -29,6 +30,7 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"flexsearch": "^0.7.21", "flexsearch": "^0.7.21",
"immer": "^10.0.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.292.0", "lucide-react": "^0.292.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
@ -41,7 +43,8 @@
"react-zoom-pan-pinch": "^3.3.0", "react-zoom-pan-pinch": "^3.3.0",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zustand": "^4.4.6"
}, },
"devDependencies": { "devDependencies": {
"@types/flexsearch": "^0.7.3", "@types/flexsearch": "^0.7.3",

View File

@ -0,0 +1,418 @@
import { useStore } from "@/lib/states"
import React, { useEffect, useState } from "react"
const DOC_MOVE_OPTS = { capture: true, passive: false }
const DRAG_HANDLE_BORDER = 2
interface EVData {
initX: number
initY: number
initHeight: number
initWidth: number
startResizeX: number
startResizeY: number
ord: string // top/right/bottom/left
}
interface Props {
maxHeight: number
maxWidth: number
scale: number
minHeight: number
minWidth: number
show: boolean
}
const clamp = (
newPos: number,
newLength: number,
oldPos: number,
oldLength: number,
minLength: number,
maxLength: number
) => {
if (newPos !== oldPos && newLength === oldLength) {
if (newPos < 0) {
return [0, oldLength]
}
if (newPos + newLength > maxLength) {
return [maxLength - oldLength, oldLength]
}
} else {
if (newLength < minLength) {
if (newPos === oldPos) {
return [newPos, minLength]
}
return [newPos + newLength - minLength, minLength]
}
if (newPos < 0) {
return [0, newPos + newLength]
}
if (newPos + newLength > maxLength) {
return [newPos, maxLength - newPos]
}
}
return [newPos, newLength]
}
const Cropper = (props: Props) => {
const { minHeight, minWidth, maxHeight, maxWidth, scale, show } = props
const [
isInpainting,
{ x, y, width, height },
setX,
setY,
setWidth,
setHeight,
] = useStore((state) => [
state.isInpainting,
state.cropperState,
state.setCropperX,
state.setCropperY,
state.setCropperWidth,
state.setCropperHeight,
])
// const [x, setX] = useRecoilState(croperX)
// const [y, setY] = useRecoilState(croperY)
// const [height, setHeight] = useRecoilState(croperHeight)
// const [width, setWidth] = useRecoilState(croperWidth)
// const isInpainting = useRecoilValue(isInpaintingState)
const [isResizing, setIsResizing] = useState(false)
const [isMoving, setIsMoving] = useState(false)
useEffect(() => {
setX(Math.round((maxWidth - 512) / 2))
setY(Math.round((maxHeight - 512) / 2))
}, [maxHeight, maxWidth])
const [evData, setEVData] = useState<EVData>({
initX: 0,
initY: 0,
initHeight: 0,
initWidth: 0,
startResizeX: 0,
startResizeY: 0,
ord: "top",
})
const onDragFocus = () => {
console.log("focus")
}
const clampLeftRight = (newX: number, newWidth: number) => {
return clamp(newX, newWidth, x, width, minWidth, maxWidth)
}
const clampTopBottom = (newY: number, newHeight: number) => {
return clamp(newY, newHeight, y, height, minHeight, maxHeight)
}
const onPointerMove = (e: PointerEvent) => {
if (isInpainting) {
return
}
const curX = e.clientX
const curY = e.clientY
const offsetY = Math.round((curY - evData.startResizeY) / scale)
const offsetX = Math.round((curX - evData.startResizeX) / scale)
const moveTop = () => {
const newHeight = evData.initHeight - offsetY
const newY = evData.initY + offsetY
const [clampedY, clampedHeight] = clampTopBottom(newY, newHeight)
setHeight(clampedHeight)
setY(clampedY)
}
const moveBottom = () => {
const newHeight = evData.initHeight + offsetY
const [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight)
setHeight(clampedHeight)
setY(clampedY)
}
const moveLeft = () => {
const newWidth = evData.initWidth - offsetX
const newX = evData.initX + offsetX
const [clampedX, clampedWidth] = clampLeftRight(newX, newWidth)
setWidth(clampedWidth)
setX(clampedX)
}
const moveRight = () => {
const newWidth = evData.initWidth + offsetX
const [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth)
setWidth(clampedWidth)
setX(clampedX)
}
if (isResizing) {
switch (evData.ord) {
case "topleft": {
moveTop()
moveLeft()
break
}
case "topright": {
moveTop()
moveRight()
break
}
case "bottomleft": {
moveBottom()
moveLeft()
break
}
case "bottomright": {
moveBottom()
moveRight()
break
}
case "top": {
moveTop()
break
}
case "right": {
moveRight()
break
}
case "bottom": {
moveBottom()
break
}
case "left": {
moveLeft()
break
}
default:
break
}
}
if (isMoving) {
const newX = evData.initX + offsetX
const newY = evData.initY + offsetY
const [clampedX, clampedWidth] = clampLeftRight(newX, evData.initWidth)
const [clampedY, clampedHeight] = clampTopBottom(newY, evData.initHeight)
setWidth(clampedWidth)
setHeight(clampedHeight)
setX(clampedX)
setY(clampedY)
}
}
const onPointerDone = (e: PointerEvent) => {
if (isResizing) {
setIsResizing(false)
}
if (isMoving) {
setIsMoving(false)
}
}
useEffect(() => {
if (isResizing || isMoving) {
document.addEventListener("pointermove", onPointerMove, DOC_MOVE_OPTS)
document.addEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
document.addEventListener("pointercancel", onPointerDone, DOC_MOVE_OPTS)
return () => {
document.removeEventListener(
"pointermove",
onPointerMove,
DOC_MOVE_OPTS
)
document.removeEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
document.removeEventListener(
"pointercancel",
onPointerDone,
DOC_MOVE_OPTS
)
}
}
}, [isResizing, isMoving, width, height, evData])
const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
const { ord } = (e.target as HTMLElement).dataset
if (ord) {
setIsResizing(true)
setEVData({
initX: x,
initY: y,
initHeight: height,
initWidth: width,
startResizeX: e.clientX,
startResizeY: e.clientY,
ord,
})
}
}
const createCropSelection = () => {
return (
<div
className="drag-elements"
onFocus={onDragFocus}
onPointerDown={onCropPointerDown}
>
<div
className="drag-bar ord-top"
data-ord="top"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-bar ord-right"
data-ord="right"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-bar ord-bottom"
data-ord="bottom"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-bar ord-left"
data-ord="left"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-topleft"
data-ord="topleft"
aria-label="topleft"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-topright"
data-ord="topright"
aria-label="topright"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-bottomleft"
data-ord="bottomleft"
aria-label="bottomleft"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-bottomright"
data-ord="bottomright"
aria-label="bottomright"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-top"
data-ord="top"
aria-label="top"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-right"
data-ord="right"
aria-label="right"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-bottom"
data-ord="bottom"
aria-label="bottom"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-left"
data-ord="left"
aria-label="left"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
</div>
)
}
const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
setIsMoving(true)
setEVData({
initX: x,
initY: y,
initHeight: height,
initWidth: width,
startResizeX: e.clientX,
startResizeY: e.clientY,
ord: "",
})
}
const createInfoBar = () => {
return (
<div
className="border absolute pointer-events-auto text-[1rem] px-[0.8rem] py-[0.2rem] flex items-center justify-center gap-[12px] rounded-full hover:cursor-move"
onPointerDown={onInfoBarPointerDown}
style={{
transform: `scale(${1 / scale})`,
top: `${10 / scale}px`,
left: `${10 / scale}px`,
}}
>
<div>
{width} x {height}
</div>
</div>
)
}
const createBorder = () => {
return (
<div
className="outline-dashed outline-primary"
style={{
height,
width,
outlineWidth: `${DRAG_HANDLE_BORDER / scale}px`,
}}
/>
)
}
return (
<div
className="absolute h-full w-full overflow-hidden pointer-events-none"
style={{ visibility: show ? "visible" : "hidden" }}
>
<div
className="relative pointer-events-none"
style={{ height, width, left: x, top: y }}
>
{createBorder()}
{createInfoBar()}
{createCropSelection()}
</div>
</div>
)
}
export default Cropper

View File

@ -2,6 +2,7 @@ import { SyntheticEvent, useCallback, useEffect, useRef, useState } from "react"
import { CursorArrowRaysIcon } from "@heroicons/react/24/outline" import { CursorArrowRaysIcon } from "@heroicons/react/24/outline"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
import { import {
ReactZoomPanPinchContentRef,
ReactZoomPanPinchRef, ReactZoomPanPinchRef,
TransformComponent, TransformComponent,
TransformWrapper, TransformWrapper,
@ -55,6 +56,8 @@ import { Slider } from "./ui/slider"
import { PluginName } from "@/lib/types" import { PluginName } from "@/lib/types"
import { useHotkeys } from "react-hotkeys-hook" import { useHotkeys } from "react-hotkeys-hook"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import Cropper from "./Cropper"
import { HotkeysEvent } from "react-hotkeys-hook/dist/types"
const TOOLBAR_HEIGHT = 200 const TOOLBAR_HEIGHT = 200
const MIN_BRUSH_SIZE = 10 const MIN_BRUSH_SIZE = 10
@ -193,7 +196,7 @@ export default function Editor(props: EditorProps) {
const windowSize = useWindowSize() const windowSize = useWindowSize()
const windowCenterX = windowSize.width / 2 const windowCenterX = windowSize.width / 2
const windowCenterY = windowSize.height / 2 const windowCenterY = windowSize.height / 2
const viewportRef = useRef<ReactZoomPanPinchRef | null>(null) const viewportRef = useRef<ReactZoomPanPinchContentRef | null>(null)
// Indicates that the image has been loaded and is centered on first load // Indicates that the image has been loaded and is centered on first load
const [initialCentered, setInitialCentered] = useState(false) const [initialCentered, setInitialCentered] = useState(false)
@ -1119,9 +1122,8 @@ export default function Editor(props: EditorProps) {
context, context,
]) ])
const undo = () => { const undo = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
// TODO: prevent default event keyboardEvent.preventDefault()
console.log("undo")
if (runMannually && curLineGroup.length !== 0) { if (runMannually && curLineGroup.length !== 0) {
undoStroke() undoStroke()
} else { } else {
@ -1186,7 +1188,8 @@ export default function Editor(props: EditorProps) {
// draw(newRenders[newRenders.length - 1], []) // draw(newRenders[newRenders.length - 1], [])
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original]) }, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
const redo = () => { const redo = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
keyboardEvent.preventDefault()
if (runMannually && redoCurLines.length !== 0) { if (runMannually && redoCurLines.length !== 0) {
redoStroke() redoStroke()
} else { } else {
@ -1194,29 +1197,12 @@ export default function Editor(props: EditorProps) {
} }
} }
// Handle Cmd+shift+Z useHotkeys("shift+ctrl+z,shift+meta+z", redo, undefined, [
const redoPredicate = (event: KeyboardEvent) => { redoStroke,
const isCmdZ = redoRender,
(event.metaKey || event.ctrlKey) && runMannually,
event.shiftKey && redoCurLines,
event.key.toLowerCase() === "z" ])
// Handle tab switch
if (event.key === "Tab") {
event.preventDefault()
}
if (isCmdZ) {
event.preventDefault()
return true
}
return false
}
// useKey(redoPredicate, redo, undefined, [
// redoStroke,
// redoRender,
// runMannually,
// redoCurLines,
// ])
const disableRedo = () => { const disableRedo = () => {
if (isProcessing) { if (isProcessing) {
@ -1410,12 +1396,13 @@ export default function Editor(props: EditorProps) {
let s = minScale let s = minScale
if (viewportRef.current?.state?.scale !== undefined) { if (viewportRef.current?.state?.scale !== undefined) {
s = viewportRef.current?.state.scale s = viewportRef.current?.state.scale
console.log("!!!!!!")
} }
return s! return s!
} }
const getBrushStyle = (_x: number, _y: number) => { const getBrushStyle = (_x: number, _y: number) => {
const curScale = getCurScale() const curScale = scale
return { return {
width: `${brushSize * curScale}px`, width: `${brushSize * curScale}px`,
height: `${brushSize * curScale}px`, height: `${brushSize * curScale}px`,
@ -1463,7 +1450,12 @@ export default function Editor(props: EditorProps) {
const renderCanvas = () => { const renderCanvas = () => {
return ( return (
<TransformWrapper <TransformWrapper
ref={viewportRef} // ref={viewportRef}
ref={(r) => {
if (r) {
viewportRef.current = r
}
}}
panning={{ disabled: !isPanning, velocityDisabled: true }} panning={{ disabled: !isPanning, velocityDisabled: true }}
wheel={{ step: 0.05 }} wheel={{ step: 0.05 }}
centerZoomedOut centerZoomedOut
@ -1546,14 +1538,15 @@ export default function Editor(props: EditorProps) {
</div> </div>
</div> </div>
{/* <Croper <Cropper
maxHeight={imageHeight} maxHeight={imageHeight}
maxWidth={imageWidth} maxWidth={imageWidth}
minHeight={Math.min(256, imageHeight)} minHeight={Math.min(256, imageHeight)}
minWidth={Math.min(256, imageWidth)} minWidth={Math.min(256, imageWidth)}
scale={scale} scale={scale}
show={isDiffusionModels && settings.showCroper} show={true}
/> */} // show={isDiffusionModels && settings.showCroper}
/>
{/* {isInteractiveSeg ? <InteractiveSeg /> : <></>} */} {/* {isInteractiveSeg ? <InteractiveSeg /> : <></>} */}
</TransformComponent> </TransformComponent>

View File

@ -197,11 +197,12 @@ export default function FileManager(props: Props) {
onClick={() => { onClick={() => {
setFileManagerLayout("masonry") setFileManagerLayout("masonry")
}} }}
className={
fileManagerState.layout !== "masonry" ? "opacity-50" : ""
}
> >
<ViewGridIcon /> <ViewGridIcon
className={
fileManagerState.layout !== "masonry" ? "opacity-50" : ""
}
/>
</IconButton> </IconButton>
</div> </div>
</div> </div>

View File

@ -4,7 +4,6 @@ import { useToggle } from "@uidotdev/usehooks"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,

View File

@ -11,6 +11,13 @@ type FileManagerState = {
searchText: string searchText: string
} }
type CropperState = {
x: number
y: number
width: number
height: number
}
type AppState = { type AppState = {
file: File | null file: File | null
imageHeight: number imageHeight: number
@ -26,6 +33,7 @@ type AppState = {
prompt: string prompt: string
fileManagerState: FileManagerState fileManagerState: FileManagerState
cropperState: CropperState
} }
type AppAction = { type AppAction = {
@ -33,13 +41,19 @@ type AppAction = {
setIsInpainting: (newValue: boolean) => void setIsInpainting: (newValue: boolean) => void
setBrushSize: (newValue: number) => void setBrushSize: (newValue: number) => void
setImageSize: (width: number, height: number) => void setImageSize: (width: number, height: number) => void
setPrompt: (newValue: string) => void
setFileManagerSortBy: (newValue: SortBy) => void setFileManagerSortBy: (newValue: SortBy) => void
setFileManagerSortOrder: (newValue: SortOrder) => void setFileManagerSortOrder: (newValue: SortOrder) => void
setFileManagerLayout: ( setFileManagerLayout: (
newValue: AppState["fileManagerState"]["layout"] newValue: AppState["fileManagerState"]["layout"]
) => void ) => void
setFileManagerSearchText: (newValue: string) => void setFileManagerSearchText: (newValue: string) => void
setPrompt: (newValue: string) => void
setCropperX: (newValue: number) => void
setCropperY: (newValue: number) => void
setCropperWidth: (newValue: number) => void
setCropperHeight: (newValue: number) => void
} }
export const useStore = create<AppState & AppAction>()( export const useStore = create<AppState & AppAction>()(
@ -56,6 +70,12 @@ export const useStore = create<AppState & AppAction>()(
isInteractiveSegRunning: false, isInteractiveSegRunning: false,
interactiveSegClicks: [], interactiveSegClicks: [],
prompt: "", prompt: "",
cropperState: {
x: 0,
y: 0,
width: 0,
height: 0,
},
fileManagerState: { fileManagerState: {
sortBy: SortBy.CTIME, sortBy: SortBy.CTIME,
sortOrder: SortOrder.DESCENDING, sortOrder: SortOrder.DESCENDING,
@ -66,15 +86,18 @@ export const useStore = create<AppState & AppAction>()(
set((state: AppState) => { set((state: AppState) => {
state.isInpainting = newValue state.isInpainting = newValue
}), }),
setFile: (file: File) => setFile: (file: File) =>
set((state: AppState) => { set((state: AppState) => {
// TODO: 清空各种状态 // TODO: 清空各种状态
state.file = file state.file = file
}), }),
setBrushSize: (newValue: number) => setBrushSize: (newValue: number) =>
set((state: AppState) => { set((state: AppState) => {
state.brushSize = newValue state.brushSize = newValue
}), }),
setImageSize: (width: number, height: number) => { setImageSize: (width: number, height: number) => {
// 根据图片尺寸调整 brushSize 的 scale // 根据图片尺寸调整 brushSize 的 scale
set((state: AppState) => { set((state: AppState) => {
@ -83,22 +106,47 @@ export const useStore = create<AppState & AppAction>()(
state.brushSizeScale = Math.max(Math.min(width, height), 512) / 512 state.brushSizeScale = Math.max(Math.min(width, height), 512) / 512
}) })
}, },
setPrompt: (newValue: string) => setPrompt: (newValue: string) =>
set((state: AppState) => { set((state: AppState) => {
state.prompt = newValue state.prompt = newValue
}), }),
setCropperX: (newValue: number) =>
set((state: AppState) => {
state.cropperState.x = newValue
}),
setCropperY: (newValue: number) =>
set((state: AppState) => {
state.cropperState.y = newValue
}),
setCropperWidth: (newValue: number) =>
set((state: AppState) => {
state.cropperState.width = newValue
}),
setCropperHeight: (newValue: number) =>
set((state: AppState) => {
state.cropperState.height = newValue
}),
setFileManagerSortBy: (newValue: SortBy) => setFileManagerSortBy: (newValue: SortBy) =>
set((state: AppState) => { set((state: AppState) => {
state.fileManagerState.sortBy = newValue state.fileManagerState.sortBy = newValue
}), }),
setFileManagerSortOrder: (newValue: SortOrder) => setFileManagerSortOrder: (newValue: SortOrder) =>
set((state: AppState) => { set((state: AppState) => {
state.fileManagerState.sortOrder = newValue state.fileManagerState.sortOrder = newValue
}), }),
setFileManagerLayout: (newValue: "rows" | "masonry") => setFileManagerLayout: (newValue: "rows" | "masonry") =>
set((state: AppState) => { set((state: AppState) => {
state.fileManagerState.layout = newValue state.fileManagerState.layout = newValue
}), }),
setFileManagerSearchText: (newValue: string) => setFileManagerSearchText: (newValue: string) =>
set((state: AppState) => { set((state: AppState) => {
state.fileManagerState.searchText = newValue state.fileManagerState.searchText = newValue