diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index 8419a86e..d42fb1b8 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -1,4 +1,5 @@ import { initAudioListeners } from '../store/initAudioListeners'; +import '../store/playbackEngineBridgeRegister'; // installs the playback-engine bridge at boot import { lazy, Suspense, useEffect, useState } from 'react'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { invoke } from '@tauri-apps/api/core'; diff --git a/src/store/authAudioSettingsActions.ts b/src/store/authAudioSettingsActions.ts index 53fe5204..b2bd99de 100644 --- a/src/store/authAudioSettingsActions.ts +++ b/src/store/authAudioSettingsActions.ts @@ -4,7 +4,7 @@ import { sanitizeAutodjOverlapCapSec, } from '../utils/playback/autodjOverlapCap'; import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from './authStoreDefaults'; -import { usePlayerStore } from './playerStore'; +import { updateReplayGainForCurrentTrack } from './playbackEngineBridge'; import type { AuthState } from './authStoreTypes'; type SetState = ( @@ -43,15 +43,15 @@ export function createAudioSettingsActions(set: SetState): Pick< return { setReplayGainEnabled: (v) => { set({ replayGainEnabled: v }); - usePlayerStore.getState().updateReplayGainForCurrentTrack(); + updateReplayGainForCurrentTrack(); }, setNormalizationEngine: (v) => { set({ normalizationEngine: v }); - usePlayerStore.getState().updateReplayGainForCurrentTrack(); + updateReplayGainForCurrentTrack(); }, setLoudnessTargetLufs: (v) => { set({ loudnessTargetLufs: v }); - usePlayerStore.getState().updateReplayGainForCurrentTrack(); + updateReplayGainForCurrentTrack(); }, setLoudnessPreAnalysisAttenuationDb: (v) => { const n = typeof v === 'number' ? v : Number(v); @@ -60,19 +60,19 @@ export function createAudioSettingsActions(set: SetState): Pick< }, resetLoudnessPreAnalysisAttenuationDbDefault: () => { set({ loudnessPreAnalysisAttenuationDb: DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB }); - usePlayerStore.getState().updateReplayGainForCurrentTrack(); + updateReplayGainForCurrentTrack(); }, setReplayGainMode: (v) => { set({ replayGainMode: v }); - usePlayerStore.getState().updateReplayGainForCurrentTrack(); + updateReplayGainForCurrentTrack(); }, setReplayGainPreGainDb: (v) => { set({ replayGainPreGainDb: v }); - usePlayerStore.getState().updateReplayGainForCurrentTrack(); + updateReplayGainForCurrentTrack(); }, setReplayGainFallbackDb: (v) => { set({ replayGainFallbackDb: v }); - usePlayerStore.getState().updateReplayGainForCurrentTrack(); + updateReplayGainForCurrentTrack(); }, setCrossfadeEnabled: (v) => set({ crossfadeEnabled: v }), setCrossfadeSecs: (v) => set({ crossfadeSecs: v }), diff --git a/src/store/authServerProfileActions.ts b/src/store/authServerProfileActions.ts index 2b263e16..56c6f761 100644 --- a/src/store/authServerProfileActions.ts +++ b/src/store/authServerProfileActions.ts @@ -1,7 +1,6 @@ import type { AuthState } from './authStoreTypes'; import { generateId } from './authStoreHelpers'; -import { usePlayerStore } from './playerStore'; -import { clearQueueServerForPlayback } from '../utils/playback/playbackServer'; +import { getQueueServerId, clearQueueServerForPlayback } from './playbackEngineBridge'; import { resolveServerIdForIndexKey } from '../utils/server/serverLookup'; type SetState = ( @@ -44,7 +43,7 @@ export function createServerProfileActions(set: SetState): Pick< // queueServerId is the canonical index key (B1); resolve the // canonical id back to a server UUID before comparing so a profile // delete still clears the matching queue binding. - const queueSid = usePlayerStore.getState().queueServerId; + const queueSid = getQueueServerId(); if (queueSid && resolveServerIdForIndexKey(queueSid) === id) { clearQueueServerForPlayback(); } diff --git a/src/store/authStore.servers.test.ts b/src/store/authStore.servers.test.ts index f0116d37..235bee49 100644 --- a/src/store/authStore.servers.test.ts +++ b/src/store/authStore.servers.test.ts @@ -13,6 +13,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { useAuthStore } from './authStore'; import { usePlayerStore } from './playerStore'; +import './playbackEngineBridgeRegister'; // wire removeServer's queue-clear through the real engine bridge import { resetAuthStore } from '@/test/helpers/storeReset'; import { resetPlayerStore } from '@/test/helpers/storeReset'; diff --git a/src/store/playbackEngineBridge.ts b/src/store/playbackEngineBridge.ts new file mode 100644 index 00000000..c4ae900c --- /dev/null +++ b/src/store/playbackEngineBridge.ts @@ -0,0 +1,39 @@ +// Playback-engine bridge. Global settings/profile stores (authStore family) need +// to trigger a few engine effects — clear a deleted server's queue binding, catch +// the current track up to a changed ReplayGain/normalization mode — but those +// stores are core and must not import the playback engine (iron rule; the engine +// will live in @/features/playback). The engine registers its operations here at +// boot (playbackEngineBridgeRegister.ts, side-effect-imported by MainApp); core +// callers invoke these neutral delegators. +// +// Default (unregistered) = no-op / null. Safe: the only callers are user-triggered +// settings/profile actions that fire long after boot, by which point the engine +// (loaded at app start) has registered. At boot there is no current track or queue +// binding, so the no-op default would be correct even if it were ever reached. + +export interface PlaybackEngineBridge { + /** Active queue's bound server id, or null when nothing is bound. */ + getQueueServerId(): string | null; + /** Drop the queue's server binding (used when its server profile is deleted). */ + clearQueueServerForPlayback(): void; + /** Re-apply ReplayGain/normalization to the currently playing track. */ + updateReplayGainForCurrentTrack(): void; +} + +let bridge: PlaybackEngineBridge | null = null; + +export function registerPlaybackEngineBridge(impl: PlaybackEngineBridge): void { + bridge = impl; +} + +export function getQueueServerId(): string | null { + return bridge ? bridge.getQueueServerId() : null; +} + +export function clearQueueServerForPlayback(): void { + bridge?.clearQueueServerForPlayback(); +} + +export function updateReplayGainForCurrentTrack(): void { + bridge?.updateReplayGainForCurrentTrack(); +} diff --git a/src/store/playbackEngineBridgeRegister.ts b/src/store/playbackEngineBridgeRegister.ts new file mode 100644 index 00000000..1a50a7f0 --- /dev/null +++ b/src/store/playbackEngineBridgeRegister.ts @@ -0,0 +1,13 @@ +// Engine-side registration for the playback-engine bridge. Side-effect module: +// importing it installs the engine's operations into @/store/playbackEngineBridge. +// MainApp side-effect-imports this at boot. Lives with the engine (moves into +// @/features/playback alongside playerStore); the bridge itself stays in core. +import { usePlayerStore } from './playerStore'; +import { clearQueueServerForPlayback } from '../utils/playback/playbackServer'; +import { registerPlaybackEngineBridge } from './playbackEngineBridge'; + +registerPlaybackEngineBridge({ + getQueueServerId: () => usePlayerStore.getState().queueServerId, + clearQueueServerForPlayback, + updateReplayGainForCurrentTrack: () => usePlayerStore.getState().updateReplayGainForCurrentTrack(), +});