mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
d49424e95b
* feat(ui): prototype compact hero and toolbar buttons (Large/Small appearance setting) * feat(settings): rename action-button size toggle to "Compact buttons" Promote the hero-button prototype to a real, app-wide setting. - rename heroButtonSize → buttonSize, data-hero-buttons → data-button-size, hero-action-bar/hero-btn-label → compact-action-bar/compact-btn-label - relabel "Hero buttons" → "Compact buttons" and broaden the description (action + toolbar buttons across detail pages and browse views) in all 11 locales - move the feature CSS out of cover-lightbox.css into its own compact-buttons.css - add tests: themeStore buttonSize toggle, SelectionToggleButton * docs(changelog): compact buttons appearance toggle (#1189) * docs(credits): compact buttons appearance toggle (#1189)
119 lines
5.4 KiB
TypeScript
119 lines
5.4 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useAuthStore } from './store/authStore';
|
|
import { usePlayerStore } from './store/playerStore';
|
|
import { useLyricsStore } from './store/lyricsStore';
|
|
import { useThemeStore } from './store/themeStore';
|
|
import { useInstalledThemesStore } from './store/installedThemesStore';
|
|
import { syncInjectedThemes } from './utils/themes/themeInjection';
|
|
import { useThemeScheduler } from './hooks/useThemeScheduler';
|
|
import { useFontStore } from './store/fontStore';
|
|
import { getWindowKind } from './app/windowKind';
|
|
import MiniPlayerApp from './app/MiniPlayerApp';
|
|
import MainApp from './app/MainApp';
|
|
|
|
export default function App() {
|
|
// Re-subscribe so themeStore changes trigger a re-render (the value itself
|
|
// is consumed via useThemeScheduler / data-theme attribute below).
|
|
useThemeStore(s => s.theme);
|
|
const effectiveTheme = useThemeScheduler();
|
|
const font = useFontStore(s => s.font);
|
|
const buttonSize = useThemeStore(s => s.buttonSize);
|
|
const installedThemes = useInstalledThemesStore(s => s.themes);
|
|
|
|
// Document-attribute hooks are shared between both window kinds — each
|
|
// webview has its own `document`, and theme / font / track-preview tokens
|
|
// are read by CSS in both trees.
|
|
|
|
// Installed community themes have no build-time CSS — inject their
|
|
// `[data-theme='<id>']` blocks into <head> from the persisted (localStorage)
|
|
// store. Runs before the data-theme effect below so the matching style exists
|
|
// when the attribute is applied. The store hydrates synchronously, so an
|
|
// active community theme is painted without a network round-trip.
|
|
useEffect(() => {
|
|
syncInjectedThemes(installedThemes);
|
|
}, [installedThemes]);
|
|
|
|
// Dev only: `--theme-watch <theme.css>` (debug builds) pushes a local theme's
|
|
// CSS in on every save. Install it under the id in its `[data-theme='<id>']`
|
|
// selector and apply it — the syncInjectedThemes effect above re-injects, so
|
|
// authoring is live without re-importing a zip. Never wired in production.
|
|
useEffect(() => {
|
|
if (!import.meta.env.DEV) return;
|
|
let unlisten: (() => void) | undefined;
|
|
void import('@tauri-apps/api/event').then(({ listen }) => {
|
|
const sub = listen<string>('theme-watch:css', ({ payload }) => {
|
|
const id = payload.match(/\[data-theme=['"]([^'"]+)['"]\]/)?.[1];
|
|
if (!id) return;
|
|
useInstalledThemesStore.getState().install({
|
|
id, name: id, author: 'dev', version: '0.0.0', description: '', mode: 'dark', css: payload, installedAt: Date.now(),
|
|
});
|
|
useThemeStore.getState().setTheme(id);
|
|
});
|
|
// Guard the mocked-in-tests case where listen() isn't a promise.
|
|
if (sub && typeof sub.then === 'function') sub.then(u => { unlisten = u; });
|
|
}).catch(() => {});
|
|
return () => unlisten?.();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-theme', effectiveTheme);
|
|
}, [effectiveTheme]);
|
|
|
|
// Expose app state on the theme root so themes can react to it with a
|
|
// same-element compound selector, e.g. `[data-theme='x'][data-playing='true']`.
|
|
// The set of allowed state attributes is the contract's `stateSelectors`.
|
|
// (Sidebar-collapsed is set in AppShell, where that state lives.)
|
|
const isPlaying = usePlayerStore(s => s.isPlaying);
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-playing', isPlaying ? 'true' : 'false');
|
|
}, [isPlaying]);
|
|
|
|
const isFullscreenOpen = usePlayerStore(s => s.isFullscreenOpen);
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-fullscreen', isFullscreenOpen ? 'true' : 'false');
|
|
}, [isFullscreenOpen]);
|
|
|
|
const lyricsOpen = useLyricsStore(s => s.activeTab === 'lyrics');
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-lyrics-open', lyricsOpen ? 'true' : 'false');
|
|
}, [lyricsOpen]);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-font', font);
|
|
}, [font]);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-button-size', buttonSize);
|
|
}, [buttonSize]);
|
|
|
|
// Hide all inline track-preview buttons when the user opts out — single
|
|
// CSS hook (`html[data-track-previews="off"]`) instead of conditional
|
|
// rendering in every tracklist. Per-location toggles use additional
|
|
// attributes `data-track-previews-{location}` consumed by scoped selectors.
|
|
const trackPreviewsEnabled = useAuthStore(s => s.trackPreviewsEnabled);
|
|
const trackPreviewLocations = useAuthStore(s => s.trackPreviewLocations);
|
|
const trackPreviewDurationSec = useAuthStore(s => s.trackPreviewDurationSec);
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute(
|
|
'data-track-previews',
|
|
trackPreviewsEnabled ? 'on' : 'off',
|
|
);
|
|
}, [trackPreviewsEnabled]);
|
|
useEffect(() => {
|
|
const root = document.documentElement;
|
|
(Object.keys(trackPreviewLocations) as Array<keyof typeof trackPreviewLocations>).forEach(loc => {
|
|
root.setAttribute(`data-track-previews-${loc.toLowerCase()}`, trackPreviewLocations[loc] ? 'on' : 'off');
|
|
});
|
|
}, [trackPreviewLocations]);
|
|
// Drive the SVG progress-ring keyframe duration from the same setting that
|
|
// governs the engine's auto-stop timer so both finish in lockstep.
|
|
useEffect(() => {
|
|
document.documentElement.style.setProperty(
|
|
'--preview-duration',
|
|
`${trackPreviewDurationSec}s`,
|
|
);
|
|
}, [trackPreviewDurationSec]);
|
|
|
|
return getWindowKind() === 'mini' ? <MiniPlayerApp /> : <MainApp />;
|
|
}
|