app.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. <script setup lang="ts">
  2. /**
  3. * App root.
  4. *
  5. * Three mutually-exclusive render states:
  6. * 1. **Loading** — branded splash while workspace initializes
  7. * 2. **Welcome** — no workspace configured or saved path is stale
  8. * 3. **Ready** — workspace loaded, normal layout renders
  9. *
  10. * The native File menu is always available once the app is ready,
  11. * even during the welcome screen. Its actions transition state
  12. * from welcome → ready via hasWorkspace.
  13. */
  14. import { invoke } from "@tauri-apps/api/core"
  15. import { Menu, MenuItem, Submenu } from "@tauri-apps/api/menu"
  16. import { nextTick, watch } from "vue"
  17. import WelcomeScreen from "~/components/WelcomeScreen.vue"
  18. import { useFileSystem } from "~/composables/useFileSystem"
  19. import type { InitResult } from "~/stores/workspace"
  20. import { useWorkspaceStore } from "~/stores/workspace"
  21. const workspace = useWorkspaceStore()
  22. const fs = useFileSystem()
  23. const ready = ref(false)
  24. const hasWorkspace = ref(false)
  25. const stalePath = ref<string | null>(null)
  26. async function handleMenuNewWorkspace() {
  27. const path = await workspace.pickWorkspace()
  28. if (!path) return
  29. await workspace.setWorkspace(path)
  30. hasWorkspace.value = true
  31. }
  32. async function handleMenuOpenWorkspace() {
  33. const path = await workspace.pickWorkspace()
  34. if (!path) return
  35. await workspace.switchWorkspace(path)
  36. hasWorkspace.value = true
  37. }
  38. async function handleMenuOpenRecent(path: string) {
  39. await workspace.switchWorkspace(path)
  40. hasWorkspace.value = true
  41. }
  42. async function handleCloseWorkspace() {
  43. await workspace.closeWorkspace()
  44. stalePath.value = null
  45. hasWorkspace.value = false
  46. }
  47. async function handleReindex() {
  48. const toast = useToast()
  49. const { id } = toast.add({
  50. title: "Rebuilding index…",
  51. duration: 0,
  52. close: false,
  53. color: "neutral",
  54. })
  55. try {
  56. await workspace.reindex()
  57. toast.update(id, {
  58. title: "Index rebuilt",
  59. color: "success",
  60. duration: 3000,
  61. close: true,
  62. })
  63. } catch {
  64. toast.update(id, {
  65. title: "Indexing failed",
  66. color: "error",
  67. duration: 5000,
  68. close: true,
  69. })
  70. }
  71. }
  72. async function handleQuit() {
  73. await invoke("exit_app")
  74. }
  75. async function buildMenu() {
  76. const fileItems: (MenuItem | Submenu | { item: "Separator" })[] = [
  77. await MenuItem.new({
  78. id: "new-workspace",
  79. text: "New",
  80. accelerator: "CmdOrCtrl+Shift+N",
  81. action: handleMenuNewWorkspace,
  82. }),
  83. await MenuItem.new({
  84. id: "open-workspace",
  85. text: "Open",
  86. accelerator: "CmdOrCtrl+O",
  87. action: handleMenuOpenWorkspace,
  88. }),
  89. { item: "Separator" },
  90. ]
  91. // Recent workspaces as a submenu
  92. const recent = workspace.recentWorkspaces
  93. const validItems: MenuItem[] = []
  94. for (const p of recent) {
  95. if (await fs.pathExists(p)) {
  96. validItems.push(
  97. await MenuItem.new({
  98. id: `recent:${p}`,
  99. text: p.split("/").pop() ?? p,
  100. action: () => handleMenuOpenRecent(p),
  101. }),
  102. )
  103. }
  104. }
  105. if (validItems.length > 0) {
  106. const recentSubmenu = await Submenu.new({
  107. text: "Recent",
  108. items: validItems,
  109. })
  110. fileItems.push(recentSubmenu)
  111. }
  112. fileItems.push(
  113. { item: "Separator" },
  114. await MenuItem.new({
  115. id: "close-workspace",
  116. text: "Close",
  117. accelerator: "CmdOrCtrl+W",
  118. action: handleCloseWorkspace,
  119. }),
  120. { item: "Separator" },
  121. await MenuItem.new({
  122. id: "reindex-library",
  123. text: "Reindex Library",
  124. accelerator: "CmdOrCtrl+Shift+R",
  125. action: handleReindex,
  126. }),
  127. { item: "Separator" },
  128. await MenuItem.new({
  129. id: "quit",
  130. text: "Quit Enesis",
  131. accelerator: "CmdOrCtrl+Q",
  132. action: handleQuit,
  133. }),
  134. )
  135. const fileSubmenu = await Submenu.new({
  136. text: "File",
  137. items: fileItems,
  138. })
  139. const menu = await Menu.new({ items: [fileSubmenu] })
  140. await menu.setAsAppMenu()
  141. }
  142. onMounted(async () => {
  143. const result: InitResult = await workspace.initialize()
  144. if (result.status === "ok") {
  145. hasWorkspace.value = true
  146. } else if (result.status === "stale") {
  147. stalePath.value = result.path
  148. }
  149. await workspace.loadRecentWorkspaces()
  150. ready.value = true
  151. })
  152. // Remove the SPA splash overlay once the view has rendered
  153. watch(ready, (isReady) => {
  154. if (isReady) nextTick(() => document.getElementById("splash")?.remove())
  155. }, { once: true })
  156. // Build the menu once the app is ready — always visible
  157. watch(
  158. ready,
  159. async (r) => {
  160. if (r) await buildMenu()
  161. },
  162. { once: true },
  163. )
  164. // Rebuild menu when recent workspaces change
  165. watch(
  166. () => workspace.recentWorkspaces,
  167. async () => {
  168. if (ready.value) await buildMenu()
  169. },
  170. { deep: true },
  171. )
  172. </script>
  173. <template>
  174. <UApp>
  175. <!-- 1. Needs onboarding — no workspace or stale path -->
  176. <WelcomeScreen
  177. v-if="ready && !hasWorkspace"
  178. :stale-path="stalePath"
  179. :recent-paths="workspace.recentWorkspaces"
  180. @complete="hasWorkspace = true"
  181. />
  182. <!-- 2. Ready — normal app chrome -->
  183. <NuxtLayout v-else-if="ready && hasWorkspace">
  184. <NuxtPage :key="workspace.workspacePath" />
  185. </NuxtLayout>
  186. <!-- 3. Initializing — nothing rendered (SPA template covers this phase) -->
  187. </UApp>
  188. </template>