diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index cc89052..0040bd3 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -7,6 +7,7 @@ import { PlaylistManager } from "./components/PlaylistManager";
import { SongDetails } from "./components/SongDetails";
import { Configuration } from "./pages/Configuration";
import { MusicStorage } from "./pages/MusicStorage";
+import { PersistentMusicPlayer } from "./components/PersistentMusicPlayer";
import { useXmlParser } from "./hooks/useXmlParser";
import { usePaginatedSongs } from "./hooks/usePaginatedSongs";
import { formatTotalDuration } from "./utils/formatters";
@@ -81,33 +82,17 @@ export default function RekordboxReader() {
}, []);
// Handle playing a song from the main view
- const handlePlaySong = useCallback(async (song: Song) => {
- try {
- // Check if song has S3 file
- if (song.s3File?.hasS3File) {
- setCurrentPlayingSong(song);
-
- // Get streaming URL
- const response = await fetch(`/api/music/${song.s3File.musicFileId}/stream`);
- if (response.ok) {
- const data = await response.json();
-
- // Create audio element and play
- const audio = new Audio(data.streamingUrl);
- audio.play().catch(error => {
- console.error('Error playing audio:', error);
- });
-
- // Update current playing song
- setCurrentPlayingSong(song);
- } else {
- console.error('Failed to get streaming URL');
- }
- }
- } catch (error) {
- console.error('Error playing song:', error);
+ const handlePlaySong = useCallback((song: Song) => {
+ // Check if song has S3 file
+ if (song.s3File?.hasS3File) {
+ setCurrentPlayingSong(song);
}
}, []);
+
+ // Handle closing the music player
+ const handleCloseMusicPlayer = useCallback(() => {
+ setCurrentPlayingSong(null);
+ }, []);
// Format total duration for display
const getFormattedTotalDuration = useCallback((durationSeconds?: number): string => {
@@ -678,6 +663,12 @@ export default function RekordboxReader() {
+
+ {/* Persistent Music Player */}
+
);
}
diff --git a/packages/frontend/src/components/PersistentMusicPlayer.tsx b/packages/frontend/src/components/PersistentMusicPlayer.tsx
new file mode 100644
index 0000000..2d0bd77
--- /dev/null
+++ b/packages/frontend/src/components/PersistentMusicPlayer.tsx
@@ -0,0 +1,331 @@
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ Box,
+ HStack,
+ VStack,
+ Text,
+ IconButton,
+ Slider,
+ SliderTrack,
+ SliderFilledTrack,
+ SliderThumb,
+ Icon,
+ useToast,
+ Badge,
+} from '@chakra-ui/react';
+import {
+ FiPlay,
+ FiPause,
+ FiSkipBack,
+ FiSkipForward,
+ FiVolume2,
+ FiVolumeX,
+ FiX,
+} from 'react-icons/fi';
+import type { Song } from '../types/interfaces';
+import { formatDuration } from '../utils/formatters';
+
+interface PersistentMusicPlayerProps {
+ currentSong: Song | null;
+ onClose: () => void;
+}
+
+export const PersistentMusicPlayer: React.FC = ({
+ currentSong,
+ onClose,
+}) => {
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [duration, setDuration] = useState(0);
+ const [volume, setVolume] = useState(1);
+ const [isMuted, setIsMuted] = useState(false);
+ const [streamingUrl, setStreamingUrl] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const audioRef = useRef(null);
+ const toast = useToast();
+
+ // Format time in MM:SS
+ const formatTime = (seconds: number): string => {
+ if (!seconds || isNaN(seconds)) return '00:00';
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ };
+
+ // Load streaming URL when current song changes
+ useEffect(() => {
+ if (currentSong && currentSong.s3File?.hasS3File) {
+ loadStreamingUrl();
+ } else {
+ setStreamingUrl(null);
+ setIsPlaying(false);
+ }
+ }, [currentSong]);
+
+ const loadStreamingUrl = async () => {
+ if (!currentSong?.s3File?.musicFileId) return;
+
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/music/${currentSong.s3File.musicFileId}/stream`);
+ if (response.ok) {
+ const data = await response.json();
+ setStreamingUrl(data.streamingUrl);
+ // Auto-play when URL is loaded
+ setTimeout(() => {
+ if (audioRef.current) {
+ audioRef.current.play().then(() => {
+ setIsPlaying(true);
+ }).catch(error => {
+ console.error('Error auto-playing:', error);
+ });
+ }
+ }, 100);
+ } else {
+ throw new Error('Failed to get streaming URL');
+ }
+ } catch (error) {
+ console.error('Error loading streaming URL:', error);
+ toast({
+ title: 'Error',
+ description: 'Failed to load music file',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handlePlay = () => {
+ if (audioRef.current) {
+ audioRef.current.play().then(() => {
+ setIsPlaying(true);
+ }).catch(error => {
+ console.error('Error playing:', error);
+ });
+ }
+ };
+
+ const handlePause = () => {
+ if (audioRef.current) {
+ audioRef.current.pause();
+ setIsPlaying(false);
+ }
+ };
+
+ const handleTimeUpdate = () => {
+ if (audioRef.current) {
+ setCurrentTime(audioRef.current.currentTime);
+ }
+ };
+
+ const handleLoadedMetadata = () => {
+ if (audioRef.current) {
+ setDuration(audioRef.current.duration);
+ }
+ };
+
+ const handleEnded = () => {
+ setIsPlaying(false);
+ setCurrentTime(0);
+ };
+
+ const handleSeek = (value: number) => {
+ if (audioRef.current) {
+ audioRef.current.currentTime = value;
+ setCurrentTime(value);
+ }
+ };
+
+ const handleVolumeChange = (value: number) => {
+ setVolume(value);
+ if (audioRef.current) {
+ audioRef.current.volume = value;
+ }
+ if (value === 0) {
+ setIsMuted(true);
+ } else if (isMuted) {
+ setIsMuted(false);
+ }
+ };
+
+ const toggleMute = () => {
+ if (audioRef.current) {
+ if (isMuted) {
+ audioRef.current.volume = volume;
+ setIsMuted(false);
+ } else {
+ audioRef.current.volume = 0;
+ setIsMuted(true);
+ }
+ }
+ };
+
+ const skipBackward = () => {
+ if (audioRef.current) {
+ audioRef.current.currentTime = Math.max(0, audioRef.current.currentTime - 10);
+ }
+ };
+
+ const skipForward = () => {
+ if (audioRef.current) {
+ audioRef.current.currentTime = Math.min(
+ audioRef.current.duration,
+ audioRef.current.currentTime + 10
+ );
+ }
+ };
+
+ if (!currentSong || !currentSong.s3File?.hasS3File) {
+ return null;
+ }
+
+ return (
+
+ {/* Hidden audio element */}
+
+ );
+};
\ No newline at end of file