diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 1ac2b29..2322010 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -63,6 +63,7 @@ pub fn run() { .setup(|app| { use tauri::Manager; log_environment(app); + paint_window_before_the_webview_does(app); // The on-device store lives in the platform app-data dir (e.g. Linux // ~/.local/share/com.fabledsword.thoughtsync/thoughtsync.db), created on // first launch. This is what makes the app work with no server or login. @@ -143,6 +144,43 @@ pub fn run() { .expect("error while running the ThoughtSync desktop app"); } +/// Match the window's own background to the theme the UI is about to render in. +/// +/// There is a gap between the window appearing and the webview painting its first +/// frame, and in it the platform's default background shows through — white. On a +/// dark-mode desktop that is the harshest thing the app does, and forcing WebKit's +/// software rendering (see `harden_linux_webkit_rendering`) makes the gap wider, +/// not narrower. +/// +/// Done here rather than as `app.windows[].backgroundColor` in tauri.conf.json +/// because that config takes ONE static colour, and picking either one would fix +/// half of users while introducing the same flash for the other half. Reading the +/// live theme is the only version that is never a regression. +/// +/// Best-effort throughout: a window that won't tell us its theme, or won't take a +/// colour, is a cosmetic loss and must never stop the app from opening. +fn paint_window_before_the_webview_does(app: &tauri::App) { + use tauri::Manager; + let Some(window) = app.get_webview_window("main") else { + return; + }; + // Unknown theme reads as light, matching the platform default we'd get anyway. + let dark = matches!(window.theme(), Ok(tauri::Theme::Dark)); + // The two values style.css actually paints: neutral-950 and neutral-50. + let color = if dark { + tauri::window::Color(10, 10, 10, 255) + } else { + tauri::window::Color(250, 250, 250, 255) + }; + match window.set_background_color(Some(color)) { + Ok(()) => log::info!( + "window background set for the {} theme", + if dark { "dark" } else { "light" } + ), + Err(e) => log::warn!("could not set the window background: {e}"), + } +} + /// Expire old trash at startup, on an unlinked device only (see `local::retention`). /// /// At startup rather than on a timer: a desktop app isn't a server, and a sweep the diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue index f183573..3a304af 100644 --- a/frontend/src/components/AppShell.vue +++ b/frontend/src/components/AppShell.vue @@ -260,9 +260,15 @@ async function signOut() { > - + + - + + - diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 29fed2c..71555ce 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -42,10 +42,16 @@ const router = createRouter({ }, { // Per-user account: linked devices (native-client sync tokens). Any user. + // + // The mirror of `requiresDesktop` above: this one needs a SERVER. The desktop + // is itself one of the devices this page lists, so offline the list is always + // empty and issuing a token rejects — its server relationship lives at /sync. + // Guarded in the router, not just hidden in the shell, so a typed URL or a + // restored history entry can't land on a dead end either. path: "/account", name: "account", component: () => import("../views/AccountView.vue"), - meta: { requiresAuth: true }, + meta: { requiresAuth: true, requiresServer: true }, }, { path: "/login", @@ -85,6 +91,13 @@ router.beforeEach(async (to) => { if (to.meta.requiresDesktop && !isDesktop()) { return { name: "board" }; } + // Deliberately NOT applied to /login and /register: bouncing those on desktop + // would loop against the requiresAuth guard above the moment a session is + // missing. Nothing on the desktop navigates to them any more (AppShell's sign-out + // is web-only), and a fresh launch always resolves the local user. + if (to.meta.requiresServer && isDesktop()) { + return { name: "board" }; + } if (to.name === "register" && !config.allowRegistration) { return { name: "login" }; }