Files
openpylon/src/App.tsx

178 lines
5.8 KiB
TypeScript

import { useState, useEffect, useCallback } from "react";
import { getCurrentWindow, LogicalSize, LogicalPosition } from "@tauri-apps/api/window";
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import { springs, fadeSlideLeft, fadeSlideRight } from "@/lib/motion";
import { useAppStore } from "@/stores/app-store";
import { useBoardStore } from "@/stores/board-store";
import { saveSettings } from "@/lib/storage";
import { AppShell } from "@/components/layout/AppShell";
import { BoardList } from "@/components/boards/BoardList";
import { BoardView } from "@/components/board/BoardView";
import { CommandPalette } from "@/components/command-palette/CommandPalette";
import { SettingsDialog } from "@/components/settings/SettingsDialog";
import { ToastContainer } from "@/components/toast/ToastContainer";
import { ShortcutHelpModal } from "@/components/shortcuts/ShortcutHelpModal";
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
import { useAnnounceStore } from "@/hooks/useAnnounce";
export default function App() {
const initialized = useAppStore((s) => s.initialized);
const init = useAppStore((s) => s.init);
const view = useAppStore((s) => s.view);
const reduceMotion = useAppStore((s) => s.settings.reduceMotion);
const boardTitle = useBoardStore((s) => s.board?.title);
const announcement = useAnnounceStore((s) => s.message);
const [settingsOpen, setSettingsOpen] = useState(false);
const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false);
useEffect(() => {
init().then(() => {
// Restore window state after settings are loaded
const { settings } = useAppStore.getState();
const ws = settings.windowState;
if (ws) {
const appWindow = getCurrentWindow();
if (ws.maximized) {
appWindow.maximize();
} else {
appWindow.setSize(new LogicalSize(ws.width, ws.height));
appWindow.setPosition(new LogicalPosition(ws.x, ws.y));
}
}
});
}, [init]);
// Flush board saves before the app window closes
useEffect(() => {
function handleBeforeUnload() {
useBoardStore.getState().closeBoard();
}
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, []);
// Save window state on resize/move (debounced) so it persists without blocking close
useEffect(() => {
const appWindow = getCurrentWindow();
let timeout: ReturnType<typeof setTimeout> | null = null;
async function saveWindowState() {
const [size, position, maximized] = await Promise.all([
appWindow.outerSize(),
appWindow.outerPosition(),
appWindow.isMaximized(),
]);
const settings = useAppStore.getState().settings;
await saveSettings({
...settings,
windowState: {
x: position.x,
y: position.y,
width: size.width,
height: size.height,
maximized,
},
});
}
function debouncedSave() {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(saveWindowState, 500);
}
const unlistenResize = appWindow.onResized(debouncedSave);
const unlistenMove = appWindow.onMoved(debouncedSave);
return () => {
if (timeout) clearTimeout(timeout);
unlistenResize.then((fn) => fn());
unlistenMove.then((fn) => fn());
};
}, []);
// Listen for custom event to open settings from TopBar or command palette
useEffect(() => {
function handleOpenSettings() {
setSettingsOpen(true);
}
document.addEventListener("open-settings-dialog", handleOpenSettings);
return () => {
document.removeEventListener("open-settings-dialog", handleOpenSettings);
};
}, []);
useEffect(() => {
function handleOpenShortcutHelp() {
setShortcutHelpOpen(true);
}
document.addEventListener("open-shortcut-help", handleOpenShortcutHelp);
return () => {
document.removeEventListener("open-shortcut-help", handleOpenShortcutHelp);
};
}, []);
useEffect(() => {
document.title = boardTitle ? `${boardTitle} - OpenPylon` : "OpenPylon";
}, [boardTitle, view]);
const handleOpenSettings = useCallback(() => {
setSettingsOpen(true);
}, []);
useKeyboardShortcuts();
if (!initialized) {
return (
<div className="flex h-screen items-center justify-center bg-pylon-bg">
<span className="font-heading text-lg text-pylon-text-secondary">
Loading...
</span>
</div>
);
}
return (
<MotionConfig reducedMotion={reduceMotion ? "always" : "user"}>
<div aria-live="assertive" aria-atomic="true" className="sr-only">
{announcement}
</div>
<AppShell>
<AnimatePresence mode="wait">
{view.type === "board-list" ? (
<motion.div
key="board-list"
className="h-full"
variants={fadeSlideRight}
initial="hidden"
animate="visible"
exit="exit"
transition={springs.gentle}
>
<BoardList />
</motion.div>
) : (
<motion.div
key={`board-${view.boardId}`}
className="h-full"
variants={fadeSlideLeft}
initial="hidden"
animate="visible"
exit="exit"
transition={springs.gentle}
>
<BoardView />
</motion.div>
)}
</AnimatePresence>
</AppShell>
<CommandPalette onOpenSettings={handleOpenSettings} />
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
<ToastContainer />
<ShortcutHelpModal open={shortcutHelpOpen} onOpenChange={setShortcutHelpOpen} />
</MotionConfig>
);
}