mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(player): E.40 — extract next action (#604)
Move the ~180-LOC `next` action body into `runNext(set, get, manual)` in `src/store/nextAction.ts`, following the helper-with-thin-wrapper pattern from E.37–E.39. Covers all three top-level outcomes: - Has next slot → playTrack + proactive infinite-queue / radio top-up when ≤ 2 of each remain ahead. Both top-ups skipped inside an Orbit session (host owns the queue). - Queue exhausted, repeat=all → wrap to index 0. - Queue exhausted, repeat=off → stop, unless radio-flagged or infinite queue is enabled (each fetches a fresh batch + continues), or an Orbit session is active (stop locally, let useOrbitGuest sync). Pure code-move. playerStore.ts: 702 → 515 LOC (−187).
This commit is contained in:
committed by
GitHub
parent
7e902b918c
commit
3130a0ad74
@@ -0,0 +1,222 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getSimilarSongs2, getTopSongs } from '../api/subsonic';
|
||||
import { buildInfiniteQueueCandidates } from '../utils/buildInfiniteQueueCandidates';
|
||||
import { songToTrack } from '../utils/songToTrack';
|
||||
import { useAuthStore } from './authStore';
|
||||
import { setIsAudioPaused } from './engineState';
|
||||
import {
|
||||
isInfiniteQueueFetching,
|
||||
setInfiniteQueueFetching,
|
||||
} from './infiniteQueueState';
|
||||
import { isInOrbitSession } from './orbitSession';
|
||||
import type { PlayerState, Track } from './playerStoreTypes';
|
||||
import {
|
||||
addRadioSessionSeen,
|
||||
getCurrentRadioArtistId,
|
||||
hasRadioSessionSeen,
|
||||
isRadioFetching,
|
||||
setRadioFetching,
|
||||
} from './radioSessionState';
|
||||
import { applySkipStarOnManualNext } from './skipStarRating';
|
||||
|
||||
type SetState = (
|
||||
partial: Partial<PlayerState> | ((state: PlayerState) => Partial<PlayerState>),
|
||||
) => void;
|
||||
type GetState = () => PlayerState;
|
||||
|
||||
/**
|
||||
* Advance to the next track. Three top-level outcomes:
|
||||
*
|
||||
* 1. **Has next slot** — `playTrack` the queue's `queueIndex + 1`,
|
||||
* then proactively top up auto-added (infinite-queue) and
|
||||
* radio-added tracks when ≤ 2 of each remain ahead. Both top-ups
|
||||
* are skipped inside an Orbit session — the host owns the queue,
|
||||
* and a silent local extension would drift the guest off the host
|
||||
* or pop the bulk-add modal at the next track-end fallback.
|
||||
*
|
||||
* 2. **Queue exhausted, repeat=all** — wrap back to index 0.
|
||||
*
|
||||
* 3. **Queue exhausted, repeat=off** — stop, unless:
|
||||
* - The current track is radio-flagged → fetch a fresh radio batch
|
||||
* and continue.
|
||||
* - Infinite queue is enabled → fetch more candidates and continue.
|
||||
* - Orbit session active → stop locally and let `useOrbitGuest`
|
||||
* sync to the host's next track.
|
||||
*/
|
||||
export function runNext(set: SetState, get: GetState, manual: boolean): void {
|
||||
const { queue, queueIndex, repeatMode, currentTrack } = get();
|
||||
applySkipStarOnManualNext(currentTrack, manual);
|
||||
const nextIdx = queueIndex + 1;
|
||||
if (nextIdx < queue.length) {
|
||||
get().playTrack(queue[nextIdx], queue, manual, false, nextIdx);
|
||||
// Proactively top up auto-added tracks when ≤ 2 remain ahead,
|
||||
// so the queue never runs dry without a visible loading pause.
|
||||
// Skipped while in Orbit — the host's queue is the source of
|
||||
// truth there, and any silent local extension would either
|
||||
// drift this client off the host or pop the bulk-add modal at
|
||||
// the next track-end fallback.
|
||||
const { infiniteQueueEnabled } = useAuthStore.getState();
|
||||
if (infiniteQueueEnabled && repeatMode === 'off' && !isInfiniteQueueFetching() && !isInOrbitSession()) {
|
||||
const remainingAuto = queue.slice(nextIdx + 1).filter(t => t.autoAdded).length;
|
||||
if (remainingAuto <= 2) {
|
||||
setInfiniteQueueFetching(true);
|
||||
const existingIds = new Set(get().queue.map(t => t.id));
|
||||
buildInfiniteQueueCandidates(currentTrack, existingIds, 5).then(newTracks => {
|
||||
// Re-check at resolution time — the user may have joined
|
||||
// an Orbit session between scheduling and resolving.
|
||||
if (isInOrbitSession()) return;
|
||||
if (newTracks.length > 0) {
|
||||
set(state => ({ queue: [...state.queue, ...newTracks] }));
|
||||
}
|
||||
}).catch(() => {}).finally(() => { setInfiniteQueueFetching(false); });
|
||||
}
|
||||
}
|
||||
// Proactively top up radio tracks when ≤ 2 remain — always, regardless
|
||||
// of infinite queue setting.
|
||||
const nextTrack = queue[nextIdx];
|
||||
if (nextTrack.radioAdded && !isRadioFetching()) {
|
||||
const remainingRadio = queue.slice(nextIdx + 1).filter(t => t.radioAdded).length;
|
||||
if (remainingRadio <= 2) {
|
||||
const artistId = nextTrack.artistId ?? getCurrentRadioArtistId() ?? null;
|
||||
const artistName = nextTrack.artist;
|
||||
if (artistId) {
|
||||
setRadioFetching(true);
|
||||
Promise.all([getSimilarSongs2(artistId), getTopSongs(artistName)])
|
||||
.then(([similar, top]) => {
|
||||
const existingIds = new Set(get().queue.map(t => t.id));
|
||||
// Lead with similar (other artists) for variety; top tracks
|
||||
// of the upcoming artist are only a fallback when similar
|
||||
// is empty. Single-pass loop dedupes against the live queue,
|
||||
// the session seen-set, and intra-batch overlap (issue #500).
|
||||
const sourceList = similar.length > 0 ? similar : top;
|
||||
const fresh: Track[] = [];
|
||||
for (const raw of sourceList) {
|
||||
if (fresh.length >= 10) break;
|
||||
const t = songToTrack(raw);
|
||||
if (existingIds.has(t.id) || hasRadioSessionSeen(t.id)) continue;
|
||||
addRadioSessionSeen(t.id);
|
||||
fresh.push({ ...t, radioAdded: true as const });
|
||||
}
|
||||
if (fresh.length > 0) {
|
||||
// Trim played tracks from the front to keep the queue bounded.
|
||||
// Without trimming the queue grows unboundedly, making every
|
||||
// Zustand persist write larger and causing UI freezes over time.
|
||||
// Keep the last HISTORY_KEEP played tracks so the user can still
|
||||
// navigate backwards a few songs. Trimmed ids stay in the seen-set.
|
||||
const HISTORY_KEEP = 5;
|
||||
set(state => {
|
||||
const trimStart = Math.max(0, state.queueIndex - HISTORY_KEEP);
|
||||
return {
|
||||
queue: [...state.queue.slice(trimStart), ...fresh],
|
||||
queueIndex: state.queueIndex - trimStart,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => { setRadioFetching(false); });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (repeatMode === 'all' && queue.length > 0) {
|
||||
get().playTrack(queue[0], queue, manual, false, 0);
|
||||
} else {
|
||||
// ── Orbit short-circuit ──
|
||||
// The host owns the shared queue. The radio / infinite-queue
|
||||
// fallbacks below would either pop the orbitBulkGuard modal (with a
|
||||
// 6-track add) or silently inject unrelated tracks into the local
|
||||
// player and drift the guest off the host. Stop instead and let the
|
||||
// next pull tick in `useOrbitGuest` sync to the host's next track.
|
||||
// Covers any active orbit phase (`active` / `joining` / `starting`)
|
||||
// so a fetch scheduled mid-join doesn't slip through.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
// Queue exhausted. Check radio first (independent of infinite queue setting),
|
||||
// then infinite queue, then stop.
|
||||
if (currentTrack?.radioAdded && !isRadioFetching()) {
|
||||
const artistId = currentTrack.artistId ?? getCurrentRadioArtistId() ?? null;
|
||||
if (artistId) {
|
||||
setRadioFetching(true);
|
||||
Promise.all([getSimilarSongs2(artistId), getTopSongs(currentTrack.artist)])
|
||||
.then(([similar, top]) => {
|
||||
setRadioFetching(false);
|
||||
// The user may have joined an Orbit session while this
|
||||
// fetch was in flight — bail without touching the queue.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
const existingIds = new Set(get().queue.map(t => t.id));
|
||||
// Same source preference + dedup contract as the proactive
|
||||
// top-up: similar first, top only as a fallback (issue #500).
|
||||
const sourceList = similar.length > 0 ? similar : top;
|
||||
const fresh: Track[] = [];
|
||||
for (const raw of sourceList) {
|
||||
if (fresh.length >= 10) break;
|
||||
const t = songToTrack(raw);
|
||||
if (existingIds.has(t.id) || hasRadioSessionSeen(t.id)) continue;
|
||||
addRadioSessionSeen(t.id);
|
||||
fresh.push({ ...t, radioAdded: true as const });
|
||||
}
|
||||
if (fresh.length > 0) {
|
||||
const currentQueue = get().queue;
|
||||
const newQueue = [...currentQueue, ...fresh];
|
||||
get().playTrack(fresh[0], newQueue, false, false, currentQueue.length);
|
||||
} else {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setRadioFetching(false);
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { infiniteQueueEnabled } = useAuthStore.getState();
|
||||
if (infiniteQueueEnabled && repeatMode === 'off') {
|
||||
if (isInfiniteQueueFetching()) return;
|
||||
setInfiniteQueueFetching(true);
|
||||
const existingIds = new Set(get().queue.map(t => t.id));
|
||||
buildInfiniteQueueCandidates(currentTrack, existingIds, 5).then(newTracks => {
|
||||
setInfiniteQueueFetching(false);
|
||||
// The user may have joined an Orbit session while this
|
||||
// fetch was in flight — bail without invoking playTrack.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
if (newTracks.length === 0) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
const currentQueue = get().queue;
|
||||
const newQueue = [...currentQueue, ...newTracks];
|
||||
get().playTrack(newTracks[0], newQueue, false);
|
||||
}).catch(() => {
|
||||
setInfiniteQueueFetching(false);
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
});
|
||||
} else {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-192
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { buildStreamUrl, savePlayQueue, reportNowPlaying, getSong, getSimilarSongs2, getTopSongs, setRating } from '../api/subsonic';
|
||||
import { buildStreamUrl, savePlayQueue, reportNowPlaying, getSong, setRating } from '../api/subsonic';
|
||||
import { resolvePlaybackUrl } from '../utils/resolvePlaybackUrl';
|
||||
import { setDeferHotCachePrefetch } from '../utils/hotCacheGate';
|
||||
import { lastfmUpdateNowPlaying, lastfmGetTrackLoved } from '../api/lastfm';
|
||||
@@ -13,7 +13,6 @@ import { useOrbitStore } from './orbitStore';
|
||||
import { resolveReplayGainDb } from '../utils/resolveReplayGainDb';
|
||||
import { shuffleArray } from '../utils/shuffleArray';
|
||||
import { songToTrack } from '../utils/songToTrack';
|
||||
import { buildInfiniteQueueCandidates } from '../utils/buildInfiniteQueueCandidates';
|
||||
import {
|
||||
queuesStructuralEqual,
|
||||
sameQueueTrackId,
|
||||
@@ -40,7 +39,6 @@ import {
|
||||
clearAllPlaybackScheduleTimers,
|
||||
} from './scheduleTimers';
|
||||
import { touchHotCacheOnPlayback } from './hotCacheTouch';
|
||||
import { applySkipStarOnManualNext } from './skipStarRating';
|
||||
import { resetLoudnessBackfillStateForTrackId } from './loudnessBackfillState';
|
||||
import {
|
||||
flushPlayQueuePosition,
|
||||
@@ -72,17 +70,6 @@ import {
|
||||
getPlayGeneration,
|
||||
setIsAudioPaused,
|
||||
} from './engineState';
|
||||
import {
|
||||
addRadioSessionSeen,
|
||||
getCurrentRadioArtistId,
|
||||
hasRadioSessionSeen,
|
||||
isRadioFetching,
|
||||
setRadioFetching,
|
||||
} from './radioSessionState';
|
||||
import {
|
||||
isInfiniteQueueFetching,
|
||||
setInfiniteQueueFetching,
|
||||
} from './infiniteQueueState';
|
||||
import { initAudioListeners } from './initAudioListeners';
|
||||
import { installQueueUndoHotkey } from './queueUndoHotkey';
|
||||
import { readInitialQueueVisibility } from './queueVisibilityStorage';
|
||||
@@ -132,6 +119,7 @@ import type { PlayerState, Track } from './playerStoreTypes';
|
||||
export type { PlayerState, Track };
|
||||
import { createLastfmActions } from './lastfmActions';
|
||||
import { createMiscActions } from './miscActions';
|
||||
import { runNext } from './nextAction';
|
||||
import { runResume } from './resumeAction';
|
||||
import { runSeek } from './seekAction';
|
||||
import { runUpdateReplayGainForCurrentTrack } from './updateReplayGainAction';
|
||||
@@ -479,184 +467,9 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
// ── resume ───────────────────────────────────────────────────────────────
|
||||
resume: () => runResume(set, get),
|
||||
|
||||
// ── next / previous ──────────────────────────────────────────────────────
|
||||
next: (manual = true) => {
|
||||
const { queue, queueIndex, repeatMode, currentTrack } = get();
|
||||
applySkipStarOnManualNext(currentTrack, manual);
|
||||
const nextIdx = queueIndex + 1;
|
||||
if (nextIdx < queue.length) {
|
||||
get().playTrack(queue[nextIdx], queue, manual, false, nextIdx);
|
||||
// Proactively top up auto-added tracks when ≤ 2 remain ahead,
|
||||
// so the queue never runs dry without a visible loading pause.
|
||||
// Skipped while in Orbit — the host's queue is the source of
|
||||
// truth there, and any silent local extension would either
|
||||
// drift this client off the host or pop the bulk-add modal at
|
||||
// the next track-end fallback.
|
||||
const { infiniteQueueEnabled } = useAuthStore.getState();
|
||||
if (infiniteQueueEnabled && repeatMode === 'off' && !isInfiniteQueueFetching() && !isInOrbitSession()) {
|
||||
const remainingAuto = queue.slice(nextIdx + 1).filter(t => t.autoAdded).length;
|
||||
if (remainingAuto <= 2) {
|
||||
setInfiniteQueueFetching(true);
|
||||
const existingIds = new Set(get().queue.map(t => t.id));
|
||||
buildInfiniteQueueCandidates(currentTrack, existingIds, 5).then(newTracks => {
|
||||
// Re-check at resolution time — the user may have joined
|
||||
// an Orbit session between scheduling and resolving.
|
||||
if (isInOrbitSession()) return;
|
||||
if (newTracks.length > 0) {
|
||||
set(state => ({ queue: [...state.queue, ...newTracks] }));
|
||||
}
|
||||
}).catch(() => {}).finally(() => { setInfiniteQueueFetching(false); });
|
||||
}
|
||||
}
|
||||
// Proactively top up radio tracks when ≤ 2 remain — always, regardless
|
||||
// of infinite queue setting.
|
||||
const nextTrack = queue[nextIdx];
|
||||
if (nextTrack.radioAdded && !isRadioFetching()) {
|
||||
const remainingRadio = queue.slice(nextIdx + 1).filter(t => t.radioAdded).length;
|
||||
if (remainingRadio <= 2) {
|
||||
const artistId = nextTrack.artistId ?? getCurrentRadioArtistId() ?? null;
|
||||
const artistName = nextTrack.artist;
|
||||
if (artistId) {
|
||||
setRadioFetching(true);
|
||||
Promise.all([getSimilarSongs2(artistId), getTopSongs(artistName)])
|
||||
.then(([similar, top]) => {
|
||||
const existingIds = new Set(get().queue.map(t => t.id));
|
||||
// Lead with similar (other artists) for variety; top tracks
|
||||
// of the upcoming artist are only a fallback when similar
|
||||
// is empty. Single-pass loop dedupes against the live queue,
|
||||
// the session seen-set, and intra-batch overlap (issue #500).
|
||||
const sourceList = similar.length > 0 ? similar : top;
|
||||
const fresh: Track[] = [];
|
||||
for (const raw of sourceList) {
|
||||
if (fresh.length >= 10) break;
|
||||
const t = songToTrack(raw);
|
||||
if (existingIds.has(t.id) || hasRadioSessionSeen(t.id)) continue;
|
||||
addRadioSessionSeen(t.id);
|
||||
fresh.push({ ...t, radioAdded: true as const });
|
||||
}
|
||||
if (fresh.length > 0) {
|
||||
// Trim played tracks from the front to keep the queue bounded.
|
||||
// Without trimming the queue grows unboundedly, making every
|
||||
// Zustand persist write larger and causing UI freezes over time.
|
||||
// Keep the last HISTORY_KEEP played tracks so the user can still
|
||||
// navigate backwards a few songs. Trimmed ids stay in the seen-set.
|
||||
const HISTORY_KEEP = 5;
|
||||
set(state => {
|
||||
const trimStart = Math.max(0, state.queueIndex - HISTORY_KEEP);
|
||||
return {
|
||||
queue: [...state.queue.slice(trimStart), ...fresh],
|
||||
queueIndex: state.queueIndex - trimStart,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => { setRadioFetching(false); });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (repeatMode === 'all' && queue.length > 0) {
|
||||
get().playTrack(queue[0], queue, manual, false, 0);
|
||||
} else {
|
||||
// ── Orbit short-circuit ──
|
||||
// The host owns the shared queue. The radio / infinite-queue
|
||||
// fallbacks below would either pop the orbitBulkGuard modal (with a
|
||||
// 6-track add) or silently inject unrelated tracks into the local
|
||||
// player and drift the guest off the host. Stop instead and let the
|
||||
// next pull tick in `useOrbitGuest` sync to the host's next track.
|
||||
// Covers any active orbit phase (`active` / `joining` / `starting`)
|
||||
// so a fetch scheduled mid-join doesn't slip through.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
// Queue exhausted. Check radio first (independent of infinite queue setting),
|
||||
// then infinite queue, then stop.
|
||||
if (currentTrack?.radioAdded && !isRadioFetching()) {
|
||||
const artistId = currentTrack.artistId ?? getCurrentRadioArtistId() ?? null;
|
||||
if (artistId) {
|
||||
setRadioFetching(true);
|
||||
Promise.all([getSimilarSongs2(artistId), getTopSongs(currentTrack.artist)])
|
||||
.then(([similar, top]) => {
|
||||
setRadioFetching(false);
|
||||
// The user may have joined an Orbit session while this
|
||||
// fetch was in flight — bail without touching the queue.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
const existingIds = new Set(get().queue.map(t => t.id));
|
||||
// Same source preference + dedup contract as the proactive
|
||||
// top-up: similar first, top only as a fallback (issue #500).
|
||||
const sourceList = similar.length > 0 ? similar : top;
|
||||
const fresh: Track[] = [];
|
||||
for (const raw of sourceList) {
|
||||
if (fresh.length >= 10) break;
|
||||
const t = songToTrack(raw);
|
||||
if (existingIds.has(t.id) || hasRadioSessionSeen(t.id)) continue;
|
||||
addRadioSessionSeen(t.id);
|
||||
fresh.push({ ...t, radioAdded: true as const });
|
||||
}
|
||||
if (fresh.length > 0) {
|
||||
const currentQueue = get().queue;
|
||||
const newQueue = [...currentQueue, ...fresh];
|
||||
get().playTrack(fresh[0], newQueue, false, false, currentQueue.length);
|
||||
} else {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setRadioFetching(false);
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { infiniteQueueEnabled } = useAuthStore.getState();
|
||||
if (infiniteQueueEnabled && repeatMode === 'off') {
|
||||
if (isInfiniteQueueFetching()) return;
|
||||
setInfiniteQueueFetching(true);
|
||||
const existingIds = new Set(get().queue.map(t => t.id));
|
||||
buildInfiniteQueueCandidates(currentTrack, existingIds, 5).then(newTracks => {
|
||||
setInfiniteQueueFetching(false);
|
||||
// The user may have joined an Orbit session while this
|
||||
// fetch was in flight — bail without invoking playTrack.
|
||||
if (isInOrbitSession()) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
if (newTracks.length === 0) {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
const currentQueue = get().queue;
|
||||
const newQueue = [...currentQueue, ...newTracks];
|
||||
get().playTrack(newTracks[0], newQueue, false);
|
||||
}).catch(() => {
|
||||
setInfiniteQueueFetching(false);
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
});
|
||||
} else {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
setIsAudioPaused(false);
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
}
|
||||
}
|
||||
},
|
||||
// ── next ────────────────────────────────────────────────────────────────
|
||||
next: (manual = true) => runNext(set, get, manual),
|
||||
|
||||
|
||||
// ── seek ─────────────────────────────────────────────────────────────────
|
||||
seek: (progress) => runSeek(set, get, progress),
|
||||
|
||||
Reference in New Issue
Block a user