|
@@ -8,6 +8,7 @@ import { listen } from "@tauri-apps/api/event"
|
|
|
import { open } from "@tauri-apps/plugin-dialog"
|
|
import { open } from "@tauri-apps/plugin-dialog"
|
|
|
import type Database from "@tauri-apps/plugin-sql"
|
|
import type Database from "@tauri-apps/plugin-sql"
|
|
|
import { defineStore } from "pinia"
|
|
import { defineStore } from "pinia"
|
|
|
|
|
+import { computed, ref } from "vue"
|
|
|
import { useFileSystem } from "~/composables/useFileSystem"
|
|
import { useFileSystem } from "~/composables/useFileSystem"
|
|
|
import { useSettings } from "~/composables/useSettings"
|
|
import { useSettings } from "~/composables/useSettings"
|
|
|
import { fullReindex, indexFile, initIndex, removeFile } from "~/lib/indexer"
|
|
import { fullReindex, indexFile, initIndex, removeFile } from "~/lib/indexer"
|
|
@@ -40,6 +41,7 @@ export const useWorkspaceStore = defineStore("workspace", () => {
|
|
|
|
|
|
|
|
const journals = computed(() => files.value.filter((f) => f.isJournal))
|
|
const journals = computed(() => files.value.filter((f) => f.isJournal))
|
|
|
const pages = computed(() => files.value.filter((f) => !f.isJournal))
|
|
const pages = computed(() => files.value.filter((f) => !f.isJournal))
|
|
|
|
|
+ const recentWorkspaces = ref<string[]>([])
|
|
|
|
|
|
|
|
async function initializeIndex(path: string) {
|
|
async function initializeIndex(path: string) {
|
|
|
db.value = await initIndex(`${path}/index.db`)
|
|
db.value = await initIndex(`${path}/index.db`)
|
|
@@ -90,33 +92,128 @@ export const useWorkspaceStore = defineStore("workspace", () => {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async function initialize(): Promise<boolean> {
|
|
|
|
|
|
|
+ /** Result of attempting to restore a previously-saved workspace. */
|
|
|
|
|
+ type InitResult =
|
|
|
|
|
+ | { status: "ok" }
|
|
|
|
|
+ | { status: "stale"; path: string }
|
|
|
|
|
+ | { status: "missing" }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Restore the saved workspace if the path still exists.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Checks the persisted `workspacePath` setting, verifies the directory
|
|
|
|
|
+ * is still accessible, then re-indexes the workspace and starts the
|
|
|
|
|
+ * file watcher. The caller should handle each status:
|
|
|
|
|
+ *
|
|
|
|
|
+ * - `"ok"` — workspace loaded, app can render normally
|
|
|
|
|
+ * - `"stale"` — path was saved but the folder is gone, prompt user
|
|
|
|
|
+ * - `"missing"` — no saved path (first launch), show onboarding
|
|
|
|
|
+ */
|
|
|
|
|
+ async function initialize(): Promise<InitResult> {
|
|
|
const saved = await settings.get<string>("workspacePath")
|
|
const saved = await settings.get<string>("workspacePath")
|
|
|
- if (saved) {
|
|
|
|
|
- workspacePath.value = saved
|
|
|
|
|
- const sessionFile = await settings.get<string>("currentFilePath")
|
|
|
|
|
- if (sessionFile) currentFilePath.value = sessionFile
|
|
|
|
|
- await scanFiles()
|
|
|
|
|
- await initializeIndex(saved)
|
|
|
|
|
- return true
|
|
|
|
|
|
|
+ if (!saved) return { status: "missing" }
|
|
|
|
|
+
|
|
|
|
|
+ // Verify stored path still exists
|
|
|
|
|
+ try {
|
|
|
|
|
+ await invoke("list_directory", { path: saved })
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return { status: "stale", path: saved }
|
|
|
}
|
|
}
|
|
|
- return false
|
|
|
|
|
|
|
+
|
|
|
|
|
+ workspacePath.value = saved
|
|
|
|
|
+ const sessionFile = await settings.get<string>("currentFilePath")
|
|
|
|
|
+ if (sessionFile) currentFilePath.value = sessionFile
|
|
|
|
|
+ await scanFiles()
|
|
|
|
|
+ await initializeIndex(saved)
|
|
|
|
|
+ return { status: "ok" }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Open a native directory picker for the user to select a workspace.
|
|
|
|
|
+ * Returns the selected path or `null` if the user cancelled.
|
|
|
|
|
+ */
|
|
|
async function pickWorkspace(): Promise<string | null> {
|
|
async function pickWorkspace(): Promise<string | null> {
|
|
|
- const selected = await open({
|
|
|
|
|
|
|
+ return await open({
|
|
|
directory: true,
|
|
directory: true,
|
|
|
multiple: false,
|
|
multiple: false,
|
|
|
title: "Select your workspace directory",
|
|
title: "Select your workspace directory",
|
|
|
})
|
|
})
|
|
|
- if (!selected) return null
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- workspacePath.value = selected
|
|
|
|
|
- await settings.set("workspacePath", selected)
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Open a native dialog for the user to create a workspace in a new folder.
|
|
|
|
|
+ * Returns the selected path or `null` if cancelled.
|
|
|
|
|
+ */
|
|
|
|
|
+ async function createWorkspace(): Promise<string | null> {
|
|
|
|
|
+ return await open({
|
|
|
|
|
+ directory: true,
|
|
|
|
|
+ multiple: false,
|
|
|
|
|
+ title: "Choose a folder for your new workspace",
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Initialize and persist a workspace at the given path.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Creates the standard subdirectory layout (journals/, pages/, templates/),
|
|
|
|
|
+ * scans for existing markdown files, builds the SQLite index, and starts
|
|
|
|
|
+ * the file watcher. Persists the path to settings so `initialize()` can
|
|
|
|
|
+ * restore it on next launch.
|
|
|
|
|
+ *
|
|
|
|
|
+ * When `options.isNew` is true and the chosen folder is empty, a one-time
|
|
|
|
|
+ * welcome note is written to `pages/welcome.md`.
|
|
|
|
|
+ */
|
|
|
|
|
+ async function loadRecentWorkspaces(): Promise<void> {
|
|
|
|
|
+ recentWorkspaces.value = await settings.getRecentWorkspaces()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function trackRecentWorkspace(path: string): Promise<void> {
|
|
|
|
|
+ await settings.addRecentWorkspace(path)
|
|
|
|
|
+ recentWorkspaces.value = await settings.getRecentWorkspaces()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function setWorkspace(
|
|
|
|
|
+ path: string,
|
|
|
|
|
+ _options?: { isNew?: boolean },
|
|
|
|
|
+ ): Promise<void> {
|
|
|
|
|
+ workspacePath.value = path
|
|
|
|
|
+ await settings.set("workspacePath", path)
|
|
|
await ensureDirectories()
|
|
await ensureDirectories()
|
|
|
await scanFiles()
|
|
await scanFiles()
|
|
|
- await initializeIndex(selected)
|
|
|
|
|
- return selected
|
|
|
|
|
|
|
+ await initializeIndex(path)
|
|
|
|
|
+ await trackRecentWorkspace(path)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Switch to a different workspace, tearing down the current one first.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Drops the SQLite pool reference, resets in-memory state, then
|
|
|
|
|
+ * initializes the new workspace at the given path.
|
|
|
|
|
+ */
|
|
|
|
|
+ async function switchWorkspace(path: string): Promise<void> {
|
|
|
|
|
+ // Tear down current state
|
|
|
|
|
+ db.value = null
|
|
|
|
|
+ files.value = []
|
|
|
|
|
+ workspacePath.value = null
|
|
|
|
|
+ currentFilePath.value = null
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize the new workspace
|
|
|
|
|
+ await setWorkspace(path)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Close the current workspace and return to uninitialized state.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Drops the SQLite pool reference, resets in-memory state, and clears
|
|
|
|
|
+ * the persisted workspace path so the welcome screen shows on next
|
|
|
|
|
+ * render.
|
|
|
|
|
+ */
|
|
|
|
|
+ async function closeWorkspace(): Promise<void> {
|
|
|
|
|
+ db.value = null
|
|
|
|
|
+ files.value = []
|
|
|
|
|
+ workspacePath.value = null
|
|
|
|
|
+ currentFilePath.value = null
|
|
|
|
|
+ await settings.set("workspacePath", null)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function ensureDirectories() {
|
|
async function ensureDirectories() {
|
|
@@ -189,8 +286,14 @@ export const useWorkspaceStore = defineStore("workspace", () => {
|
|
|
currentFilePath,
|
|
currentFilePath,
|
|
|
loading,
|
|
loading,
|
|
|
db,
|
|
db,
|
|
|
|
|
+ recentWorkspaces,
|
|
|
initialize,
|
|
initialize,
|
|
|
pickWorkspace,
|
|
pickWorkspace,
|
|
|
|
|
+ createWorkspace,
|
|
|
|
|
+ setWorkspace,
|
|
|
|
|
+ switchWorkspace,
|
|
|
|
|
+ closeWorkspace,
|
|
|
|
|
+ loadRecentWorkspaces,
|
|
|
scanFiles,
|
|
scanFiles,
|
|
|
loadFile,
|
|
loadFile,
|
|
|
saveFile,
|
|
saveFile,
|
|
@@ -199,3 +302,12 @@ export const useWorkspaceStore = defineStore("workspace", () => {
|
|
|
openFile,
|
|
openFile,
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Discriminated union returned by {@link useWorkspaceStore.initialize | initialize()}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * - `"ok"` — workspace restored, app ready
|
|
|
|
|
+ * - `"stale"` — saved path no longer accessible, user should pick again
|
|
|
|
|
+ * - `"missing"` — no saved path (first launch)
|
|
|
|
|
+ */
|
|
|
|
|
+export type { InitResult }
|