// apps/web/src/pages/editor-page.tsx import { Monitor } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { CanvasResizeDialog } from "@/components/editor/common/canvas-resize-dialog"; import { AutosaveRecoveryBanner, ExportDialog, saveEditorState, useAutosave, } from "@/components/editor/common/export-dialog"; import { FillDialog } from "@/components/editor/common/fill-dialog"; import { ImageResizeDialog } from "@/components/editor/common/image-resize-dialog"; import { NewDocumentDialog } from "@/components/editor/common/new-document-dialog"; import { HorizontalRuler, VerticalRuler } from "@/components/editor/common/rulers"; import { WelcomeScreen } from "@/components/editor/common/welcome-screen"; import { EditorCanvas } from "@/components/editor/editor-canvas"; import { EditorMenuBar } from "@/components/editor/editor-menu-bar"; import { EditorOptionsBar } from "@/components/editor/editor-options-bar"; import { EditorRightPanel } from "@/components/editor/editor-right-panel"; import { EditorStatusBar } from "@/components/editor/editor-status-bar"; import { EditorToolbar } from "@/components/editor/editor-toolbar"; import { useTranslation } from "@/contexts/i18n-context"; import { useEditorShortcuts } from "@/hooks/use-editor-shortcuts"; import { useMobile } from "@/hooks/use-mobile"; import { usePageTitle } from "@/hooks/use-page-title"; import { useEditorStore } from "@/stores/editor-store"; const SERVER_DECODED_EXTS = new Set(["psd", "tga", "exr", "hdr"]); export function EditorPage() { const { t } = useTranslation(); usePageTitle(t.sidebar.editor); const isMobile = useMobile(); const sourceImageUrl = useEditorStore((s) => s.sourceImageUrl); const isDirty = useEditorStore((s) => s.isDirty); const loadImage = useEditorStore((s) => s.loadImage); const rulersVisible = useEditorStore((s) => s.rulersVisible); const [showExport, setShowExport] = useState(false); const [showCanvasResize, setShowCanvasResize] = useState(false); const [showImageResize, setShowImageResize] = useState(false); const [fillDialogOpen, setFillDialogOpen] = useState(false); const [showNewDocument, setShowNewDocument] = useState(false); // Autosave recovery const { recoveryData, dismissRecovery, restoreRecovery } = useAutosave(); // Issue #10: Shortcuts belong at page level, not canvas level useEditorShortcuts({ onSave: () => saveEditorState(), onExport: () => setShowExport(true), onFillDialog: () => setFillDialogOpen(true), }); // Listen for fill-dialog custom event (dispatched from Shift+Backspace shortcut) useEffect(() => { const handler = () => setFillDialogOpen(true); window.addEventListener("snapotter:open-fill-dialog", handler); return () => window.removeEventListener("snapotter:open-fill-dialog", handler); }, []); useEffect(() => { const handler = (e: BeforeUnloadEvent) => { if (isDirty) { e.preventDefault(); } }; window.addEventListener("beforeunload", handler); return () => window.removeEventListener("beforeunload", handler); }, [isDirty]); const handlePaste = useCallback( (e: ClipboardEvent) => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith("image/")) { e.preventDefault(); const blob = item.getAsFile(); if (!blob) return; const url = URL.createObjectURL(blob); const img = new Image(); img.onload = () => loadImage(url, img.naturalWidth, img.naturalHeight); img.src = url; return; } } }, [loadImage], ); useEffect(() => { document.addEventListener("paste", handlePaste); return () => document.removeEventListener("paste", handlePaste); }, [handlePaste]); const handleOpenImage = useCallback(() => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*,.avif,.psd,.tga,.exr,.hdr"; input.onchange = async () => { const file = input.files?.[0]; if (!file) return; const ext = file.name.split(".").pop()?.toLowerCase() ?? ""; if (SERVER_DECODED_EXTS.has(ext)) { try { const formData = new FormData(); formData.append("file", file); formData.append("settings", JSON.stringify({ format: "png" })); const res = await fetch("/api/v1/tools/convert", { method: "POST", body: formData }); if (!res.ok) throw new Error("Server decode failed"); const json = await res.json(); if (json.downloadUrl) { const imgRes = await fetch(json.downloadUrl); const blob = await imgRes.blob(); const url = URL.createObjectURL(blob); const img = new Image(); img.onload = () => loadImage(url, img.naturalWidth, img.naturalHeight); img.src = url; } } catch (err) { console.error("Failed to decode file via server:", err); } return; } const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => loadImage(url, img.naturalWidth, img.naturalHeight); img.src = url; }; input.click(); }, [loadImage]); useEffect(() => { const params = new URLSearchParams(window.location.search); const url = params.get("url"); if (url) { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => loadImage(url, img.naturalWidth, img.naturalHeight); img.src = url; } }, [loadImage]); if (isMobile) { return (

{t.editor.mobile.heading}

{t.editor.mobile.description}

); } return (

{t.editor.welcome.heading}

setShowNewDocument(true)} onOpenImage={handleOpenImage} onExport={() => setShowExport(true)} onSave={() => saveEditorState()} onCanvasResize={() => setShowCanvasResize(true)} onImageResize={() => setShowImageResize(true)} /> {/* Autosave recovery banner */} {recoveryData && ( )} {/* Horizontal ruler along the top edge */} {rulersVisible && }
{/* Vertical ruler along the left edge */} {rulersVisible && }
setShowCanvasResize(true)} onImageResize={() => setShowImageResize(true)} /> {!sourceImageUrl && }
{showExport && setShowExport(false)} />} setShowCanvasResize(false)} /> setShowImageResize(false)} /> setFillDialogOpen(false)} /> setShowNewDocument(false)} />
); }