refactor: extract player state into playerStore
Move playback state management into dedicated store: - PlayingTrack interface - isPlaying, currentTime, duration, volume, isFavorite - Polling logic with setOnTrackEnded callback - Playback controls (togglePlay, seek, setVolume)
This commit is contained in:
1 parent
cef950b08b
commit
55ec44cdc2
2 files changed
+423
-123
No files matched your search
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* Player State Store
|
||||
*
|
||||
* Manages playback state including current track, play/pause, position, volume.
|
||||
* Currently uses polling to sync with backend - designed to be replaced with
|
||||
* Tauri events in the future.
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
export interface PlayingTrack {
|
||||
id: number;
|
||||
title: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
artwork: string;
|
||||
duration: number;
|
||||
quality: string;
|
||||
bitDepth?: number;
|
||||
samplingRate?: number;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
|
||||
interface BackendPlaybackState {
|
||||
is_playing: boolean;
|
||||
position: number;
|
||||
duration: number;
|
||||
track_id: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
// ============ State ============
|
||||
|
||||
let currentTrack: PlayingTrack | null = null;
|
||||
let isPlaying = false;
|
||||
let currentTime = 0;
|
||||
let duration = 0;
|
||||
let volume = 75;
|
||||
let isFavorite = false;
|
||||
|
||||
// Polling state
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let isAdvancingTrack = false;
|
||||
let isSkipping = false;
|
||||
let queueEnded = false;
|
||||
|
||||
// Callbacks for track advancement (set by consumer)
|
||||
let onTrackEnded: (() => Promise<void>) | null = null;
|
||||
|
||||
// Listeners
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function notifyListeners(): void {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to player state changes
|
||||
*/
|
||||
export function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
listener(); // Immediately notify with current state
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
// ============ Getters ============
|
||||
|
||||
export function getCurrentTrack(): PlayingTrack | null {
|
||||
return currentTrack;
|
||||
}
|
||||
|
||||
export function getIsPlaying(): boolean {
|
||||
return isPlaying;
|
||||
}
|
||||
|
||||
export function getCurrentTime(): number {
|
||||
return currentTime;
|
||||
}
|
||||
|
||||
export function getDuration(): number {
|
||||
return duration;
|
||||
}
|
||||
|
||||
export function getVolume(): number {
|
||||
return volume;
|
||||
}
|
||||
|
||||
export function getIsFavorite(): boolean {
|
||||
return isFavorite;
|
||||
}
|
||||
|
||||
export function getIsSkipping(): boolean {
|
||||
return isSkipping;
|
||||
}
|
||||
|
||||
// ============ State Setter ============
|
||||
|
||||
export interface PlayerState {
|
||||
currentTrack: PlayingTrack | null;
|
||||
isPlaying: boolean;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
volume: number;
|
||||
isFavorite: boolean;
|
||||
isSkipping: boolean;
|
||||
}
|
||||
|
||||
export function getPlayerState(): PlayerState {
|
||||
return {
|
||||
currentTrack,
|
||||
isPlaying,
|
||||
currentTime,
|
||||
duration,
|
||||
volume,
|
||||
isFavorite,
|
||||
isSkipping
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Track Actions ============
|
||||
|
||||
/**
|
||||
* Set the current track (called when starting playback)
|
||||
*/
|
||||
export function setCurrentTrack(track: PlayingTrack | null): void {
|
||||
currentTrack = track;
|
||||
if (track) {
|
||||
duration = track.duration;
|
||||
currentTime = 0;
|
||||
queueEnded = false;
|
||||
} else {
|
||||
duration = 0;
|
||||
currentTime = 0;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set favorite status
|
||||
*/
|
||||
export function setIsFavorite(favorite: boolean): void {
|
||||
isFavorite = favorite;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set skipping state (prevents concurrent skip operations)
|
||||
*/
|
||||
export function setIsSkipping(skipping: boolean): void {
|
||||
isSkipping = skipping;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark queue as ended (prevents spam when no more tracks)
|
||||
*/
|
||||
export function setQueueEnded(ended: boolean): void {
|
||||
queueEnded = ended;
|
||||
}
|
||||
|
||||
// ============ Playback Controls ============
|
||||
|
||||
/**
|
||||
* Toggle play/pause
|
||||
*/
|
||||
export async function togglePlay(): Promise<void> {
|
||||
if (!currentTrack) return;
|
||||
|
||||
const newIsPlaying = !isPlaying;
|
||||
isPlaying = newIsPlaying;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (newIsPlaying) {
|
||||
await invoke('resume_playback');
|
||||
} else {
|
||||
await invoke('pause_playback');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle playback:', err);
|
||||
// Revert on error
|
||||
isPlaying = !newIsPlaying;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set playing state directly
|
||||
*/
|
||||
export function setIsPlaying(playing: boolean): void {
|
||||
isPlaying = playing;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to position
|
||||
*/
|
||||
export async function seek(position: number): Promise<void> {
|
||||
const clampedPosition = Math.max(0, Math.min(duration, position));
|
||||
currentTime = clampedPosition;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await invoke('seek', { position: Math.floor(clampedPosition) });
|
||||
} catch (err) {
|
||||
console.error('Failed to seek:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set volume (0-100)
|
||||
*/
|
||||
export async function setVolume(newVolume: number): Promise<void> {
|
||||
const clampedVolume = Math.max(0, Math.min(100, newVolume));
|
||||
volume = clampedVolume;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await invoke('set_volume', { volume: clampedVolume / 100 });
|
||||
} catch (err) {
|
||||
console.error('Failed to set volume:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop playback
|
||||
*/
|
||||
export async function stop(): Promise<void> {
|
||||
try {
|
||||
await invoke('stop_playback');
|
||||
isPlaying = false;
|
||||
currentTrack = null;
|
||||
currentTime = 0;
|
||||
duration = 0;
|
||||
notifyListeners();
|
||||
} catch (err) {
|
||||
console.error('Failed to stop playback:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Polling ============
|
||||
|
||||
/**
|
||||
* Set callback for when track ends (for auto-advance)
|
||||
*/
|
||||
export function setOnTrackEnded(callback: () => Promise<void>): void {
|
||||
onTrackEnded = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll playback state from backend
|
||||
*/
|
||||
async function pollPlaybackState(): Promise<void> {
|
||||
if (!currentTrack) return;
|
||||
|
||||
try {
|
||||
const state = await invoke<BackendPlaybackState>('get_playback_state');
|
||||
|
||||
// Only update if we have a matching track
|
||||
if (state.track_id === currentTrack.id) {
|
||||
currentTime = state.position;
|
||||
isPlaying = state.is_playing;
|
||||
notifyListeners();
|
||||
|
||||
// Check if track ended - auto-advance to next
|
||||
if (
|
||||
state.duration > 0 &&
|
||||
state.position >= state.duration - 1 &&
|
||||
!state.is_playing &&
|
||||
!isAdvancingTrack &&
|
||||
!queueEnded &&
|
||||
onTrackEnded
|
||||
) {
|
||||
console.log('Track finished, advancing to next...');
|
||||
isAdvancingTrack = true;
|
||||
|
||||
try {
|
||||
await onTrackEnded();
|
||||
} catch (err) {
|
||||
console.error('Failed to auto-advance:', err);
|
||||
} finally {
|
||||
isAdvancingTrack = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to poll playback state:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling for playback state
|
||||
*/
|
||||
export function startPolling(): void {
|
||||
if (pollInterval) return;
|
||||
|
||||
pollInterval = setInterval(pollPlaybackState, 500);
|
||||
pollPlaybackState(); // Poll immediately
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop polling
|
||||
*/
|
||||
export function stopPolling(): void {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if polling is active
|
||||
*/
|
||||
export function isPollingActive(): boolean {
|
||||
return pollInterval !== null;
|
||||
}
|
||||
|
||||
// ============ Cleanup ============
|
||||
|
||||
/**
|
||||
* Reset all state (for logout)
|
||||
*/
|
||||
export function reset(): void {
|
||||
stopPolling();
|
||||
currentTrack = null;
|
||||
isPlaying = false;
|
||||
currentTime = 0;
|
||||
duration = 0;
|
||||
isFavorite = false;
|
||||
isAdvancingTrack = false;
|
||||
isSkipping = false;
|
||||
queueEnded = false;
|
||||
notifyListeners();
|
||||
}
|
||||
+85
-123
@@ -65,6 +65,26 @@
|
||||
type NavigationState
|
||||
} from '$lib/stores/navigationStore';
|
||||
|
||||
// Player state management
|
||||
import {
|
||||
subscribe as subscribePlayer,
|
||||
setCurrentTrack,
|
||||
setIsPlaying,
|
||||
setIsFavorite,
|
||||
setIsSkipping,
|
||||
setQueueEnded,
|
||||
setOnTrackEnded,
|
||||
togglePlay,
|
||||
seek as playerSeek,
|
||||
setVolume as playerSetVolume,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
reset as resetPlayer,
|
||||
getPlayerState,
|
||||
type PlayingTrack,
|
||||
type PlayerState
|
||||
} from '$lib/stores/playerStore';
|
||||
|
||||
// Components
|
||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||
import NowPlayingBar from '$lib/components/NowPlayingBar.svelte';
|
||||
@@ -228,18 +248,6 @@
|
||||
total_tracks: number;
|
||||
}
|
||||
|
||||
interface PlayingTrack {
|
||||
id: number;
|
||||
title: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
artwork: string;
|
||||
duration: number;
|
||||
quality: string;
|
||||
bitDepth?: number;
|
||||
samplingRate?: number;
|
||||
}
|
||||
|
||||
// Auth State (from authStore subscription)
|
||||
let isLoggedIn = $state(false);
|
||||
let userInfo = $state<UserInfo | null>(null);
|
||||
@@ -266,15 +274,17 @@
|
||||
// Sidebar reference for refreshing playlists
|
||||
let sidebarRef: { getPlaylists: () => { id: number; name: string; tracks_count: number }[], refreshPlaylists: () => void } | undefined;
|
||||
|
||||
// Playback State
|
||||
// Playback State (from playerStore subscription)
|
||||
let currentTrack = $state<PlayingTrack | null>(null);
|
||||
let isPlaying = $state(false);
|
||||
let currentTime = $state(0);
|
||||
let duration = $state(0);
|
||||
let volume = $state(75);
|
||||
let isFavorite = $state(false);
|
||||
|
||||
// Queue/Shuffle State (will move to queueStore)
|
||||
let isShuffle = $state(false);
|
||||
let repeatMode = $state<'off' | 'all' | 'one'>('off');
|
||||
let isFavorite = $state(false);
|
||||
|
||||
// Queue State (synced from backend)
|
||||
let queue = $state<QueueTrack[]>([]);
|
||||
@@ -539,7 +549,7 @@
|
||||
? `${track.maximum_bit_depth}bit/${track.maximum_sampling_rate}kHz`
|
||||
: 'CD Quality';
|
||||
|
||||
currentTrack = {
|
||||
const newTrack: PlayingTrack = {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.performer?.name || 'Unknown Artist',
|
||||
@@ -550,9 +560,7 @@
|
||||
bitDepth: track.maximum_bit_depth,
|
||||
samplingRate: track.maximum_sampling_rate
|
||||
};
|
||||
|
||||
duration = track.duration;
|
||||
currentTime = 0;
|
||||
setCurrentTrack(newTrack);
|
||||
|
||||
// Try to play the track
|
||||
try {
|
||||
@@ -560,7 +568,7 @@
|
||||
showToast(`Loading: ${track.title}`, 'info');
|
||||
await invoke('play_track', { trackId: track.id });
|
||||
console.log('play_track invoke succeeded');
|
||||
isPlaying = true;
|
||||
setIsPlaying(true);
|
||||
showToast(`Playing: ${track.title}`, 'success');
|
||||
|
||||
// Update MPRIS metadata for system media controls
|
||||
@@ -581,11 +589,11 @@
|
||||
);
|
||||
|
||||
// Check if track is favorite
|
||||
isFavorite = await checkTrackFavorite(track.id);
|
||||
setIsFavorite(await checkTrackFavorite(track.id));
|
||||
} catch (err) {
|
||||
console.error('Failed to play track:', err);
|
||||
showToast(`Playback error: ${err}`, 'error');
|
||||
isPlaying = false;
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +607,7 @@
|
||||
? `${track.bitDepth}bit/${track.samplingRate}kHz`
|
||||
: 'CD Quality';
|
||||
|
||||
currentTrack = {
|
||||
const newTrack: PlayingTrack = {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist || selectedAlbum?.artist || 'Unknown Artist',
|
||||
@@ -608,9 +616,7 @@
|
||||
duration: track.durationSeconds,
|
||||
quality
|
||||
};
|
||||
|
||||
duration = track.durationSeconds;
|
||||
currentTime = 0;
|
||||
setCurrentTrack(newTrack);
|
||||
|
||||
// Build queue from album tracks
|
||||
if (selectedAlbum?.tracks) {
|
||||
@@ -646,7 +652,7 @@
|
||||
showToast(`Loading: ${track.title}`, 'info');
|
||||
await invoke('play_track', { trackId: track.id });
|
||||
console.log('play_track invoke succeeded');
|
||||
isPlaying = true;
|
||||
setIsPlaying(true);
|
||||
showToast(`Playing: ${track.title}`, 'success');
|
||||
|
||||
// Update MPRIS metadata for system media controls
|
||||
@@ -677,30 +683,17 @@
|
||||
} catch (err) {
|
||||
console.error('Failed to play track:', err);
|
||||
showToast(`Playback error: ${err}`, 'error');
|
||||
isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
if (!currentTrack) return;
|
||||
isPlaying = !isPlaying;
|
||||
|
||||
// TODO: invoke pause/resume commands
|
||||
if (isPlaying) {
|
||||
invoke('resume_playback').catch(console.error);
|
||||
} else {
|
||||
invoke('pause_playback').catch(console.error);
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Playback controls (delegating to playerStore)
|
||||
function handleSeek(time: number) {
|
||||
currentTime = Math.max(0, Math.min(duration, time));
|
||||
invoke('seek', { position: time }).catch(console.error);
|
||||
playerSeek(time);
|
||||
}
|
||||
|
||||
function handleVolumeChange(newVolume: number) {
|
||||
volume = Math.max(0, Math.min(100, newVolume));
|
||||
invoke('set_volume', { volume: newVolume / 100 }).catch(console.error);
|
||||
playerSetVolume(newVolume);
|
||||
}
|
||||
|
||||
async function toggleShuffle() {
|
||||
@@ -757,7 +750,7 @@
|
||||
await invoke('remove_favorite', { favType: 'track', itemId: trackId });
|
||||
showToast('Removed from favorites', 'success');
|
||||
}
|
||||
isFavorite = newFavoriteState;
|
||||
setIsFavorite(newFavoriteState);
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle favorite:', err);
|
||||
showToast('Failed to update favorites', 'error');
|
||||
@@ -766,14 +759,15 @@
|
||||
|
||||
// Skip track handlers - wired to backend queue
|
||||
async function handleSkipBack() {
|
||||
if (!currentTrack || isSkipping) return;
|
||||
const playerState = getPlayerState();
|
||||
if (!playerState.currentTrack || playerState.isSkipping) return;
|
||||
// If more than 3 seconds in, restart track; otherwise go to previous
|
||||
if (currentTime > 3) {
|
||||
if (playerState.currentTime > 3) {
|
||||
handleSeek(0);
|
||||
return;
|
||||
}
|
||||
|
||||
isSkipping = true;
|
||||
setIsSkipping(true);
|
||||
try {
|
||||
const prevTrack = await invoke<BackendQueueTrack | null>('previous_track');
|
||||
if (prevTrack) {
|
||||
@@ -786,14 +780,15 @@
|
||||
console.error('Failed to go to previous track:', err);
|
||||
showToast('Failed to go to previous track', 'error');
|
||||
} finally {
|
||||
isSkipping = false;
|
||||
setIsSkipping(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSkipForward() {
|
||||
if (!currentTrack || isSkipping) return;
|
||||
const playerState = getPlayerState();
|
||||
if (!playerState.currentTrack || playerState.isSkipping) return;
|
||||
|
||||
isSkipping = true;
|
||||
setIsSkipping(true);
|
||||
try {
|
||||
const nextTrackResult = await invoke<BackendQueueTrack | null>('next_track');
|
||||
if (nextTrackResult) {
|
||||
@@ -801,14 +796,14 @@
|
||||
} else {
|
||||
// No next track - stop playback
|
||||
await invoke('stop_playback');
|
||||
isPlaying = false;
|
||||
setIsPlaying(false);
|
||||
showToast('Queue ended', 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to go to next track:', err);
|
||||
showToast('Failed to go to next track', 'error');
|
||||
} finally {
|
||||
isSkipping = false;
|
||||
setIsSkipping(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -817,20 +812,19 @@
|
||||
const isLocalTrack = localTrackIds.has(track.id);
|
||||
|
||||
// Reset queue ended flag when playing a new track
|
||||
queueEnded = false;
|
||||
setQueueEnded(false);
|
||||
|
||||
currentTrack = {
|
||||
const newTrack: PlayingTrack = {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
album: track.album,
|
||||
artwork: track.artwork_url || '',
|
||||
duration: track.duration_secs,
|
||||
quality: isLocalTrack ? 'Local' : 'Hi-Res'
|
||||
quality: isLocalTrack ? 'Local' : 'Hi-Res',
|
||||
isLocal: isLocalTrack
|
||||
};
|
||||
|
||||
duration = track.duration_secs;
|
||||
currentTime = 0;
|
||||
setCurrentTrack(newTrack);
|
||||
|
||||
try {
|
||||
// Use appropriate playback command based on track source
|
||||
@@ -839,7 +833,7 @@
|
||||
} else {
|
||||
await invoke('play_track', { trackId: track.id });
|
||||
}
|
||||
isPlaying = true;
|
||||
setIsPlaying(true);
|
||||
|
||||
// Update MPRIS
|
||||
await invoke('set_media_metadata', {
|
||||
@@ -858,9 +852,9 @@
|
||||
|
||||
// Check if track is favorite (for Qobuz tracks only)
|
||||
if (!isLocalTrack) {
|
||||
isFavorite = await checkTrackFavorite(track.id);
|
||||
setIsFavorite(await checkTrackFavorite(track.id));
|
||||
} else {
|
||||
isFavorite = false;
|
||||
setIsFavorite(false);
|
||||
}
|
||||
|
||||
// Refresh queue state
|
||||
@@ -868,7 +862,7 @@
|
||||
} catch (err) {
|
||||
console.error('Failed to play queue track:', err);
|
||||
showToast(`Playback error: ${err}`, 'error');
|
||||
isPlaying = false;
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1410,72 +1404,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Playback state polling - sync with backend every 500ms
|
||||
interface PlaybackState {
|
||||
is_playing: boolean;
|
||||
position: number;
|
||||
duration: number;
|
||||
track_id: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
let isAdvancingTrack = false; // Prevent multiple advances
|
||||
let isSkipping = false; // Prevent concurrent skip operations
|
||||
let queueEnded = false; // Prevent spam when queue has no more tracks
|
||||
|
||||
async function pollPlaybackState() {
|
||||
if (!currentTrack) return;
|
||||
|
||||
try {
|
||||
const state = await invoke<PlaybackState>('get_playback_state');
|
||||
|
||||
// Only update if we have a matching track
|
||||
if (state.track_id === currentTrack.id) {
|
||||
currentTime = state.position;
|
||||
isPlaying = state.is_playing;
|
||||
|
||||
// Check if track ended - auto-advance to next
|
||||
// Don't try if queue already ended or we're already advancing
|
||||
if (state.duration > 0 && state.position >= state.duration - 1 && !state.is_playing && !isAdvancingTrack && !queueEnded) {
|
||||
console.log('Track finished, advancing to next...');
|
||||
isAdvancingTrack = true;
|
||||
|
||||
try {
|
||||
const nextTrackResult = await invoke<BackendQueueTrack | null>('next_track');
|
||||
if (nextTrackResult) {
|
||||
await playQueueTrack(nextTrackResult);
|
||||
} else {
|
||||
// Queue ended - set flag to prevent further attempts
|
||||
console.log('Queue ended');
|
||||
queueEnded = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to auto-advance:', err);
|
||||
} finally {
|
||||
isAdvancingTrack = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to poll playback state:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Playback state polling - managed by playerStore
|
||||
// Start/stop polling based on whether there's a current track
|
||||
$effect(() => {
|
||||
if (currentTrack) {
|
||||
// Start polling when we have a track
|
||||
pollInterval = setInterval(pollPlaybackState, 500);
|
||||
// Also poll immediately
|
||||
pollPlaybackState();
|
||||
} else if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
startPolling();
|
||||
} else {
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
stopPolling();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1542,6 +1481,27 @@
|
||||
selectedPlaylistId = navState.selectedPlaylistId;
|
||||
});
|
||||
|
||||
// Subscribe to player state changes
|
||||
const unsubscribePlayer = subscribePlayer(() => {
|
||||
const playerState = getPlayerState();
|
||||
currentTrack = playerState.currentTrack;
|
||||
isPlaying = playerState.isPlaying;
|
||||
currentTime = playerState.currentTime;
|
||||
duration = playerState.duration;
|
||||
volume = playerState.volume;
|
||||
isFavorite = playerState.isFavorite;
|
||||
});
|
||||
|
||||
// Set up track ended callback for auto-advance
|
||||
setOnTrackEnded(async () => {
|
||||
const nextTrackResult = await invoke<BackendQueueTrack | null>('next_track');
|
||||
if (nextTrackResult) {
|
||||
await playQueueTrack(nextTrackResult);
|
||||
} else {
|
||||
setQueueEnded(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore Last.fm session on app startup
|
||||
(async () => {
|
||||
try {
|
||||
@@ -1576,6 +1536,8 @@
|
||||
unsubscribeUI();
|
||||
unsubscribeAuth();
|
||||
unsubscribeNav();
|
||||
unsubscribePlayer();
|
||||
stopPolling();
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user