Files
tessera-ctl/apps/web/src/components/settings/desktop-app-settings.tsx
T
schalli 4c79874278 feat(web): Einstellungen → Desktop-App zeigt in der App den verbundenen Server
- DesktopAppSettings rendert im Desktop-Client (useIsDesktopClient) einen Block "Verbunden mit: {origin}" plus Hinweis auf "Server-Adresse ändern…" im Infobereich-Menü; im Browser bleibt der Block weg
- Neue i18n-Schlüssel settings.desktop.connectedTo/changeHint in de.json und en.json
- Test 4 (Cookie tessera_desktop=1) und Test 5 (ohne Cookie) ergänzt; RED zuerst (Test 4 schlug auf der Zielbehauptung fehl), dann GREEN
- Alle 431 Web-Tests und type-check grün

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:52:47 +02:00

140 lines
4.7 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useLocale, useTranslations } from 'next-intl';
import { desktopDownloadUrl, type DesktopFileInfo, type DesktopLatestInfo, formatFileSize, loadDesktopLatest } from '@/lib/desktop';
import { useIsDesktopClient } from '@/lib/desktop-client';
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 (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<rect x="1" y="1" width="6" height="6" rx="1" />
<rect x="9" y="1" width="6" height="6" rx="1" />
<rect x="1" y="9" width="6" height="6" rx="1" />
<rect x="9" y="9" width="6" height="6" rx="1" />
</svg>
);
}
function LinuxIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
<rect x="1" y="2" width="14" height="12" rx="1.5" />
<path d="M4 6l2.5 2L4 10" strokeLinecap="round" strokeLinejoin="round" />
<line x1="8" y1="10" x2="11" y2="10" strokeLinecap="round" />
</svg>
);
}
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 (
<div>
<a href={desktopDownloadUrl(file)} download data-testid={testId} className={BUTTON_CLASS}>
{icon}
{label}
</a>
<p className="mt-1 text-xs text-muted-foreground">
{fileInfoText({ name: file.name, size: formatFileSize(file.size, locale) })}
</p>
</div>
);
}
/**
* 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`). Im Desktop-Client
* (260917-jn2) zusaetzlich ein Block "Verbunden mit: {origin}" -- `isDesktop`
* ist im Server-HTML und im ersten Client-Render false (Hook mit
* `useEffect`), der Block rendert also erst nach der Hydration, weshalb
* `window.location.origin` hier direkt gelesen werden darf (kein
* Hydration-Fehler).
*/
export function DesktopAppSettings() {
const t = useTranslations('settings');
const locale = useLocale();
const isDesktop = useIsDesktopClient();
const [info, setInfo] = useState<DesktopLatestInfo | null | undefined>(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 (
<div>
<p className="text-sm text-muted-foreground">{t('desktop.intro')}</p>
<p className="text-sm text-muted-foreground">{t('desktop.firstStart')}</p>
<p className="text-sm text-muted-foreground">{t('desktop.tray')}</p>
<p className="text-sm text-muted-foreground">{t('desktop.update')}</p>
{isDesktop && (
<div
data-testid="desktop-connected"
className="mt-4 space-y-1 rounded border border-border bg-muted/30 p-3 text-sm"
>
<p className="text-foreground">{t('desktop.connectedTo', { origin: window.location.origin })}</p>
<p className="text-muted-foreground">{t('desktop.changeHint')}</p>
</div>
)}
{info === null && <p className="mt-4 text-sm text-muted-foreground">{t('desktop.unavailable')}</p>}
{info && (
<>
<p className="mt-4 text-sm text-foreground">{t('desktop.versionLabel', { version: info.version })}</p>
{info.channel === 'beta' && (
<p className="text-sm text-muted-foreground">{t('desktop.channelBeta', { commit: info.commit })}</p>
)}
<div className="mt-4 flex flex-wrap gap-4">
{info.files.windows && (
<DownloadButton
file={info.files.windows}
label={t('desktop.downloadWindows')}
icon={<WindowsIcon />}
testId="desktop-download-windows"
locale={locale}
fileInfoText={fileInfoText}
/>
)}
{info.files.linux && (
<DownloadButton
file={info.files.linux}
label={t('desktop.downloadLinux')}
icon={<LinuxIcon />}
testId="desktop-download-linux"
locale={locale}
fileInfoText={fileInfoText}
/>
)}
</div>
</>
)}
</div>
);
}