mirror of
https://github.com/markuryy/shark.git
synced 2025-12-13 12:01:01 +00:00
feat(ui): add now playing panel and context menu for tracks
This commit is contained in:
164
src/lib/services/audioPlayer.ts
Normal file
164
src/lib/services/audioPlayer.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { playback } from '$lib/stores/playback';
|
||||
import type { Track } from '$lib/types/track';
|
||||
|
||||
class AudioPlayer {
|
||||
private audio: HTMLAudioElement | null = null;
|
||||
private currentTrackPath: string | null = null;
|
||||
private isSeeking = false;
|
||||
|
||||
constructor() {
|
||||
if (typeof window !== 'undefined') {
|
||||
this.audio = new Audio();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
private setupEventListeners() {
|
||||
if (!this.audio) return;
|
||||
|
||||
// Time updates
|
||||
this.audio.addEventListener('timeupdate', () => {
|
||||
if (this.audio && !this.isSeeking) {
|
||||
playback.setCurrentTime(this.audio.currentTime);
|
||||
}
|
||||
});
|
||||
|
||||
// Seeking events
|
||||
this.audio.addEventListener('seeking', () => {
|
||||
this.isSeeking = true;
|
||||
});
|
||||
|
||||
this.audio.addEventListener('seeked', () => {
|
||||
this.isSeeking = false;
|
||||
if (this.audio) {
|
||||
playback.setCurrentTime(this.audio.currentTime);
|
||||
}
|
||||
});
|
||||
|
||||
// Duration loaded
|
||||
this.audio.addEventListener('loadedmetadata', () => {
|
||||
if (this.audio) {
|
||||
playback.setDuration(this.audio.duration);
|
||||
}
|
||||
});
|
||||
|
||||
// Track ended - auto-advance
|
||||
this.audio.addEventListener('ended', () => {
|
||||
console.log('[AudioPlayer] Track ended, advancing to next');
|
||||
playback.next();
|
||||
});
|
||||
|
||||
// Error handling
|
||||
this.audio.addEventListener('error', (e) => {
|
||||
console.error('[AudioPlayer] Playback error:', e);
|
||||
const error = this.audio?.error;
|
||||
|
||||
if (error) {
|
||||
console.error(`[AudioPlayer] Error code: ${error.code}, message: ${error.message}`);
|
||||
}
|
||||
|
||||
// Skip to next track on error
|
||||
console.log('[AudioPlayer] Skipping to next track due to error');
|
||||
playback.next();
|
||||
});
|
||||
}
|
||||
|
||||
async loadTrack(track: Track) {
|
||||
if (!this.audio) {
|
||||
console.error('[AudioPlayer] Audio element not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert file path to Tauri asset URL
|
||||
const audioUrl = convertFileSrc(track.path);
|
||||
|
||||
console.log('[AudioPlayer] Loading track:', track.metadata.title || track.filename);
|
||||
console.log('[AudioPlayer] File path:', track.path);
|
||||
console.log('[AudioPlayer] Asset URL:', audioUrl);
|
||||
|
||||
// Only reload if different track
|
||||
if (this.currentTrackPath !== track.path) {
|
||||
this.audio.src = audioUrl;
|
||||
this.currentTrackPath = track.path;
|
||||
|
||||
// Check for LRC file (same path but .lrc extension)
|
||||
const lrcPath = track.path.replace(/\.(flac|mp3|opus|ogg|m4a|wav)$/i, '.lrc');
|
||||
playback.setLrcPath(lrcPath);
|
||||
|
||||
await this.audio.load();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AudioPlayer] Error loading track:', error);
|
||||
// Skip to next on load error
|
||||
playback.next();
|
||||
}
|
||||
}
|
||||
|
||||
async play() {
|
||||
if (!this.audio) return;
|
||||
|
||||
try {
|
||||
await this.audio.play();
|
||||
} catch (error) {
|
||||
console.error('[AudioPlayer] Error playing:', error);
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.audio) return;
|
||||
this.audio.pause();
|
||||
}
|
||||
|
||||
seek(time: number) {
|
||||
if (!this.audio) return;
|
||||
this.audio.currentTime = time;
|
||||
}
|
||||
|
||||
setVolume(volume: number) {
|
||||
if (!this.audio) return;
|
||||
this.audio.volume = Math.max(0, Math.min(1, volume));
|
||||
}
|
||||
|
||||
getCurrentTime(): number {
|
||||
return this.audio?.currentTime || 0;
|
||||
}
|
||||
|
||||
getDuration(): number {
|
||||
return this.audio?.duration || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const audioPlayer = new AudioPlayer();
|
||||
|
||||
// Watch playback state and control audio
|
||||
if (typeof window !== 'undefined') {
|
||||
let prevTrack: Track | null = null;
|
||||
let prevIsPlaying = false;
|
||||
|
||||
playback.subscribe(state => {
|
||||
const { currentTrack, isPlaying } = state;
|
||||
|
||||
// Track changed
|
||||
if (currentTrack && currentTrack !== prevTrack) {
|
||||
audioPlayer.loadTrack(currentTrack).then(() => {
|
||||
if (isPlaying) {
|
||||
audioPlayer.play();
|
||||
}
|
||||
});
|
||||
prevTrack = currentTrack;
|
||||
}
|
||||
|
||||
// Play/pause state changed
|
||||
if (isPlaying !== prevIsPlaying) {
|
||||
if (isPlaying && currentTrack) {
|
||||
audioPlayer.play();
|
||||
} else {
|
||||
audioPlayer.pause();
|
||||
}
|
||||
prevIsPlaying = isPlaying;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user