Add configurable startup view

This commit is contained in:
Yuri Karamian
2026-08-03 00:12:55 +02:00
parent 344728dbe8
commit 54e051ba02
8 changed files with 250 additions and 38 deletions
+2
View File
@@ -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;
+39
View File
@@ -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<VaultConfig>,
@@ -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<String>,
pub priority: Option<String>,
}
#[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::<StartupView>("\"future-view\"").unwrap(),
StartupView::All
);
}
}
+3
View File
@@ -16,6 +16,7 @@ import type {
VersionEntry,
TaskItem,
ExternalVaultResult,
StartupView,
} from "./types";
export async function openVault(path: string): Promise<void> {
@@ -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,
+49 -19
View File
@@ -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<boolean> {
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<any> | null = null;
if (isMobile && lastNotePath) {
if (isMobile && restoreLastSession && lastNotePath) {
prefetchPromise = readNote(lastNotePath).catch(() => null);
}
@@ -637,24 +673,19 @@
}, 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();
const startupTarget = resolveStartupTarget({
startupView: $appConfig?.startup_view,
restoreLastSession,
lastViewMode,
lastNotebook,
lastTag
});
if (!(await applyStartupTarget(startupTarget))) {
await applyStartupTarget({ mode: normalizeStartupView($appConfig?.startup_view) });
}
// Open the last note on desktop (mobile reopens it via its own prefetch path below).
if (!isMobile && lastNotePath) {
// Reopen the last note only when session restoration is enabled.
if (restoreLastSession && !isMobile && lastNotePath) {
try {
const content = await readNote(lastNotePath);
$activeNote = content;
@@ -663,7 +694,6 @@
editor?.loadNote(lastNotePath, content.content);
} catch (_) {}
}
}
// On mobile, derive tags from the scanned notes (avoids a separate full-scan Rust call)
if (isMobile) {
+29 -11
View File
@@ -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<StartupView>(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 @@
</div>
{/if}
<div class="settings-section">
<h3>Startup</h3>
<div class="setting-label">
<span class="setting-name">Default view</span>
<span class="setting-desc">Used when session restore is off or the previous view is unavailable.</span>
</div>
<div class="setting-options" style="margin-top: 8px;">
<button class="option-btn" class:active={startupView === 'all'} onclick={() => { startupView = 'all'; saveGeneralSettings(); }}>All Notes</button>
<button class="option-btn" class:active={startupView === 'quickaccess'} onclick={() => { startupView = 'quickaccess'; saveGeneralSettings(); }}>Quick Access</button>
<button class="option-btn" class:active={startupView === 'tasks'} onclick={() => { startupView = 'tasks'; saveGeneralSettings(); }}>Tasks</button>
<button class="option-btn" class:active={startupView === 'daily'} onclick={() => { startupView = 'daily'; saveGeneralSettings(); }}>Daily Notes</button>
</div>
<label class="setting-toggle" style="margin-top: 12px;">
<span class="setting-label">
<span class="setting-name">Restore last session on launch</span>
<span class="setting-desc">Reopen the last view and note when possible. This overrides the default view.</span>
</span>
<button class="toggle-switch" class:on={restoreLastSession} onclick={() => { restoreLastSession = !restoreLastSession; saveGeneralSettings(); }}>
<span class="toggle-knob"></span>
</button>
</label>
</div>
<div class="settings-section">
<h3>Time Format</h3>
<div class="setting-options">
@@ -1282,15 +1309,6 @@
<span class="toggle-knob"></span>
</button>
</label>
<label class="setting-toggle">
<span class="setting-label">
<span class="setting-name">Restore last session on launch</span>
<span class="setting-desc">Reopen the note and folder you were last using, instead of All Notes.</span>
</span>
<button class="toggle-switch" class:on={restoreLastSession} onclick={() => { restoreLastSession = !restoreLastSession; saveGeneralSettings(); }}>
<span class="toggle-knob"></span>
</button>
</label>
</div>
<div class="settings-section">
+3
View File
@@ -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;
+57
View File
@@ -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;
}
+60
View File
@@ -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' });
});