mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
70c2fdfbf9
* feat(linux): session GDK defaults, nvidia-quirk, optional x11-legacy wrap Ship PSYSONIC_ALLOW_NATIVE_GDK from Nix/AUR instead of pinning WEBKIT_DISABLE_* and GDK x11. Add flake psysonic-x11-legacy for the old wrap; alias gdk-session to psysonic. Startup uses webkit2gtk-nvidia-quirk and Wayland-aware compositing; refresh Help (a45) and nixos-install docs. * fix(linux): session GDK and nvidia-quirk only; drop wrapper env heuristics Remove PSYSONIC_ALLOW_NATIVE_GDK and devShell GDK/WEBKIT exports; stop synthesizing GDK/WebKit vars in main.rs. Update Nix/AUR wrappers, install docs, CHANGELOG, and help FAQ with practical user-facing workarounds. * fix(linux): X11-pinned GDK uses DMABUF quirk path, not Wayland explicit-sync When GDK_BACKEND is forced to x11 on a wayland user session, webkit2gtk-nvidia-quirk would still apply __NV_DISABLE_EXPLICIT_SYNC and gray out the webview. Map that case to WEBKIT_DISABLE_DMABUF_RENDERER like native X11. * fix(ui): stabilize WebKitGTK/Wayland hover paint for nav and media cards Sidebar nav links avoid transition:all and promote icons with translateZ(0). Artist rows and album/artist/song cards use compositing hints; card shadows and borders no longer interpolate so cover zoom can stay smooth without jitter. * fix(ui): isolate artist/album card text and cover paint on WebKitGTK Promote cover blocks with contain/paint and text stacks with translateZ(0); use artist-card-info on the artists grid for the same layout as other cards. * feat(artists): in-page overlay scroll and locked main viewport Move list/grid into an inner OverlayScrollArea, stop sticky toolbar from owning the route scroll, align the rail with the main panel edge, and skip the main-route overlay thumb when the viewport cannot scroll vertically. * feat(browse): extend in-page overlay scroll to more library routes Reuse the locked main viewport pattern from Artists for Albums, Composers, Lossless albums, and New releases; wire VirtualCardGrid and scroll chrome to the matching in-page viewport ids. * fix(linux): improve Wayland GPU compositing text clarity in WebKitGTK Use on-demand hardware acceleration on main and mini webviews when the session is Wayland and compositing stays on; gate subpixel body AA on the same conditions via new Tauri probes. Document PSYSONIC_SKIP_WAYLAND_FONT_TUNING for opt-out and changelog. * fix(rust): satisfy clippy needless_return in Linux webkit helpers * fix(linux): tune Wayland text rendering with HW policy env and CSS Allow PSYSONIC_WEBKIT_WAYLAND_HW_POLICY to select WebKit hardware acceleration policy (never/always vs default on-demand). Extend Wayland font CSS to #root with geometricPrecision and text-size-adjust on html. * feat(linux): Wayland text presets in settings, safe WebKit apply, CPU default Persist profile to app config; apply WebKit policy at startup/mini only to avoid WebKitGTK hangs on live toggles. UI + CSS preview stays live; default preset is sharp (CPU-friendly). * fix(linux): map Wayland sharp preset to OnDemand WebKit policy HardwareAccelerationPolicy::Never at startup broke main-viewport wheel scrolling on WebKitGTK+Wayland; sharp vs balanced remains a CSS AA path. Use PSYSONIC_WEBKIT_WAYLAND_HW_POLICY for a true Never policy. * fix(rust): gate Linux-only Wayland WebKit helpers for Windows builds Re-export startup helpers only under cfg(linux) and drop non-Linux stubs so Windows compiles without unused-import and dead-code warnings. * chore(release): CHANGELOG + credits for Linux session/WebKit work (PR #731) Consolidate scattered incremental changelog notes into two [1.47.0] entries with PR link; remove duplicate Linux blocks from [1.46.0] Fixed. Append settings credit line for cucadmuh.
343 lines
15 KiB
TypeScript
343 lines
15 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
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 '../../i18n';
|
|
import { useAuthStore } from '../../store/authStore';
|
|
import type { ClockFormat, LinuxWaylandTextRenderProfile, LoggingMode } from '../../store/authStoreTypes';
|
|
import { IS_LINUX } from '../../utils/platform';
|
|
import { showToast } from '../../utils/ui/toast';
|
|
import { AboutPsysonicBrandHeader } from '../AboutPsysonicLol';
|
|
import CustomSelect from '../CustomSelect';
|
|
import LicensesPanel from '../LicensesPanel';
|
|
import SettingsSubSection from '../SettingsSubSection';
|
|
import { BackupSection } from './BackupSection';
|
|
import { CONTRIBUTORS, MAINTAINERS } from '../../config/settingsCredits';
|
|
|
|
export function SystemTab() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const auth = useAuthStore();
|
|
const [waylandTextRenderAvailable, setWaylandTextRenderAvailable] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!IS_LINUX) return;
|
|
invoke<boolean>('linux_wayland_text_render_settings_available')
|
|
.then(setWaylandTextRenderAvailable)
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
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 lines = await invoke<number>('export_runtime_logs', { path: selected });
|
|
showToast(t('settings.loggingExportSuccess', { count: lines }), 3500, 'info');
|
|
} catch (e) {
|
|
console.error(e);
|
|
showToast(t('settings.loggingExportError'), 4500, 'error');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<SettingsSubSection
|
|
title={t('settings.language')}
|
|
icon={<Globe size={16} />}
|
|
>
|
|
<div className="settings-card">
|
|
<div className="form-group" style={{ maxWidth: '300px' }}>
|
|
<CustomSelect
|
|
value={i18n.language}
|
|
onChange={v => 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: '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') },
|
|
]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
{/* App-Verhalten (aus altem library/general Behavior-Block) */}
|
|
<SettingsSubSection
|
|
title={t('settings.behavior')}
|
|
icon={<AppWindow size={16} />}
|
|
>
|
|
<div className="settings-card">
|
|
<div className="settings-toggle-row">
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>{t('settings.showTrayIcon')}</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.showTrayIconDesc')}</div>
|
|
</div>
|
|
<label className="toggle-switch" aria-label={t('settings.showTrayIcon')}>
|
|
<input type="checkbox" checked={auth.showTrayIcon} onChange={e => auth.setShowTrayIcon(e.target.checked)} />
|
|
<span className="toggle-track" />
|
|
</label>
|
|
</div>
|
|
<div className="settings-section-divider" />
|
|
<div className="settings-toggle-row">
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>{t('settings.minimizeToTray')}</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.minimizeToTrayDesc')}</div>
|
|
</div>
|
|
<label className="toggle-switch" aria-label={t('settings.minimizeToTray')}>
|
|
<input type="checkbox" checked={auth.minimizeToTray} onChange={e => auth.setMinimizeToTray(e.target.checked)} />
|
|
<span className="toggle-track" />
|
|
</label>
|
|
</div>
|
|
{IS_LINUX && (
|
|
<>
|
|
<div className="settings-section-divider" />
|
|
<div className="settings-toggle-row">
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>{t('settings.linuxWebkitSmoothScroll')}</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.linuxWebkitSmoothScrollDesc')}</div>
|
|
</div>
|
|
<label className="toggle-switch" aria-label={t('settings.linuxWebkitSmoothScroll')}>
|
|
<input
|
|
type="checkbox"
|
|
checked={auth.linuxWebkitKineticScroll}
|
|
onChange={e => auth.setLinuxWebkitKineticScroll(e.target.checked)}
|
|
/>
|
|
<span className="toggle-track" />
|
|
</label>
|
|
</div>
|
|
{waylandTextRenderAvailable && (
|
|
<>
|
|
<div className="settings-section-divider" />
|
|
<div className="form-group" style={{ maxWidth: '420px' }}>
|
|
<div style={{ fontWeight: 500 }}>{t('settings.linuxWaylandTextRender')}</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
|
{t('settings.linuxWaylandTextRenderDesc')}
|
|
</div>
|
|
<CustomSelect
|
|
value={auth.linuxWaylandTextRenderProfile}
|
|
onChange={v => 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') },
|
|
]}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
<div className="settings-section-divider" />
|
|
<div className="settings-toggle-row">
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ fontWeight: 500 }}>{t('settings.clockFormat')}</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.clockFormatDesc')}</div>
|
|
</div>
|
|
<div style={{ minWidth: 160 }}>
|
|
<CustomSelect
|
|
value={auth.clockFormat}
|
|
onChange={(v) => auth.setClockFormat(v as ClockFormat)}
|
|
options={[
|
|
{ value: 'auto', label: t('settings.clockFormatAuto') },
|
|
{ value: '24h', label: t('settings.clockFormatTwentyFour') },
|
|
{ value: '12h', label: t('settings.clockFormatTwelve') },
|
|
]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
<SettingsSubSection
|
|
title={t('settings.backupTitle')}
|
|
icon={<HardDrive size={16} />}
|
|
>
|
|
<BackupSection />
|
|
</SettingsSubSection>
|
|
|
|
<SettingsSubSection
|
|
title={t('settings.loggingTitle')}
|
|
icon={<Sliders size={16} />}
|
|
>
|
|
<div className="settings-card">
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
|
|
{t('settings.loggingModeDesc')}
|
|
</div>
|
|
<CustomSelect
|
|
value={auth.loggingMode}
|
|
onChange={(v) => 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' && (
|
|
<div style={{ marginTop: '0.75rem' }}>
|
|
<button className="btn btn-surface" onClick={exportRuntimeLogs}>
|
|
<Download size={14} />
|
|
{t('settings.loggingExport')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
<SettingsSubSection
|
|
title={t('settings.aboutTitle')}
|
|
icon={<Info size={16} />}
|
|
>
|
|
<div className="settings-card settings-about">
|
|
<AboutPsysonicBrandHeader appVersion={appVersion} aboutVersionLabel={t('settings.aboutVersion')} />
|
|
|
|
<p style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.6, margin: '1rem 0 0.5rem' }}>
|
|
{t('settings.aboutDesc')}
|
|
</p>
|
|
|
|
<div className="divider" style={{ margin: '1rem 0' }} />
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: 13 }}>
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
<span style={{ color: 'var(--text-muted)', minWidth: 56 }}>{t('settings.aboutLicense')}</span>
|
|
<span style={{ color: 'var(--text-secondary)' }}>{t('settings.aboutLicenseText')}</span>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
<span style={{ color: 'var(--text-muted)', minWidth: 56 }}>Stack</span>
|
|
<span style={{ color: 'var(--text-secondary)' }}>{t('settings.aboutBuiltWith')}</span>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
|
<span style={{ color: 'var(--text-muted)', minWidth: 56, flexShrink: 0 }}>{t('settings.aboutMaintainersLabel')}</span>
|
|
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
|
{MAINTAINERS.map(m => (
|
|
<div key={m.github} style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
<img
|
|
src={`https://github.com/${m.github}.png?size=32`}
|
|
width={20} height={20}
|
|
style={{ borderRadius: '50%', flexShrink: 0 }}
|
|
alt={m.github}
|
|
/>
|
|
<button
|
|
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: 'var(--accent)', fontWeight: 600, fontSize: 13 }}
|
|
onClick={() => openUrl(`https://github.com/${m.github}`)}
|
|
>
|
|
@{m.github}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
<span style={{ color: 'var(--text-muted)', minWidth: 56 }}>{t('settings.aboutReleaseNotesLabel')}</span>
|
|
<button
|
|
onClick={() => {
|
|
useAuthStore.getState().setLastSeenChangelogVersion('');
|
|
navigate('/whats-new');
|
|
}}
|
|
style={{ color: 'var(--accent)', background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}
|
|
>
|
|
{t('settings.aboutReleaseNotesLink')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="settings-section-divider" style={{ marginTop: '1.25rem' }} />
|
|
<div className="settings-toggle-row">
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>{t('settings.showChangelogOnUpdate')}</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.showChangelogOnUpdateDesc')}</div>
|
|
</div>
|
|
<label className="toggle-switch" aria-label={t('settings.showChangelogOnUpdate')}>
|
|
<input
|
|
type="checkbox"
|
|
checked={auth.showChangelogOnUpdate}
|
|
onChange={e => auth.setShowChangelogOnUpdate(e.target.checked)}
|
|
/>
|
|
<span className="toggle-track" />
|
|
</label>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1.25rem', flexWrap: 'wrap' }}>
|
|
<button
|
|
className="btn btn-ghost"
|
|
style={{ alignSelf: 'flex-start' }}
|
|
onClick={() => openUrl('https://github.com/Psychotoxical/psysonic')}
|
|
>
|
|
<ExternalLink size={14} />
|
|
{t('settings.aboutRepo')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
<SettingsSubSection
|
|
title={t('settings.aboutContributorsLabel')}
|
|
icon={<Users size={16} />}
|
|
>
|
|
<div className="contributors-grid">
|
|
{CONTRIBUTORS.map(c => (
|
|
<details key={c.github} className="contributor-card">
|
|
<summary className="contributor-card-summary">
|
|
<img
|
|
src={`https://github.com/${c.github}.png?size=48`}
|
|
width={32}
|
|
height={32}
|
|
className="contributor-card-avatar"
|
|
alt={c.github}
|
|
/>
|
|
<div className="contributor-card-meta">
|
|
<span
|
|
className="contributor-card-name"
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={e => { e.stopPropagation(); openUrl(`https://github.com/${c.github}`); }}
|
|
onKeyDown={e => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
openUrl(`https://github.com/${c.github}`);
|
|
}
|
|
}}
|
|
>
|
|
@{c.github}
|
|
</span>
|
|
<span className="contributor-card-sub">
|
|
<span className="contributor-card-since">v{c.since}</span>
|
|
<span>·</span>
|
|
<span>{t('settings.aboutContributorsCount', { count: c.contributions.length })}</span>
|
|
</span>
|
|
</div>
|
|
<ChevronDown size={14} className="contributor-card-chevron" aria-hidden />
|
|
</summary>
|
|
<ul className="contributor-card-list">
|
|
{c.contributions.map(item => <li key={item}>{item}</li>)}
|
|
</ul>
|
|
</details>
|
|
))}
|
|
</div>
|
|
</SettingsSubSection>
|
|
|
|
<SettingsSubSection
|
|
title={t('licenses.title')}
|
|
icon={<Scale size={16} />}
|
|
>
|
|
<LicensesPanel />
|
|
</SettingsSubSection>
|
|
</>
|
|
);
|
|
}
|