| 1 | import { useEffect, useState } from "react"; |
| 2 | |
| 3 | import { useDesktopRunner } from "../lib/desktop-runner"; |
| 4 | import { type SectionId, desktopSettings, sectionOf } from "../lib/desktop-settings"; |
| 5 | import { DesktopRunnerMenu } from "./desktop-runner-menu"; |
| 6 | import { DesktopSettingsDialog } from "./desktop-settings"; |
| 7 | |
| 8 | /** Where the dot sits when the app doesn't say: near the right of the title bar, before the window's buttons. */ |
| 9 | const DOT_AT = 0.86; |
| 10 | |
| 11 | /** |
| 12 | * Inside the desktop app, on every page: the app's settings dialog and the |
| 13 | * runner's menu, which this page draws over itself since it fills the |
| 14 | * window under the title bar (lib/desktop-settings.ts). It tells the app it |
| 15 | * can as soon as it mounts, and no longer when it goes, so the app opens |
| 16 | * them here rather than in windows of their own. It draws nothing until |
| 17 | * the app asks. Outside the app, and in an app too old to ask, it is |
| 18 | * nothing at all. |
| 19 | */ |
| 20 | export function DesktopApp() { |
| 21 | // Found once hydrated: the server never has it, and the first render matches the server's. |
| 22 | const [bridge, setBridge] = useState<ReturnType<typeof desktopSettings>>(null); |
| 23 | useEffect(() => setBridge(desktopSettings()), []); |
| 24 | const runner = useDesktopRunner(); |
| 25 | const [settings, setSettings] = useState<{ open: boolean; section: SectionId; check: number }>({ open: false, section: "general", check: 0 }); |
| 26 | const [menu, setMenu] = useState<{ open: boolean; at: number }>({ open: false, at: DOT_AT }); |
| 27 | |
| 28 | useEffect(() => { |
| 29 | if (!bridge) return; |
| 30 | const stopOpen = bridge.onOpen((opening) => { |
| 31 | setMenu((now) => ({ ...now, open: false })); |
| 32 | setSettings((now) => ({ |
| 33 | open: true, |
| 34 | // Opened again with no section named, it opens where it was. |
| 35 | section: opening?.section ? sectionOf(opening.section) : now.open ? now.section : "general", |
| 36 | check: opening?.check ? now.check + 1 : now.check, |
| 37 | })); |
| 38 | }); |
| 39 | const stopMenu = bridge.onRunnerMenu((asked) => { |
| 40 | const at = typeof asked?.at === "number" && Number.isFinite(asked.at) ? asked.at : null; |
| 41 | setMenu((now) => ({ open: asked?.action === "toggle" ? !now.open : true, at: at ?? now.at })); |
| 42 | }); |
| 43 | void bridge.ready(true).catch(() => {}); |
| 44 | return () => { |
| 45 | stopOpen(); |
| 46 | stopMenu(); |
| 47 | void bridge.ready(false).catch(() => {}); |
| 48 | }; |
| 49 | }, [bridge]); |
| 50 | |
| 51 | // The title bar's dot shows whether its menu is open. |
| 52 | useEffect(() => { |
| 53 | bridge?.setRunnerMenuOpen(menu.open); |
| 54 | }, [bridge, menu.open]); |
| 55 | |
| 56 | if (!bridge) return null; |
| 57 | return ( |
| 58 | <> |
| 59 | <DesktopRunnerMenu |
| 60 | open={menu.open} |
| 61 | at={menu.at} |
| 62 | runner={runner} |
| 63 | onOpenChange={(open) => setMenu((now) => ({ ...now, open }))} |
| 64 | onSettings={() => { |
| 65 | setMenu((now) => ({ ...now, open: false })); |
| 66 | setSettings((now) => ({ ...now, open: true, section: "runner" })); |
| 67 | }} |
| 68 | /> |
| 69 | <DesktopSettingsDialog |
| 70 | open={settings.open} |
| 71 | section={settings.section} |
| 72 | check={settings.check} |
| 73 | runner={runner} |
| 74 | onOpenChange={(open) => setSettings((now) => ({ ...now, open }))} |
| 75 | onSection={(section) => setSettings((now) => ({ ...now, section }))} |
| 76 | /> |
| 77 | </> |
| 78 | ); |
| 79 | } |