mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
cb1a110afb
Relocate the playback/queue/transport/audio-output engine out of the type-first
store/ + utils/playback/ + utils/audio/ dirs into a cohesive src/features/playback/,
structure-preserving:
store/<x> -> features/playback/store/<x>
store/audioListenerSetup/<x> -> features/playback/store/audioListenerSetup/<x>
utils/playback/<x> -> features/playback/utils/playback/<x>
utils/audio/<x> -> features/playback/utils/audio/<x>
184 files moved (107 source + 77 tests), 365 consumers rewritten. Pure move — no
behavior change, no state-split (the playerStore state-split stays a separate M5
question). Enabled by this session's decouple seams (artist/offline/orbit/auth →
core registries), so the engine carries no inbound core->feature inversion: store/
now holds only the 50 cross-cutting global stores (auth family, the seams, library
index, UI/settings stores).
KEPT OUT of the move (would re-create global->engine edges): the 3 pure config
helpers utils/audio/{loudnessPreAnalysisSlider,hiResCrossfadeResample} +
utils/playback/autodjOverlapCap (authStore + settings UI read them — they stay in
utils/). Ambiguous view-state stores (eqStore, queueToolbarStore,
playerBarLayoutStore) stay global (no engine imports).
Consumers use DEEP paths (@/features/playback/...), no barrel — matches the lib/
approach and avoids barrel-mock-collapse across the 140 usePlayerStore consumers.
Two tolerated type-only core->feature edges remain (localPlaybackStore->QueueItemRef,
localPlaybackMigration->HotCacheEntry, both erased).
tsc 0, lint 0, full suite 319/2353 green, iron-rule clean (no runtime store->feature
import). Behavior-touching only via the prerequisite bridge seam (already QA-flagged);
the move itself is pure.
134 lines
5.5 KiB
TypeScript
134 lines
5.5 KiB
TypeScript
import { installQueueUndoHotkey } from '@/features/playback/store/queueUndoHotkey';
|
|
import { configureStartupSplash } from './startupSplash';
|
|
import { setupMusicNetworkRuntime } from './musicNetworkBridge';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { getWindowKind } from './windowKind';
|
|
import { migrateThemeSelection } from '../utils/themes/themeMigration';
|
|
import { getScheduledTheme, useThemeStore } from '../store/themeStore';
|
|
import { syncInjectedThemes } from '../utils/themes/themeInjection';
|
|
import { useInstalledThemesStore, type InstalledTheme } from '../store/installedThemesStore';
|
|
|
|
/** Sync backend HTTP User-Agent from the main webview once at startup. */
|
|
export function pushUserAgentToBackend(): void {
|
|
try {
|
|
if (getWindowKind() !== 'main') return;
|
|
const ua = window.navigator.userAgent?.trim();
|
|
if (ua) {
|
|
void invoke('set_subsonic_wire_user_agent', { userAgent: ua, windowLabel: 'main' });
|
|
}
|
|
} catch {
|
|
// Ignore in non-Tauri runtimes.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Push the persisted logging mode to Rust before React mounts. Zustand rehydrate
|
|
* runs after first paint; AppShell's useEffect can miss the user's persisted
|
|
* `loggingMode` until then — but waveform/audio may already run. Matches the
|
|
* `psysonic-auth` localStorage key.
|
|
*/
|
|
export function pushLoggingModeToBackend(): void {
|
|
try {
|
|
const raw = localStorage.getItem('psysonic-auth');
|
|
if (!raw) return;
|
|
const parsed = JSON.parse(raw) as { state?: { loggingMode?: string } };
|
|
const mode = parsed.state?.loggingMode;
|
|
if (mode === 'off' || mode === 'normal' || mode === 'debug') {
|
|
void invoke('set_logging_mode', { mode });
|
|
}
|
|
} catch {
|
|
// Ignore parse / non-Tauri.
|
|
}
|
|
}
|
|
|
|
function readInstalledThemes(): InstalledTheme[] {
|
|
try {
|
|
const raw = localStorage.getItem('psysonic_installed_themes');
|
|
if (!raw) return [];
|
|
const parsed = JSON.parse(raw) as { state?: { themes?: InstalledTheme[] } };
|
|
return Array.isArray(parsed.state?.themes) ? (parsed.state!.themes as InstalledTheme[]) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply the active theme synchronously, before React mounts, so the first paint
|
|
* is already correct. Zustand rehydrate + the `data-theme` effect run after the
|
|
* first paint, so without this a non-Mocha active theme (every light theme and
|
|
* every installed community theme) flashes the `:root` Mocha default for a
|
|
* frame. We set `data-theme` to the effective (scheduler-resolved) theme and
|
|
* inject installed community themes' CSS up front. Runs after the migration so
|
|
* the persisted ids are already resolved.
|
|
*/
|
|
export function applyThemeAtStartup(): void {
|
|
try {
|
|
const raw = localStorage.getItem('psysonic_theme');
|
|
if (!raw) return; // fresh profile — the :root default is correct
|
|
const parsed = JSON.parse(raw) as { state?: Record<string, unknown> };
|
|
const s = parsed.state;
|
|
if (!s) return;
|
|
syncInjectedThemes(readInstalledThemes());
|
|
// First-frame best effort for the "follow system" mode: the Web media query
|
|
// is sync here (the native Tauri theme resolves only after mount, when the
|
|
// App effect re-applies the effective theme). Unreliable on Linux WebKitGTK,
|
|
// but only affects this initial paint before the effect corrects it.
|
|
const systemPrefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
const effective = getScheduledTheme(
|
|
{
|
|
enableThemeScheduler: !!s.enableThemeScheduler,
|
|
schedulerMode: s.schedulerMode === 'system' ? 'system' : 'time',
|
|
theme: String(s.theme ?? 'mocha'),
|
|
themeDay: String(s.themeDay ?? 'latte'),
|
|
themeNight: String(s.themeNight ?? 'mocha'),
|
|
timeDayStart: String(s.timeDayStart ?? '07:00'),
|
|
timeNightStart: String(s.timeNightStart ?? '19:00'),
|
|
},
|
|
systemPrefersDark,
|
|
);
|
|
if (effective) document.documentElement.setAttribute('data-theme', effective);
|
|
} catch {
|
|
// Non-fatal — App's effects apply the theme after mount.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Keep theme state in sync across webviews (main ↔ mini player). Zustand
|
|
* persist does not sync across windows on its own; the `storage` event fires in
|
|
* *other* windows when localStorage changes, so rehydrate the relevant store
|
|
* there. Installing/applying/uninstalling in one window then live-updates the
|
|
* other (App's effects re-run and re-apply `data-theme` / injected styles).
|
|
*/
|
|
export function installCrossWindowThemeSync(): void {
|
|
if (typeof window === 'undefined') return;
|
|
window.addEventListener('storage', (e) => {
|
|
if (e.key === 'psysonic_theme') void useThemeStore.persist?.rehydrate?.();
|
|
else if (e.key === 'psysonic_installed_themes') void useInstalledThemesStore.persist?.rehydrate?.();
|
|
});
|
|
}
|
|
|
|
/** Mark the document in Vite dev so CSS can show dev-only chrome. */
|
|
export function markDevBuildDocument(): void {
|
|
if (import.meta.env.DEV) {
|
|
document.documentElement.dataset.devBuild = 'true';
|
|
}
|
|
}
|
|
|
|
/** Orchestrates everything that must run before React mounts. */
|
|
export function runPreReactBootstrap(): void {
|
|
// Pre-warm the window-kind cache so subsequent reads are sync + safe.
|
|
getWindowKind();
|
|
// Reset any persisted theme that is no longer bundled and not installed, so
|
|
// the store hydrates onto a paintable theme (no unstyled-:root flash).
|
|
migrateThemeSelection();
|
|
// Paint the correct theme on the very first frame (no Mocha flash).
|
|
applyThemeAtStartup();
|
|
configureStartupSplash();
|
|
installCrossWindowThemeSync();
|
|
markDevBuildDocument();
|
|
pushUserAgentToBackend();
|
|
pushLoggingModeToBackend();
|
|
installQueueUndoHotkey();
|
|
setupMusicNetworkRuntime();
|
|
}
|