diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 761e458..d4535e2 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1385,6 +1385,7 @@ pub fn set_general_settings( close_to_tray: bool, enable_wiki_links: bool, show_note_dates: bool, + startup_view: StartupView, restore_last_session: bool, show_all_notes: bool, show_quick_access: bool, @@ -1395,6 +1396,7 @@ pub fn set_general_settings( let mut config = state.config.lock().map_err(|e| e.to_string())?; config.compact_notes = compact_notes; config.show_note_dates = show_note_dates; + config.startup_view = startup_view; config.restore_last_session = restore_last_session; config.show_all_notes = show_all_notes; config.show_quick_access = show_quick_access; diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index d67ae57..db4cebe 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -96,6 +96,17 @@ pub struct CustomTheme { pub colors: CustomThemeColors, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum StartupView { + Daily, + QuickAccess, + Tasks, + #[default] + #[serde(other)] + All, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { pub vaults: Vec, @@ -192,6 +203,8 @@ pub struct AppConfig { #[serde(default = "default_true")] pub enable_wiki_links: bool, #[serde(default)] + pub startup_view: StartupView, + #[serde(default)] pub restore_last_session: bool, // DEPRECATED: WebDAV sync moved to per-vault VaultConfig. Kept for one release to migrate old configs. #[serde(default)] @@ -305,6 +318,7 @@ impl Default for AppConfig { show_tray_icon: false, close_to_tray: false, enable_wiki_links: true, + startup_view: StartupView::All, restore_last_session: false, sync_provider: None, webdav_url: None, @@ -469,3 +483,28 @@ pub struct TaskItem { pub due: Option, pub priority: Option, } + +#[cfg(test)] +mod startup_view_tests { + use super::StartupView; + + #[test] + fn serializes_supported_startup_views() { + for (view, expected) in [ + (StartupView::All, "\"all\""), + (StartupView::QuickAccess, "\"quickaccess\""), + (StartupView::Tasks, "\"tasks\""), + (StartupView::Daily, "\"daily\""), + ] { + assert_eq!(serde_json::to_string(&view).unwrap(), expected); + } + } + + #[test] + fn unknown_startup_view_falls_back_to_all_notes() { + assert_eq!( + serde_json::from_str::("\"future-view\"").unwrap(), + StartupView::All + ); + } +} diff --git a/src/lib/api.ts b/src/lib/api.ts index eb5b5c8..935ab14 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -16,6 +16,7 @@ import type { VersionEntry, TaskItem, ExternalVaultResult, + StartupView, } from "./types"; export async function openVault(path: string): Promise { @@ -264,6 +265,7 @@ export async function setGeneralSettings( closeToTray: boolean, enableWikiLinks: boolean, showNoteDates: boolean, + startupView: StartupView, restoreLastSession: boolean, showAllNotes: boolean, showQuickAccess: boolean, @@ -289,6 +291,7 @@ export async function setGeneralSettings( closeToTray, enableWikiLinks, showNoteDates, + startupView, restoreLastSession, showAllNotes, showQuickAccess, diff --git a/src/lib/components/AppLayout.svelte b/src/lib/components/AppLayout.svelte index 5cad8c4..ce39468 100644 --- a/src/lib/components/AppLayout.svelte +++ b/src/lib/components/AppLayout.svelte @@ -65,8 +65,10 @@ import { darkThemes, isAndroid } from '$lib/platform'; import { debounce } from '$lib/utils/debounce'; import { openNoteWindow } from '$lib/utils/window'; + import { normalizeStartupView, resolveStartupTarget } from '$lib/utils/startup-view'; import { get } from 'svelte/store'; import type { VaultState, FileEvent, NotebookEntry, TaskItem } from '$lib/types'; + import type { StartupTarget } from '$lib/utils/startup-view'; function findNotebookByPath(list: NotebookEntry[], relPath: string): NotebookEntry | null { for (const nb of list) { @@ -81,6 +83,38 @@ let noteList: NoteList; let editor: Editor; let unlistenFileChange: (() => void) | null = null; + async function applyStartupTarget(target: StartupTarget): Promise { + if (target.mode === 'notebook') { + const vault = $appConfig?.active_vault; + const notebook = target.notebookPath === '' && vault + ? { name: 'Unfiled Notes', path: vault, relative_path: '', children: [], note_count: $rootNoteCount } + : findNotebookByPath($notebooks, target.notebookPath); + if (!notebook) return false; + const changed = $viewMode !== 'notebook' || $activeNotebook?.relative_path !== notebook.relative_path; + $viewMode = 'notebook'; + $activeNotebook = notebook; + $activeTag = null; + if (changed) await noteList?.refresh(); + return true; + } + + if (target.mode === 'tag') { + const changed = $viewMode !== 'tag' || $activeTag !== target.tag; + $viewMode = 'tag'; + $activeTag = target.tag; + $activeNotebook = null; + if (changed) await noteList?.refresh(); + return true; + } + + const changed = $viewMode !== target.mode || $activeNotebook !== null || $activeTag !== null; + $viewMode = target.mode; + $activeNotebook = null; + $activeTag = null; + if (changed) await noteList?.refresh(); + return true; + } + let unlistenOpenFile: (() => void) | null = null; // Mobile editor header helpers @@ -599,9 +633,11 @@ lastTag = state.last_tag ?? null; } catch (_) {} + const restoreLastSession = $appConfig?.restore_last_session === true; + // On mobile, prefetch last-opened note so first tap is instant let prefetchPromise: Promise | null = null; - if (isMobile && lastNotePath) { + if (isMobile && restoreLastSession && lastNotePath) { prefetchPromise = readNote(lastNotePath).catch(() => null); } @@ -637,32 +673,26 @@ }, 3000); } - // Restore the last session (view + open note) if enabled. - if ($appConfig?.restore_last_session) { - const vault = $appConfig?.active_vault; - if (lastViewMode === 'notebook' && lastNotebook === '' && vault) { - $viewMode = 'notebook'; - $activeNotebook = { name: 'Unfiled Notes', path: vault, relative_path: '', children: [], note_count: $rootNoteCount }; - $activeTag = null; - await noteList?.refresh(); - } else if (lastViewMode === 'notebook' && lastNotebook) { - const nb = findNotebookByPath($notebooks, lastNotebook); - if (nb) { $viewMode = 'notebook'; $activeNotebook = nb; $activeTag = null; await noteList?.refresh(); } - } else if (lastViewMode === 'tag' && lastTag) { - $viewMode = 'tag'; $activeTag = lastTag; $activeNotebook = null; await noteList?.refresh(); - } else if (lastViewMode === 'quickaccess') { - $viewMode = 'quickaccess'; $activeNotebook = null; $activeTag = null; await noteList?.refresh(); - } - // Open the last note on desktop (mobile reopens it via its own prefetch path below). - if (!isMobile && lastNotePath) { - try { - const content = await readNote(lastNotePath); - $activeNote = content; - $activeNotePath = lastNotePath; - $editorDirty = false; - editor?.loadNote(lastNotePath, content.content); - } catch (_) {} - } + const startupTarget = resolveStartupTarget({ + startupView: $appConfig?.startup_view, + restoreLastSession, + lastViewMode, + lastNotebook, + lastTag + }); + if (!(await applyStartupTarget(startupTarget))) { + await applyStartupTarget({ mode: normalizeStartupView($appConfig?.startup_view) }); + } + + // Reopen the last note only when session restoration is enabled. + if (restoreLastSession && !isMobile && lastNotePath) { + try { + const content = await readNote(lastNotePath); + $activeNote = content; + $activeNotePath = lastNotePath; + $editorDirty = false; + editor?.loadNote(lastNotePath, content.content); + } catch (_) {} } // On mobile, derive tags from the scanned notes (avoids a separate full-scan Rust call) diff --git a/src/lib/components/SettingsPanel.svelte b/src/lib/components/SettingsPanel.svelte index 73dfb2b..b15d053 100644 --- a/src/lib/components/SettingsPanel.svelte +++ b/src/lib/components/SettingsPanel.svelte @@ -7,7 +7,8 @@ import { getVersion } from '@tauri-apps/api/app'; import { getCurrentWebview } from '@tauri-apps/api/webview'; import { openUrl } from '$lib/api'; - import type { ImportResult, BackupEntry, CustomTheme, CustomThemeColors } from '$lib/types'; + import type { ImportResult, BackupEntry, CustomTheme, CustomThemeColors, StartupView } from '$lib/types'; + import { normalizeStartupView } from '$lib/utils/startup-view'; const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl'; @@ -700,6 +701,7 @@ // General settings let compactNotes = $state($appConfig?.compact_notes ?? false); let showNoteDates = $state($appConfig?.show_note_dates ?? true); + let startupView = $state(normalizeStartupView($appConfig?.startup_view)); let restoreLastSession = $state($appConfig?.restore_last_session ?? false); let timeFormat = $state($appConfig?.time_format ?? 'relative'); let weekStart = $state($appConfig?.week_start ?? 'monday'); @@ -773,6 +775,7 @@ if ($appConfig) { $appConfig.compact_notes = compactNotes; $appConfig.show_note_dates = showNoteDates; + $appConfig.startup_view = startupView; $appConfig.restore_last_session = restoreLastSession; $appConfig.time_format = timeFormat; $appConfig.week_start = weekStart; @@ -795,7 +798,7 @@ $appConfig.show_daily_notes = showDailyNotes; $appConfig.show_trash = showTrash; } - setGeneralSettings(compactNotes, timeFormat, weekStart, dailyTitleFormat, gpuAcceleration, autostart, pdfPreview, pdfHeight, titleMode, hideTitleInBody, showLineNumbers, showLinkArrows, defaultViewMode, showTrayIcon, closeToTray, enableWikiLinks, showNoteDates, restoreLastSession, showAllNotes, showQuickAccess, showTasks, showDailyNotes, showTrash) + setGeneralSettings(compactNotes, timeFormat, weekStart, dailyTitleFormat, gpuAcceleration, autostart, pdfPreview, pdfHeight, titleMode, hideTitleInBody, showLineNumbers, showLinkArrows, defaultViewMode, showTrayIcon, closeToTray, enableWikiLinks, showNoteDates, startupView, restoreLastSession, showAllNotes, showQuickAccess, showTasks, showDailyNotes, showTrash) .catch((e) => console.error('Failed to save general settings:', e)); } @@ -975,6 +978,7 @@ if ($appConfig) { compactNotes = $appConfig.compact_notes ?? false; showNoteDates = $appConfig.show_note_dates ?? true; + startupView = normalizeStartupView($appConfig.startup_view); restoreLastSession = $appConfig.restore_last_session ?? false; timeFormat = $appConfig.time_format ?? 'relative'; weekStart = $appConfig.week_start ?? 'monday'; @@ -1151,6 +1155,29 @@ {/if} +
+

Startup

+
+ Default view + Used when session restore is off or the previous view is unavailable. +
+
+ + + + +
+ +
+

Time Format

@@ -1282,15 +1309,6 @@ -
diff --git a/src/lib/types.ts b/src/lib/types.ts index f9d9b55..23805ef 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -81,6 +81,8 @@ export interface CustomTheme { colors: CustomThemeColors; } +export type StartupView = "all" | "quickaccess" | "tasks" | "daily"; + export interface AppConfig { vaults: VaultConfig[]; active_vault: string | null; @@ -130,6 +132,7 @@ export interface AppConfig { show_tray_icon: boolean; close_to_tray: boolean; enable_wiki_links: boolean; + startup_view: StartupView; restore_last_session: boolean; sync_provider: string | null; webdav_url: string | null; diff --git a/src/lib/utils/startup-view.ts b/src/lib/utils/startup-view.ts new file mode 100644 index 0000000..d14ad75 --- /dev/null +++ b/src/lib/utils/startup-view.ts @@ -0,0 +1,57 @@ +import type { StartupView } from "../types"; + +type RestorableListView = StartupView | "trash"; + +export type StartupTarget = + | { mode: RestorableListView } + | { mode: "notebook"; notebookPath: string } + | { mode: "tag"; tag: string }; + +export interface StartupState { + startupView: unknown; + restoreLastSession: boolean; + lastViewMode: unknown; + lastNotebook: string | null; + lastTag: string | null; +} + +export function normalizeStartupView(value: unknown): StartupView { + switch (value) { + case "daily": + case "quickaccess": + case "tasks": + return value; + default: + return "all"; + } +} + +function restorableListView(value: unknown): RestorableListView | null { + switch (value) { + case "all": + case "daily": + case "quickaccess": + case "tasks": + case "trash": + return value; + default: + return null; + } +} + +export function resolveStartupTarget(state: StartupState): StartupTarget { + const fallback: StartupTarget = { + mode: normalizeStartupView(state.startupView), + }; + if (!state.restoreLastSession) return fallback; + + if (state.lastViewMode === "notebook" && typeof state.lastNotebook === "string") { + return { mode: "notebook", notebookPath: state.lastNotebook }; + } + if (state.lastViewMode === "tag" && state.lastTag) { + return { mode: "tag", tag: state.lastTag }; + } + + const listView = restorableListView(state.lastViewMode); + return listView ? { mode: listView } : fallback; +} diff --git a/tests/startup-view.test.mjs b/tests/startup-view.test.mjs new file mode 100644 index 0000000..0e69fac --- /dev/null +++ b/tests/startup-view.test.mjs @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import { transformWithEsbuild } from 'vite'; + +const source = await readFile( + new URL('../src/lib/utils/startup-view.ts', import.meta.url), + 'utf8' +); +const { code } = await transformWithEsbuild(source, 'startup-view.ts', { + loader: 'ts', + format: 'esm', + target: 'esnext' +}); +const startup = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`); + +test('normalizes every supported default view and falls back to All Notes', () => { + for (const view of ['all', 'quickaccess', 'tasks', 'daily']) { + assert.equal(startup.normalizeStartupView(view), view); + } + assert.equal(startup.normalizeStartupView('notebook'), 'all'); + assert.equal(startup.normalizeStartupView(undefined), 'all'); +}); + +test('uses the configured default when session restoration is disabled', () => { + assert.deepEqual(startup.resolveStartupTarget({ + startupView: 'tasks', + restoreLastSession: false, + lastViewMode: 'daily', + lastNotebook: null, + lastTag: null + }), { mode: 'tasks' }); +}); + +test('restores a supported previous list when restoration is enabled', () => { + assert.deepEqual(startup.resolveStartupTarget({ + startupView: 'daily', + restoreLastSession: true, + lastViewMode: 'quickaccess', + lastNotebook: null, + lastTag: null + }), { mode: 'quickaccess' }); +}); + +test('restores notebook and tag identifiers, otherwise uses the configured default', () => { + assert.deepEqual(startup.resolveStartupTarget({ + startupView: 'all', + restoreLastSession: true, + lastViewMode: 'notebook', + lastNotebook: 'Projects', + lastTag: null + }), { mode: 'notebook', notebookPath: 'Projects' }); + assert.deepEqual(startup.resolveStartupTarget({ + startupView: 'tasks', + restoreLastSession: true, + lastViewMode: 'tag', + lastNotebook: null, + lastTag: null + }), { mode: 'tasks' }); +});