From fffb7ffbecefc179537ea536a2d00f7a597ea7c6 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 2 Oct 2026 23:31:26 +0200 Subject: [PATCH] fix(desktop): Dateidialog folgt dem Dunkelmodus (GTK prefer-dark via set_theme) Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/desktop/src-tauri/src/lib.rs | 13 +++- apps/web/src/app/layout.tsx | 2 + .../desktop/desktop-theme-sync.test.tsx | 65 +++++++++++++++++++ .../components/desktop/desktop-theme-sync.tsx | 30 +++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/desktop/desktop-theme-sync.test.tsx create mode 100644 apps/web/src/components/desktop/desktop-theme-sync.tsx diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 9222293..e98c442 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -385,10 +385,18 @@ fn server_host(url: Option<&str>) -> Option { /// `is_permission_granted.toml`, `request_permission.toml`). Eine unbekannte /// Kennung wuerde in `add_capability` ebenfalls in einen Panic laufen; darum /// pinnt ein Test die exakte Menge samt Reihenfolge. -const SERVER_NOTIFICATION_PERMISSIONS: [&str; 3] = [ +/// +/// Dazu `core:window:allow-set-theme` (quick-261002, Dunkelmodus): die +/// Web-Oberflaeche meldet ihr helles/dunkles Erscheinungsbild an das Fenster. +/// Unter Linux setzt tao damit `gtk-application-prefer-dark-theme`, sodass +/// der GTK-Dateidialog eines `` dem Dunkelmodus folgt; +/// unter Windows folgt die Titelleiste. Erlaubt nur `set_theme`, keine +/// anderen Fensterbefehle. +const SERVER_NOTIFICATION_PERMISSIONS: [&str; 4] = [ "notification:allow-notify", "notification:allow-is-permission-granted", "notification:allow-request-permission", + "core:window:allow-set-theme", ]; /// Bereits berechtigte Herkunfts-Muster. `add_capability` haengt bei jedem @@ -1859,7 +1867,7 @@ mod tests { } #[test] - fn server_origin_berechtigungen_sind_genau_diese_drei() { + fn server_origin_berechtigungen_sind_genau_diese_vier() { // Pinnt die Menge (T-IF2-03): ein zusaetzliches `notification:default` // oder eine Platzhalter-Berechtigung laesst den Test rot werden. assert_eq!( @@ -1868,6 +1876,7 @@ mod tests { "notification:allow-notify", "notification:allow-is-permission-granted", "notification:allow-request-permission", + "core:window:allow-set-theme", ] ); } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ea8814c..da13f81 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -4,6 +4,7 @@ import { getLocale, getMessages } from 'next-intl/server'; import { ThemeProvider } from 'next-themes'; import { DesktopContextMenuGuard } from '@/components/desktop/desktop-context-menu-guard'; import { DesktopExternalLinks } from '@/components/desktop/desktop-external-links'; +import { DesktopThemeSync } from '@/components/desktop/desktop-theme-sync'; import './globals.css'; export const metadata: Metadata = { @@ -27,6 +28,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo + {children} diff --git a/apps/web/src/components/desktop/desktop-theme-sync.test.tsx b/apps/web/src/components/desktop/desktop-theme-sync.test.tsx new file mode 100644 index 0000000..f08c2c1 --- /dev/null +++ b/apps/web/src/components/desktop/desktop-theme-sync.test.tsx @@ -0,0 +1,65 @@ +import { act, cleanup, render } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DESKTOP_COOKIE_NAME } from '@/lib/desktop-client'; +import { DesktopThemeSync } from './desktop-theme-sync'; + +/** + * desktop-theme-sync.test (quick-261002) — meldet hell/dunkel per + * `plugin:window|set_theme` an das Desktop-Fenster, im Browser nichts. + */ +const theme = vi.hoisted(() => ({ resolvedTheme: 'dark' as string | undefined })); +vi.mock('next-themes', () => ({ useTheme: () => theme })); + +const invoke = vi.fn(() => Promise.resolve()); + +function setCookie() { + document.cookie = `${DESKTOP_COOKIE_NAME}=1; path=/`; +} + +beforeEach(() => { + invoke.mockClear(); + (window as unknown as Record).__TAURI_INTERNALS__ = { invoke }; + theme.resolvedTheme = 'dark'; +}); + +afterEach(() => { + cleanup(); + document.cookie = `${DESKTOP_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; + delete (window as unknown as Record).__TAURI_INTERNALS__; +}); + +describe('DesktopThemeSync', () => { + it('meldet im Desktop-Client das dunkle Erscheinungsbild', async () => { + setCookie(); + await act(async () => { + render(); + }); + expect(invoke).toHaveBeenCalledWith('plugin:window|set_theme', { value: 'dark' }); + }); + + it('meldet einen Wechsel auf hell erneut', async () => { + setCookie(); + const view = await act(async () => render()); + theme.resolvedTheme = 'light'; + await act(async () => { + view.rerender(); + }); + expect(invoke).toHaveBeenLastCalledWith('plugin:window|set_theme', { value: 'light' }); + }); + + it('ruft im Browser nichts auf', async () => { + await act(async () => { + render(); + }); + expect(invoke).not.toHaveBeenCalled(); + }); + + it('wartet, solange das Erscheinungsbild noch nicht feststeht', async () => { + setCookie(); + theme.resolvedTheme = undefined; + await act(async () => { + render(); + }); + expect(invoke).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/components/desktop/desktop-theme-sync.tsx b/apps/web/src/components/desktop/desktop-theme-sync.tsx new file mode 100644 index 0000000..c9522ba --- /dev/null +++ b/apps/web/src/components/desktop/desktop-theme-sync.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { useEffect } from 'react'; +import { useIsDesktopClient } from '@/lib/desktop-client'; + +type TauriInvoke = (cmd: string, args?: Record) => Promise; + +/** + * DesktopThemeSync (quick-261002) — meldet dem Desktop-Fenster, ob Tessera + * hell oder dunkel dargestellt wird. Unter Linux schaltet das GTK auf die + * dunkle Variante, damit z. B. der Dateidialog eines Uploads nicht grell weiss + * aufgeht; unter Windows folgt die Titelleiste. Im Browser ohne Wirkung. + * Fehler (aeltere Desktop-App ohne Berechtigung) werden still ignoriert. + */ +export function DesktopThemeSync() { + const isDesktop = useIsDesktopClient(); + const { resolvedTheme } = useTheme(); + + useEffect(() => { + if (!isDesktop || (resolvedTheme !== 'dark' && resolvedTheme !== 'light')) return; + const internals = (window as unknown as Record).__TAURI_INTERNALS__ as + | { invoke?: TauriInvoke } + | undefined; + if (typeof internals?.invoke !== 'function') return; + internals.invoke('plugin:window|set_theme', { value: resolvedTheme }).catch(() => {}); + }, [isDesktop, resolvedTheme]); + + return null; +}