From d6646a64fb1cf58f7657547fd475650cde7a5af9 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 16 Aug 2026 12:46:38 -0400 Subject: [PATCH] desktop: remove two dead ends from the shell, and stop the launch flash (task 1999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sign out was a trap on the desktop, not an action. It nulls the synthetic local user and redirects to /login, but the offline adapter rejects every sign-in with "there's no account to sign in to" — so the only way back into your own notes was to restart the app. There is nothing to sign out of; the notes are on this machine either way. Linked devices was a quieter version of the same thing: it lists the tokens a SERVER has issued to native clients, and the desktop is one of those clients, so offline the list is always empty and issuing a token rejects. Its actual relationship with a server already has a home at /sync. Also hid the account name, which named a login the app doesn't have. /account is now blocked in the router too, not merely hidden — the mirror of the existing requiresDesktop guard — so a typed URL or a restored history entry can't reach the dead end either. Deliberately not applied to /login and /register: bouncing those on desktop would loop against the requiresAuth guard whenever a session is missing. The launch flash is the window painting before the webview does, showing the platform default white through the gap — worst on a dark-mode desktop, and widened by the software rendering we force on Linux. Set from the live system theme rather than app.windows[].backgroundColor, because that config carries one static colour and either choice would fix half of users while introducing the same flash for the other half. Co-Authored-By: Claude Opus 5 (1M context) --- desktop/src-tauri/src/lib.rs | 38 ++++++++++++++++++++++++++++ frontend/src/components/AppShell.vue | 38 ++++++++++++++++++++++++---- frontend/src/router/index.ts | 15 ++++++++++- 3 files changed, 85 insertions(+), 6 deletions(-) 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" }; }