import { useEffect, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { commands } from '@/generated/bindings'; import { linuxWaylandTextRenderSettingsAvailable } from '@/lib/api/platformShell'; import { save as saveDialog } from '@tauri-apps/plugin-dialog'; import { open as openUrl } from '@tauri-apps/plugin-shell'; import { AppWindow, ChevronDown, Download, ExternalLink, Globe, HardDrive, Info, Scale, Sliders, Users } from 'lucide-react'; import { version as appVersion } from '@/../package.json'; import i18n from '@/lib/i18n'; import { useAuthStore } from '@/store/authStore'; import type { ClockFormat, LinuxWaylandTextRenderProfile, LoggingMode } from '@/store/authStoreTypes'; import { IS_LINUX } from '@/lib/util/platform'; import { showToast } from '@/lib/dom/toast'; import { AboutPsysonicBrandHeader } from '@/features/settings/components/AboutPsysonicLol'; import CustomSelect from '@/ui/CustomSelect'; import LicensesPanel from '@/features/settings/components/LicensesPanel'; import SettingsSubSection from '@/features/settings/components/SettingsSubSection'; import { SettingsGroup } from '@/features/settings/components/SettingsGroup'; import { SettingsToggle } from '@/features/settings/components/SettingsToggle'; import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard'; import { BackupSection } from '@/features/settings/components/BackupSection'; import { CONTRIBUTORS, MAINTAINERS, themeContributorsFromRegistry, type ThemeContributor } from '@/config/settingsCredits'; import { revalidateRegistry } from '@/lib/themes/themeRegistry'; export function SystemTab() { const { t } = useTranslation(); const navigate = useNavigate(); const auth = useAuthStore(); const [waylandTextRenderAvailable, setWaylandTextRenderAvailable] = useState(false); const [themeContributors, setThemeContributors] = useState([]); useEffect(() => { if (!IS_LINUX) return; linuxWaylandTextRenderSettingsAvailable() .then(setWaylandTextRenderAvailable) .catch(() => {}); }, []); // Community theme authors come from the store registry. Stale-while-revalidate: // the cached copy paints immediately (offline-safe), then a background refresh // corrects it. Reading the plain TTL cache would leave a corrected author // mis-credited for up to 12 hours, and Credits has no refresh control of its // own. On a first run with no cached registry this simply stays empty. useEffect(() => { let cancelled = false; void revalidateRegistry(registry => { if (!cancelled) setThemeContributors(themeContributorsFromRegistry(registry.themes)); }); return () => { cancelled = true; }; }, []); const exportRuntimeLogs = async () => { const suggestedName = `psysonic-logs-${new Date().toISOString().replace(/[:.]/g, '-')}.log`; const selected = await saveDialog({ defaultPath: suggestedName, filters: [{ name: 'Log files', extensions: ['log', 'txt'] }], title: t('settings.loggingExport'), }); if (!selected || Array.isArray(selected)) return; try { const res = await commands.exportRuntimeLogs(selected); if (res.status === 'error') throw new Error(res.error); showToast(t('settings.loggingExportSuccess', { count: res.data }), 3500, 'info'); } catch (e) { console.error(e); showToast(t('settings.loggingExportError'), 4500, 'error'); } }; // Shared card for both credit sub-sections: avatar + @handle link, a sub-line, // and an expandable list (code contributions for App, theme names for Themes). const renderContributorCard = (github: string, sub: ReactNode, items: readonly string[]) => (
{github}
{ e.stopPropagation(); openUrl(`https://github.com/${github}`); }} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); e.preventDefault(); openUrl(`https://github.com/${github}`); } }} > @{github} {sub}
); return ( <> } >
i18n.changeLanguage(v)} options={[ { value: 'en', label: t('settings.languageEn') }, { value: 'de', label: t('settings.languageDe') }, { value: 'es', label: t('settings.languageEs') }, { value: 'fr', label: t('settings.languageFr') }, { value: 'it', label: t('settings.languageIt') }, { value: 'nl', label: t('settings.languageNl') }, { value: 'nb', label: t('settings.languageNb') }, { value: 'ru', label: t('settings.languageRu') }, { value: 'zh', label: t('settings.languageZh') }, { value: 'ro', label: t('settings.languageRo') }, { value: 'ja', label: t('settings.languageJa') }, { value: 'hu', label: t('settings.languageHu') }, { value: 'pl', label: t('settings.languagePl') }, { value: 'bg', label: t('settings.languageBg') }, ]} />
{/* App-Verhalten (aus altem library/general Behavior-Block) */} } >
{IS_LINUX && (
{waylandTextRenderAvailable && ( auth.setLinuxWaylandTextRenderProfile(v as LinuxWaylandTextRenderProfile)} options={[ { value: 'balanced', label: t('settings.linuxWaylandTextRenderBalanced') }, { value: 'sharp', label: t('settings.linuxWaylandTextRenderSharp') }, { value: 'gpu', label: t('settings.linuxWaylandTextRenderGpu') }, { value: 'minimal', label: t('settings.linuxWaylandTextRenderMinimal') }, ]} /> )} )}
auth.setClockFormat(v as ClockFormat)} options={[ { value: 'auto', label: t('settings.clockFormatAuto') }, { value: '24h', label: t('settings.clockFormatTwentyFour') }, { value: '12h', label: t('settings.clockFormatTwelve') }, ]} />
} > } >
auth.setLoggingMode(v as LoggingMode)} options={[ { value: 'off', label: t('settings.loggingModeOff') }, { value: 'normal', label: t('settings.loggingModeNormal') }, { value: 'debug', label: t('settings.loggingModeDebug') }, ]} /> {auth.loggingMode === 'debug' && (
)}
} >

{t('settings.aboutDesc')}

{t('settings.aboutLicense')} {t('settings.aboutLicenseText')}
Stack {t('settings.aboutBuiltWith')}
{t('settings.aboutMaintainersLabel')}
{MAINTAINERS.map(m => (
{m.github}
))}
{t('settings.aboutReleaseNotesLabel')}
} >
{t('settings.aboutContributorsApp')}
{CONTRIBUTORS.map(c => renderContributorCard( c.github, <> v{c.since} ยท {t('settings.aboutContributorsCount', { count: c.contributions.length })} , c.contributions, ), )}
{themeContributors.length > 0 && ( <>
{t('settings.aboutContributorsThemes')}
{themeContributors.map(c => renderContributorCard( c.github, {t('settings.aboutThemeContributorsCount', { count: c.themes.length })}, c.themes, ), )}
)}
} > ); }