| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- <script setup lang="ts">
- /**
- * App root.
- *
- * Three mutually-exclusive render states:
- * 1. **Loading** — branded splash while workspace initializes
- * 2. **Welcome** — no workspace configured or saved path is stale
- * 3. **Ready** — workspace loaded, normal layout renders
- *
- * The native File menu is always available once the app is ready,
- * even during the welcome screen. Its actions transition state
- * from welcome → ready via hasWorkspace.
- */
- import { invoke } from "@tauri-apps/api/core"
- import { Menu, MenuItem, Submenu } from "@tauri-apps/api/menu"
- import { nextTick, watch } from "vue"
- import WelcomeScreen from "~/components/WelcomeScreen.vue"
- import { useFileSystem } from "~/composables/useFileSystem"
- import type { InitResult } from "~/stores/workspace"
- import { useWorkspaceStore } from "~/stores/workspace"
- const workspace = useWorkspaceStore()
- const fs = useFileSystem()
- const ready = ref(false)
- const hasWorkspace = ref(false)
- const stalePath = ref<string | null>(null)
- async function handleMenuNewWorkspace() {
- const path = await workspace.pickWorkspace()
- if (!path) return
- await workspace.setWorkspace(path)
- hasWorkspace.value = true
- }
- async function handleMenuOpenWorkspace() {
- const path = await workspace.pickWorkspace()
- if (!path) return
- await workspace.switchWorkspace(path)
- hasWorkspace.value = true
- }
- async function handleMenuOpenRecent(path: string) {
- await workspace.switchWorkspace(path)
- hasWorkspace.value = true
- }
- async function handleCloseWorkspace() {
- await workspace.closeWorkspace()
- stalePath.value = null
- hasWorkspace.value = false
- }
- async function handleReindex() {
- const toast = useToast()
- const { id } = toast.add({
- title: "Rebuilding index…",
- duration: 0,
- close: false,
- color: "neutral",
- })
- try {
- await workspace.reindex()
- toast.update(id, {
- title: "Index rebuilt",
- color: "success",
- duration: 3000,
- close: true,
- })
- } catch {
- toast.update(id, {
- title: "Indexing failed",
- color: "error",
- duration: 5000,
- close: true,
- })
- }
- }
- async function handleQuit() {
- await invoke("exit_app")
- }
- async function buildMenu() {
- const fileItems: (MenuItem | Submenu | { item: "Separator" })[] = [
- await MenuItem.new({
- id: "new-workspace",
- text: "New",
- accelerator: "CmdOrCtrl+Shift+N",
- action: handleMenuNewWorkspace,
- }),
- await MenuItem.new({
- id: "open-workspace",
- text: "Open",
- accelerator: "CmdOrCtrl+O",
- action: handleMenuOpenWorkspace,
- }),
- { item: "Separator" },
- ]
- // Recent workspaces as a submenu
- const recent = workspace.recentWorkspaces
- const validItems: MenuItem[] = []
- for (const p of recent) {
- if (await fs.pathExists(p)) {
- validItems.push(
- await MenuItem.new({
- id: `recent:${p}`,
- text: p.split("/").pop() ?? p,
- action: () => handleMenuOpenRecent(p),
- }),
- )
- }
- }
- if (validItems.length > 0) {
- const recentSubmenu = await Submenu.new({
- text: "Recent",
- items: validItems,
- })
- fileItems.push(recentSubmenu)
- }
- fileItems.push(
- { item: "Separator" },
- await MenuItem.new({
- id: "close-workspace",
- text: "Close",
- accelerator: "CmdOrCtrl+W",
- action: handleCloseWorkspace,
- }),
- { item: "Separator" },
- await MenuItem.new({
- id: "reindex-library",
- text: "Reindex Library",
- accelerator: "CmdOrCtrl+Shift+R",
- action: handleReindex,
- }),
- { item: "Separator" },
- await MenuItem.new({
- id: "quit",
- text: "Quit Enesis",
- accelerator: "CmdOrCtrl+Q",
- action: handleQuit,
- }),
- )
- const fileSubmenu = await Submenu.new({
- text: "File",
- items: fileItems,
- })
- const menu = await Menu.new({ items: [fileSubmenu] })
- await menu.setAsAppMenu()
- }
- onMounted(async () => {
- const result: InitResult = await workspace.initialize()
- if (result.status === "ok") {
- hasWorkspace.value = true
- } else if (result.status === "stale") {
- stalePath.value = result.path
- }
- await workspace.loadRecentWorkspaces()
- ready.value = true
- })
- // Remove the SPA splash overlay once the view has rendered
- watch(ready, (isReady) => {
- if (isReady) nextTick(() => document.getElementById("splash")?.remove())
- }, { once: true })
- // Build the menu once the app is ready — always visible
- watch(
- ready,
- async (r) => {
- if (r) await buildMenu()
- },
- { once: true },
- )
- // Rebuild menu when recent workspaces change
- watch(
- () => workspace.recentWorkspaces,
- async () => {
- if (ready.value) await buildMenu()
- },
- { deep: true },
- )
- </script>
- <template>
- <UApp>
- <!-- 1. Needs onboarding — no workspace or stale path -->
- <WelcomeScreen
- v-if="ready && !hasWorkspace"
- :stale-path="stalePath"
- :recent-paths="workspace.recentWorkspaces"
- @complete="hasWorkspace = true"
- />
- <!-- 2. Ready — normal app chrome -->
- <NuxtLayout v-else-if="ready && hasWorkspace">
- <NuxtPage :key="workspace.workspacePath" />
- </NuxtLayout>
- <!-- 3. Initializing — nothing rendered (SPA template covers this phase) -->
- </UApp>
- </template>
|