Files
Psychotoxical-psysonic/src/app/bootstrap.ts
T
Psychotoxical cb1a110afb refactor(playback): move the audio engine into features/playback
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.
2026-06-30 15:00:20 +02:00

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();
}