feat: add real-time playback position sync and skip buttons
Backend (Rust): - Add position tracking based on elapsed wall clock time - SharedState now tracks playback start time and position at start - get_state() returns real-time position instead of stale value - Position automatically pauses/resumes with playback Frontend (Svelte): - Replace dumb setInterval timer with backend polling (500ms) - Poll get_playback_state to sync position from Rust - Add onSkipBack/onSkipForward handlers to NowPlayingBar - Add onSkipBack/onSkipForward handlers to FullScreenNowPlaying - Wire skip buttons to handlers (restart if >3s, otherwise previous) - Add keyboard shortcuts: Ctrl+Left/Right for skip
This commit is contained in:
1 parent
b354161d82
commit
d1a13110ea
4 files changed
+177
-32
No files matched your search
@@ -12,6 +12,8 @@
|
||||
qualityLevel: number;
|
||||
isPlaying: boolean;
|
||||
onTogglePlay: () => void;
|
||||
onSkipBack?: () => void;
|
||||
onSkipForward?: () => void;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
onSeek: (time: number) => void;
|
||||
@@ -38,6 +40,8 @@
|
||||
qualityLevel,
|
||||
isPlaying,
|
||||
onTogglePlay,
|
||||
onSkipBack,
|
||||
onSkipForward,
|
||||
currentTime,
|
||||
duration,
|
||||
onSeek,
|
||||
@@ -170,7 +174,7 @@
|
||||
<button class="control-btn" class:active={isShuffle} onclick={onToggleShuffle}>
|
||||
<Shuffle size={24} />
|
||||
</button>
|
||||
<button class="control-btn primary">
|
||||
<button class="control-btn primary" onclick={onSkipBack}>
|
||||
<SkipBack size={32} />
|
||||
</button>
|
||||
<button
|
||||
@@ -186,7 +190,7 @@
|
||||
<Play size={28} fill="white" color="white" class="play-icon" />
|
||||
{/if}
|
||||
</button>
|
||||
<button class="control-btn primary">
|
||||
<button class="control-btn primary" onclick={onSkipForward}>
|
||||
<SkipForward size={32} />
|
||||
</button>
|
||||
<button class="control-btn" class:active={repeatMode !== 'off'} onclick={onToggleRepeat}>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
qualityLevel?: number;
|
||||
isPlaying?: boolean;
|
||||
onTogglePlay?: () => void;
|
||||
onSkipBack?: () => void;
|
||||
onSkipForward?: () => void;
|
||||
currentTime?: number;
|
||||
duration?: number;
|
||||
onSeek?: (time: number) => void;
|
||||
@@ -45,6 +47,8 @@
|
||||
qualityLevel = 5,
|
||||
isPlaying = false,
|
||||
onTogglePlay,
|
||||
onSkipBack,
|
||||
onSkipForward,
|
||||
currentTime = 201,
|
||||
duration = 345,
|
||||
onSeek,
|
||||
@@ -157,7 +161,7 @@
|
||||
>
|
||||
<Shuffle size={20} />
|
||||
</button>
|
||||
<button class="control-btn primary">
|
||||
<button class="control-btn primary" onclick={onSkipBack}>
|
||||
<SkipBack size={24} />
|
||||
</button>
|
||||
<button class="play-btn" onclick={onTogglePlay}>
|
||||
@@ -167,7 +171,7 @@
|
||||
<Play size={24} fill="white" color="white" class="play-icon" />
|
||||
{/if}
|
||||
</button>
|
||||
<button class="control-btn primary">
|
||||
<button class="control-btn primary" onclick={onSkipForward}>
|
||||
<SkipForward size={24} />
|
||||
</button>
|
||||
<button
|
||||
|
||||
+76
-14
@@ -322,6 +322,25 @@
|
||||
showToast(isFavorite ? 'Added to favorites' : 'Removed from favorites', 'success');
|
||||
}
|
||||
|
||||
// Skip track handlers (will be wired to queue when implemented)
|
||||
function handleSkipBack() {
|
||||
if (!currentTrack) return;
|
||||
// If more than 3 seconds in, restart track; otherwise go to previous
|
||||
if (currentTime > 3) {
|
||||
handleSeek(0);
|
||||
showToast('Restarted track', 'info');
|
||||
} else {
|
||||
// TODO: Go to previous track in queue
|
||||
showToast('Previous track (not implemented yet)', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSkipForward() {
|
||||
if (!currentTrack) return;
|
||||
// TODO: Go to next track in queue
|
||||
showToast('Next track (not implemented yet)', 'info');
|
||||
}
|
||||
|
||||
// Toast Functions
|
||||
function showToast(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
||||
toast = { message, type };
|
||||
@@ -364,6 +383,18 @@
|
||||
e.preventDefault();
|
||||
togglePlay();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault();
|
||||
handleSkipBack();
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault();
|
||||
handleSkipForward();
|
||||
}
|
||||
break;
|
||||
case 'f':
|
||||
if (!e.ctrlKey && !e.metaKey) {
|
||||
isFocusModeOpen = !isFocusModeOpen;
|
||||
@@ -380,25 +411,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Playback progress timer
|
||||
let progressInterval: ReturnType<typeof setInterval> | null = null;
|
||||
// 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;
|
||||
|
||||
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
|
||||
if (state.duration > 0 && state.position >= state.duration && !state.is_playing) {
|
||||
// Track finished - could trigger next track here
|
||||
console.log('Track finished playing');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to poll playback state:', err);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isPlaying && currentTrack) {
|
||||
progressInterval = setInterval(() => {
|
||||
if (currentTime < duration) {
|
||||
currentTime += 1;
|
||||
} else {
|
||||
isPlaying = false;
|
||||
}
|
||||
}, 1000);
|
||||
} else if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
if (currentTrack) {
|
||||
// Start polling when we have a track
|
||||
pollInterval = setInterval(pollPlaybackState, 500);
|
||||
// Also poll immediately
|
||||
pollPlaybackState();
|
||||
} else if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -467,6 +525,8 @@
|
||||
quality={currentTrack.quality}
|
||||
{isPlaying}
|
||||
onTogglePlay={togglePlay}
|
||||
onSkipBack={handleSkipBack}
|
||||
onSkipForward={handleSkipForward}
|
||||
{currentTime}
|
||||
{duration}
|
||||
onSeek={handleSeek}
|
||||
@@ -514,6 +574,8 @@
|
||||
qualityLevel={currentTrack.quality.includes('24') ? 5 : 3}
|
||||
{isPlaying}
|
||||
onTogglePlay={togglePlay}
|
||||
onSkipBack={handleSkipBack}
|
||||
onSkipForward={handleSkipForward}
|
||||
{currentTime}
|
||||
{duration}
|
||||
onSeek={handleSeek}
|
||||
|
||||
Reference in new issue
Block a user