import { invoke } from '@tauri-apps/api/core'; import { setIsAudioPaused } from './engineState'; import type { PlayerState } from './playerStoreTypes'; import { flushQueueSyncToServer } from './queueSync'; import { playListenSessionFinalize, playListenSessionOnPause } from './playListenSession'; import { playbackReportPaused, playbackReportStopped } from './playbackReportSession'; import { pauseRadio, stopRadio } from './radioPlayer'; import { clearAllPlaybackScheduleTimers } from './scheduleTimers'; import { clearSeekDebounce } from './seekDebounce'; import { clearSeekFallbackRetry } from './seekFallbackState'; import { clearSeekTarget } from './seekTargetState'; import { tryAcquireTogglePlayLock } from './togglePlayLock'; import { refreshWaveformForTrack } from './waveformRefresh'; type SetState = ( partial: Partial | ((state: PlayerState) => Partial), ) => void; type GetState = () => PlayerState; /** * Light transport actions — everything except `resume` (own module, * see `resumeAction.ts`) and scheduled timers (`scheduleActions.ts`). * `togglePlay` is guarded so a double media-key tap can't race * pause + resume into a stuck state. `resetAudioPause` flips the * engine-paused flag without touching the UI `isPlaying`, used by * `audio:ended` paths. */ export function createTransportLightActions(set: SetState, get: GetState): Pick< PlayerState, 'stop' | 'pause' | 'resetAudioPause' | 'togglePlay' > { return { stop: () => { void playListenSessionFinalize('stop'); // Report stopped before the position is reset below so the server drops the // now-playing entry at the right point (playbackReport extension). void playbackReportStopped(); clearAllPlaybackScheduleTimers(); const wasRadio = !!get().currentRadio; if (wasRadio) { stopRadio(); } else { invoke('audio_stop').catch(console.error); } setIsAudioPaused(false); clearSeekFallbackRetry(); clearSeekDebounce(); clearSeekTarget(); // Stop keeps `currentTrack` (the bar still shows the stopped song), so its // waveform stays valid. Radio has no analysis waveform — drop the bins. const keptTrackId = wasRadio ? null : get().currentTrack?.id ?? null; set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0, currentRadio: null, ...(keptTrackId ? {} : { waveformBins: null }), normalizationNowDb: null, normalizationTargetLufs: null, normalizationEngineLive: 'off', currentPlaybackSource: null, enginePreloadedTrackId: null, scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null, }); // Re-hydrate from the analysis DB in case the bins were never loaded or // only partially filled while the (now stopped) track was playing. if (keptTrackId) void refreshWaveformForTrack(keptTrackId); }, pause: () => { clearAllPlaybackScheduleTimers(); playListenSessionOnPause(); if (get().currentRadio) { pauseRadio(); } else { invoke('audio_pause').catch(console.error); setIsAudioPaused(true); playbackReportPaused(get().currentTime); // Flush position so a quick close after pause still leaves the // server with the right resume point for other devices. const s = get(); if (s.currentTrack) { void flushQueueSyncToServer(s.queueItems, s.currentTrack, s.currentTime); } } set({ isPlaying: false, scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null }); }, resetAudioPause: () => { setIsAudioPaused(false); }, togglePlay: () => { if (!tryAcquireTogglePlayLock()) return; const { isPlaying } = get(); isPlaying ? get().pause() : get().resume(); }, }; }