initial commit
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
import { useEffect, useState, useRef } from "react"
|
||||
import { Toolbar } from "./components/Toolbar"
|
||||
import { TitleBar } from "./components/TitleBar"
|
||||
import { LeftSidebar } from "./components/LeftSidebar"
|
||||
import { CanvasView } from "./components/CanvasView"
|
||||
import { PropertiesPanel } from "./components/PropertiesPanel"
|
||||
import { StatusBar } from "./components/StatusBar"
|
||||
import { KeyboardCheatSheet } from "./components/KeyboardCheatSheet"
|
||||
import { ResizeHandle } from "./components/ResizeHandle"
|
||||
import { useStore } from "./store/useStore"
|
||||
import { loadTemplateDialog } from "./utils/loadFile"
|
||||
|
||||
const MIN_SIDEBAR = 160
|
||||
const MAX_SIDEBAR = 500
|
||||
|
||||
export default function App() {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [showCheatSheet, setShowCheatSheet] = useState(false)
|
||||
const [leftWidth, setLeftWidth] = useState(280)
|
||||
const [rightWidth, setRightWidth] = useState(320)
|
||||
const [leftCollapsed, setLeftCollapsed] = useState(false)
|
||||
const [rightCollapsed, setRightCollapsed] = useState(false)
|
||||
const setToolMode = useStore((s) => s.setToolMode)
|
||||
const undo = useStore((s) => s.undo)
|
||||
const deleteSection = useStore((s) => s.deleteSection)
|
||||
const selectedSectionIdx = useStore((s) => s.selectedSectionIdx)
|
||||
const toolMode = useStore((s) => s.toolMode)
|
||||
const currentBlockName = useStore((s) => s.currentBlockName)
|
||||
const darkMode = useStore((s) => s.darkMode)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle("dark", darkMode)
|
||||
window.electronAPI?.setTheme(darkMode)
|
||||
}, [darkMode])
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") return
|
||||
|
||||
const ctrl = e.ctrlKey || e.metaKey
|
||||
|
||||
if (e.key === "Escape") { setShowCheatSheet(false); return }
|
||||
if (e.key === "?") { e.preventDefault(); setShowCheatSheet((v) => !v); return }
|
||||
|
||||
switch (e.key.toLowerCase()) {
|
||||
case "s": {
|
||||
e.preventDefault()
|
||||
if (ctrl) {
|
||||
const state = useStore.getState()
|
||||
const errors = state.validateExport()
|
||||
if (errors.length > 0) {
|
||||
alert("Validation errors:\n" + errors.map((e) => ` - ${e}`).join("\n"))
|
||||
return
|
||||
}
|
||||
const data = state.buildExportData()
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.saveFileDialog("metadata.json", [{ name: "JSON", extensions: ["json"] }])
|
||||
.then((path: string) => {
|
||||
if (!path) { state.setStatusMessage("Export cancelled"); return }
|
||||
return window.electronAPI!.writeFile(path, json)
|
||||
})
|
||||
.then(() => state.setStatusMessage("Exported successfully"))
|
||||
.catch((err: Error) => { state.setStatusMessage("Export failed"); console.error(err) })
|
||||
} else {
|
||||
const blob = new Blob([json], { type: "application/json" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "metadata.json"
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
state.setStatusMessage("Exported metadata.json")
|
||||
}
|
||||
} else {
|
||||
setToolMode("select")
|
||||
}
|
||||
break
|
||||
}
|
||||
case "o":
|
||||
if (ctrl) {
|
||||
e.preventDefault()
|
||||
loadTemplateDialog().then((result) => {
|
||||
if (!result) return
|
||||
useStore.getState().setTemplate(result.dataUrl, result.filePath, result.width, result.height)
|
||||
useStore.getState().setStatusMessage(`Loaded: ${result.name}`)
|
||||
})
|
||||
}
|
||||
break
|
||||
case "z":
|
||||
if (ctrl) {
|
||||
e.preventDefault()
|
||||
undo()
|
||||
}
|
||||
break
|
||||
case "delete":
|
||||
case "backspace":
|
||||
if (selectedSectionIdx !== null) {
|
||||
e.preventDefault()
|
||||
deleteSection(selectedSectionIdx)
|
||||
}
|
||||
break
|
||||
case "d":
|
||||
if (!ctrl) { e.preventDefault(); setToolMode("draw") }
|
||||
break
|
||||
case "p":
|
||||
if (!ctrl) { e.preventDefault(); setToolMode("pan") }
|
||||
break
|
||||
case "arrowup":
|
||||
case "arrowdown":
|
||||
case "arrowleft":
|
||||
case "arrowright":
|
||||
if (toolMode === "select" && selectedSectionIdx !== null && currentBlockName) {
|
||||
e.preventDefault()
|
||||
const step = e.shiftKey ? 10 : 1
|
||||
const state = useStore.getState()
|
||||
const block = state.blocks[currentBlockName]
|
||||
if (!block) break
|
||||
const roi = block.sections[selectedSectionIdx]
|
||||
if (!roi) break
|
||||
const [x1, x2, y1, y2] = roi.cords
|
||||
let nx1 = x1, nx2 = x2, ny1 = y1, ny2 = y2
|
||||
switch (e.key.toLowerCase()) {
|
||||
case "arrowup": ny1 -= step; ny2 -= step; break
|
||||
case "arrowdown": ny1 += step; ny2 += step; break
|
||||
case "arrowleft": nx1 -= step; nx2 -= step; break
|
||||
case "arrowright": nx1 += step; nx2 += step; break
|
||||
}
|
||||
state.pushUndo()
|
||||
state.updateSectionCords(selectedSectionIdx, [nx1, nx2, ny1, ny2])
|
||||
state.setStatusMessage(`Nudged to (${nx1}, ${ny1})`)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown)
|
||||
return () => window.removeEventListener("keydown", onKeyDown)
|
||||
}, [setToolMode, undo, deleteSection, selectedSectionIdx, toolMode, currentBlockName])
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col overflow-hidden">
|
||||
<TitleBar />
|
||||
<Toolbar />
|
||||
<div ref={containerRef} className="flex flex-1 overflow-hidden">
|
||||
<LeftSidebar width={leftCollapsed ? 32 : leftWidth} collapsed={leftCollapsed} onToggle={() => setLeftCollapsed((v) => !v)} />
|
||||
{!leftCollapsed && <ResizeHandle onResize={(clientX) => setLeftWidth(Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, clientX)))} />}
|
||||
<CanvasView />
|
||||
{!rightCollapsed && <ResizeHandle onResize={(clientX) => {
|
||||
if (!containerRef.current) return
|
||||
const cr = containerRef.current.getBoundingClientRect()
|
||||
setRightWidth(Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, cr.right - clientX)))
|
||||
}} />}
|
||||
<PropertiesPanel width={rightCollapsed ? 32 : rightWidth} collapsed={rightCollapsed} onToggle={() => setRightCollapsed((v) => !v)} />
|
||||
</div>
|
||||
<StatusBar />
|
||||
{showCheatSheet && <KeyboardCheatSheet onClose={() => setShowCheatSheet(false)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user