From e96d460b8ecd8cbeab8a78f415976ea6de31bb0e Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 16 Sep 2026 16:30:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(18-03):=20Task=202=20=E2=80=94=20Einstellu?= =?UTF-8?q?ngsseite=20Desktop-App=20mit=20Knoepfen,=20Groesse=20und=20Erkl?= =?UTF-8?q?aerung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue Route /settings/general/desktop (DesktopSettingsPage) - DesktopAppSettings: Version, zwei Primaerknoepfe mit Plattform-Symbol, Dateiname/Groesse je Knopf, Beta-Kanal-Hinweis, vier erklaerende Saetze, Hinweistext wenn keine Pakete hinterlegt sind - Seitenleiste: Eintrag "Desktop-App" unter "Allgemein" - de/en: settings.categoryDesktopApp, settings.desktop.* - umlaut-dictionary.ts: "neuere" zur Allowlist ergaenzt (Rule 1 — Waechter-Test brach an einer bereits korrekten Umlautschreibung, die zufaellig die Buchstabenfolge "ue" enthaelt) Co-Authored-By: Claude Opus 5 (1M context) --- .../settings/general/desktop/page.tsx | 21 +++ .../settings/desktop-app-settings.test.tsx | 105 +++++++++++++++ .../settings/desktop-app-settings.tsx | 122 ++++++++++++++++++ .../components/settings/settings-sidebar.tsx | 13 ++ apps/web/src/messages/de.json | 14 ++ apps/web/src/messages/en.json | 14 ++ apps/web/src/messages/umlaut-dictionary.ts | 1 + 7 files changed, 290 insertions(+) create mode 100644 apps/web/src/app/(portal)/settings/general/desktop/page.tsx create mode 100644 apps/web/src/components/settings/desktop-app-settings.test.tsx create mode 100644 apps/web/src/components/settings/desktop-app-settings.tsx diff --git a/apps/web/src/app/(portal)/settings/general/desktop/page.tsx b/apps/web/src/app/(portal)/settings/general/desktop/page.tsx new file mode 100644 index 0000000..ffba9fa --- /dev/null +++ b/apps/web/src/app/(portal)/settings/general/desktop/page.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { DesktopAppSettings } from '@/components/settings/desktop-app-settings'; + +/** + * Desktop-App settings page — /settings/general/desktop (D-12). + * Shows version, download buttons, file size, and explanatory text. + */ +export default function DesktopSettingsPage() { + const t = useTranslations('settings'); + + return ( +
+

+ {t('desktop.title')} +

+ +
+ ); +} diff --git a/apps/web/src/components/settings/desktop-app-settings.test.tsx b/apps/web/src/components/settings/desktop-app-settings.test.tsx new file mode 100644 index 0000000..0cdd742 --- /dev/null +++ b/apps/web/src/components/settings/desktop-app-settings.test.tsx @@ -0,0 +1,105 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { DesktopAppSettings } from './desktop-app-settings'; + +/** + * desktop-app-settings.test — Einstellungsseite "Desktop-App" (18-03). + * next-intl-Mock nach dem Muster in widget-settings-panel.test.tsx + * (de.json-gestuetzt), `@/lib/desktop` gemockt. + */ +vi.mock('next-intl', async () => { + const messages = (await import('@/messages/de.json')).default as Record; + const lookup = (path: string): string | undefined => + path.split('.').reduce((o, k) => (o && typeof o === 'object' ? (o as any)[k] : undefined), messages) as + | string + | undefined; + return { + useTranslations: + (ns?: string) => + (key: string, values?: Record) => { + const raw = lookup(ns ? `${ns}.${key}` : key) ?? key; + return values ? raw.replace(/\{(\w+)\}/g, (_: string, n: string) => String(values[n] ?? '')) : raw; + }, + useLocale: () => 'de', + }; +}); + +const { loadDesktopLatest } = vi.hoisted(() => ({ loadDesktopLatest: vi.fn() })); + +vi.mock('@/lib/desktop', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadDesktopLatest, + }; +}); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +const windowsFile = { + name: 'Tessera-Setup-1.1.0.exe', + size: 6123456, + sha256: 'x', + url: '/desktop/download/windows', +}; +const linuxFile = { + name: 'Tessera-1.1.0.AppImage', + size: 106461688, + sha256: 'y', + url: '/desktop/download/linux', +}; + +describe('DesktopAppSettings (18-03)', () => { + it('Test 1 (beide Plattformen): Version, zwei Links, Dateiname und Groesse', async () => { + loadDesktopLatest.mockResolvedValue({ + version: '1.1.0', + channel: 'live', + commit: 'abc1234', + buildTime: 'x', + files: { windows: windowsFile, linux: linuxFile }, + }); + render(); + + expect(await screen.findByText('Aktuelle Version: 1.1.0')).toBeInTheDocument(); + + const windowsLink = screen.getByTestId('desktop-download-windows'); + expect(windowsLink).toHaveAttribute('href', 'http://localhost:3001/desktop/download/windows'); + const linuxLink = screen.getByTestId('desktop-download-linux'); + expect(linuxLink).toHaveAttribute('href', 'http://localhost:3001/desktop/download/linux'); + + expect(screen.getByText('Tessera-Setup-1.1.0.exe · 5,8 MB')).toBeInTheDocument(); + expect(screen.getByText('Tessera-1.1.0.AppImage · 101,5 MB')).toBeInTheDocument(); + }); + + it('Test 2 (Beta-Kanal): Hinweis mit Commit', async () => { + loadDesktopLatest.mockResolvedValue({ + version: '1.1.0', + channel: 'beta', + commit: 'abc1234', + buildTime: 'x', + files: { windows: windowsFile, linux: linuxFile }, + }); + render(); + + expect(await screen.findByText('Beta-Ausgabe, Stand abc1234')).toBeInTheDocument(); + }); + + it('Test 3 (keine Pakete): Hinweistext statt Knoepfe, Saetze bleiben', async () => { + loadDesktopLatest.mockResolvedValue(null); + render(); + + expect( + await screen.findByText('Auf diesem Server sind derzeit keine Desktop-Pakete hinterlegt.'), + ).toBeInTheDocument(); + expect(screen.queryByTestId('desktop-download-windows')).toBeNull(); + expect(screen.queryByTestId('desktop-download-linux')).toBeNull(); + expect( + screen.getByText( + 'Die Desktop-App öffnet Tessera in einem eigenen Fenster – ohne Browser, mit Symbol im Infobereich der Taskleiste.', + ), + ).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/settings/desktop-app-settings.tsx b/apps/web/src/components/settings/desktop-app-settings.tsx new file mode 100644 index 0000000..7726d97 --- /dev/null +++ b/apps/web/src/components/settings/desktop-app-settings.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useLocale, useTranslations } from 'next-intl'; +import { desktopDownloadUrl, type DesktopFileInfo, type DesktopLatestInfo, formatFileSize, loadDesktopLatest } from '@/lib/desktop'; + +const BUTTON_CLASS = + 'inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:opacity-90'; + +function WindowsIcon() { + return ( + + ); +} + +function LinuxIcon() { + return ( + + ); +} + +function DownloadButton({ + file, + label, + icon, + testId, + locale, + fileInfoText, +}: { + file: DesktopFileInfo; + label: string; + icon: React.ReactNode; + testId: string; + locale: string; + fileInfoText: (values: { name: string; size: string }) => string; +}) { + return ( +
+ + {icon} + {label} + +

+ {fileInfoText({ name: file.name, size: formatFileSize(file.size, locale) })} +

+
+ ); +} + +/** + * Inhalt der Einstellungsseite "Desktop-App" (D-12): vier erklaerende + * Saetze, dann Version/Downloads oder ein Hinweis, wenn der Server keine + * Pakete traegt (`loadDesktopLatest()` liefert `null`). + */ +export function DesktopAppSettings() { + const t = useTranslations('settings'); + const locale = useLocale(); + const [info, setInfo] = useState(undefined); + + useEffect(() => { + let active = true; + loadDesktopLatest().then((data) => { + if (active) setInfo(data); + }); + return () => { + active = false; + }; + }, []); + + const fileInfoText = (values: { name: string; size: string }) => t('desktop.fileInfo', values); + + return ( +
+

{t('desktop.intro')}

+

{t('desktop.firstStart')}

+

{t('desktop.tray')}

+

{t('desktop.update')}

+ + {info === null &&

{t('desktop.unavailable')}

} + + {info && ( + <> +

{t('desktop.versionLabel', { version: info.version })}

+ {info.channel === 'beta' && ( +

{t('desktop.channelBeta', { commit: info.commit })}

+ )} +
+ {info.files.windows && ( + } + testId="desktop-download-windows" + locale={locale} + fileInfoText={fileInfoText} + /> + )} + {info.files.linux && ( + } + testId="desktop-download-linux" + locale={locale} + fileInfoText={fileInfoText} + /> + )} +
+ + )} +
+ ); +} diff --git a/apps/web/src/components/settings/settings-sidebar.tsx b/apps/web/src/components/settings/settings-sidebar.tsx index 4dd7223..4d16890 100644 --- a/apps/web/src/components/settings/settings-sidebar.tsx +++ b/apps/web/src/components/settings/settings-sidebar.tsx @@ -58,6 +58,19 @@ export function SettingsSidebar() { > {t('categoryAccount')} + + {t('categoryDesktopApp')} + {/* Dashboard category — existing items unchanged */} diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 6b1276d..18b6f9d 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -132,6 +132,7 @@ "categoryCalendar": "Kalender", "categoryGeneral": "Allgemein", "categoryAccount": "Konto", + "categoryDesktopApp": "Desktop-App", "categorySmtp": "SMTP", "account": { "title": "Konto", @@ -152,6 +153,19 @@ "deleteAvatarSuccess": "Profilbild erfolgreich gelöscht.", "deleteAvatarError": "Löschen fehlgeschlagen." }, + "desktop": { + "title": "Desktop-App", + "intro": "Die Desktop-App öffnet Tessera in einem eigenen Fenster – ohne Browser, mit Symbol im Infobereich der Taskleiste.", + "firstStart": "Beim ersten Start fragt die App nach der Adresse Ihres Tessera-Servers; das ist die Adresse, unter der Sie Tessera auch im Browser öffnen.", + "tray": "Schließen Sie das Fenster, läuft Tessera im Infobereich weiter; über das Symbol dort öffnen Sie das Fenster wieder, schalten den automatischen Start ein oder beenden die App.", + "update": "Erscheint eine neuere Version, weist die App Sie darauf hin und führt Sie auf diese Seite.", + "versionLabel": "Aktuelle Version: {version}", + "channelBeta": "Beta-Ausgabe, Stand {commit}", + "downloadWindows": "Für Windows herunterladen", + "downloadLinux": "Für Linux herunterladen", + "fileInfo": "{name} · {size}", + "unavailable": "Auf diesem Server sind derzeit keine Desktop-Pakete hinterlegt." + }, "sourceDeleteConfirm": "Möchten Sie diese Kalenderquelle wirklich löschen?", "sourceDeleteCta": "Quelle löschen", "providerDeleteConfirm": "Möchten Sie diesen Suchanbieter wirklich löschen?", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index f628732..0031532 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -132,6 +132,7 @@ "categoryCalendar": "Calendar", "categoryGeneral": "General", "categoryAccount": "Account", + "categoryDesktopApp": "Desktop App", "categorySmtp": "SMTP", "account": { "title": "Account", @@ -152,6 +153,19 @@ "deleteAvatarSuccess": "Profile picture deleted successfully.", "deleteAvatarError": "Failed to delete picture." }, + "desktop": { + "title": "Desktop App", + "intro": "The desktop app opens Tessera in its own window – no browser, with an icon in the notification area of the taskbar.", + "firstStart": "On first start the app asks for the address of your Tessera server; it is the address you also use to open Tessera in the browser.", + "tray": "If you close the window, Tessera keeps running in the notification area; use the icon there to reopen the window, enable automatic start, or quit the app.", + "update": "When a newer version is available the app notifies you and brings you to this page.", + "versionLabel": "Current version: {version}", + "channelBeta": "Beta build, commit {commit}", + "downloadWindows": "Download for Windows", + "downloadLinux": "Download for Linux", + "fileInfo": "{name} · {size}", + "unavailable": "No desktop packages are available on this server yet." + }, "sourceDeleteConfirm": "Are you sure you want to delete this calendar source?", "sourceDeleteCta": "Delete source", "providerDeleteConfirm": "Are you sure you want to delete this search provider?", diff --git a/apps/web/src/messages/umlaut-dictionary.ts b/apps/web/src/messages/umlaut-dictionary.ts index f17d685..05952ae 100644 --- a/apps/web/src/messages/umlaut-dictionary.ts +++ b/apps/web/src/messages/umlaut-dictionary.ts @@ -114,6 +114,7 @@ export const UMLAUT_ALLOWLIST: readonly string[] = [ 'Energiequellen', 'neue', 'neuen', + 'neuere', 'Neue', 'Neues', 'aktuelle',