diff --git a/packages/frontend/src/components/SongDetails.tsx b/packages/frontend/src/components/SongDetails.tsx index a9fddbb..4656ac3 100644 --- a/packages/frontend/src/components/SongDetails.tsx +++ b/packages/frontend/src/components/SongDetails.tsx @@ -1,22 +1,45 @@ import { Box, VStack, Text, Divider } from "@chakra-ui/react"; import { Song } from "../types/interfaces"; +import { formatDuration } from '../utils/formatters'; interface SongDetailsProps { song: Song | null; } +const calculateBitrate = (size: string, totalTime: string): number | null => { + if (!size || !totalTime) return null; + + // Convert size from bytes to bits + const bits = parseInt(size) * 8; + + // Convert duration to seconds (handle both milliseconds and seconds format) + const seconds = parseInt(totalTime) / (totalTime.length > 4 ? 1000 : 1); + + if (seconds <= 0) return null; + + // Calculate bitrate in kbps + return Math.round(bits / seconds / 1000); +}; + export const SongDetails: React.FC = ({ song }) => { if (!song) { return ( - - Select a song to view details + + Select a song to view details ); } + // Calculate bitrate if not directly available + const calculatedBitrate = song.size && song.totalTime ? calculateBitrate(song.size, song.totalTime) : null; + const displayBitrate = song.bitRate ? + `${song.bitRate} kbps` : + (calculatedBitrate ? `${calculatedBitrate} kbps (calculated)` : undefined); + const details = [ { label: "Title", value: song.title }, { label: "Artist", value: song.artist }, + { label: "Duration", value: formatDuration(song.totalTime) }, { label: "Album", value: song.album }, { label: "Genre", value: song.genre }, { label: "BPM", value: song.averageBpm }, @@ -25,12 +48,13 @@ export const SongDetails: React.FC = ({ song }) => { { label: "Label", value: song.label }, { label: "Mix", value: song.mix }, { label: "Rating", value: song.rating }, + { label: "Bitrate", value: displayBitrate }, { label: "Comments", value: song.comments }, ].filter(detail => detail.value); return ( - - + + {song.title} diff --git a/packages/frontend/src/components/SongList.tsx b/packages/frontend/src/components/SongList.tsx index 425a731..038e8d4 100644 --- a/packages/frontend/src/components/SongList.tsx +++ b/packages/frontend/src/components/SongList.tsx @@ -19,6 +19,7 @@ import type { Song, PlaylistNode } from "../types/interfaces"; import { useState, useCallback, useMemo } from "react"; import type { MouseEvent } from 'react'; import { v4 as uuidv4 } from 'uuid'; +import { formatDuration, formatTotalDuration } from '../utils/formatters'; interface SongListProps { songs: Song[]; @@ -95,6 +96,16 @@ export const SongList: React.FC = ({ } }; + // Calculate total duration + const totalDuration = useMemo(() => { + return filteredSongs.reduce((total, song) => { + if (!song.totalTime) return total; + // Convert to seconds, handling milliseconds if present + const seconds = Math.floor(Number(song.totalTime) / (song.totalTime.length > 4 ? 1000 : 1)); + return total + seconds; + }, 0); + }, [filteredSongs]); + return ( {/* Search Bar */} @@ -115,7 +126,7 @@ export const SongList: React.FC = ({ {/* Bulk Actions Toolbar */} - + 0 && selectedSongs.size < filteredSongs.length} @@ -133,6 +144,9 @@ export const SongList: React.FC = ({ ? "Select All" : `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`} + + {filteredSongs.length} song{filteredSongs.length === 1 ? '' : 's'} • {formatTotalDuration(totalDuration)} + {selectedSongs.size > 0 && ( @@ -149,9 +163,8 @@ export const SongList: React.FC = ({ {allPlaylists.map((playlist) => ( { - e.stopPropagation(); - onAddToPlaylist([Array.from(selectedSongs)[0]], playlist.name); + onClick={() => { + handleBulkAddToPlaylist(playlist.name); }} > Add to {playlist.name} @@ -162,9 +175,9 @@ export const SongList: React.FC = ({ { - e.stopPropagation(); + onClick={() => { onRemoveFromPlaylist(Array.from(selectedSongs)); + setSelectedSongs(new Set()); }} > Remove from {currentPlaylist} @@ -212,7 +225,7 @@ export const SongList: React.FC = ({ fontSize={depth > 0 ? "xs" : "sm"} color={selectedSongId === song.id ? "gray.300" : "gray.500"} > - {song.artist} + {song.artist} • {formatDuration(song.totalTime)} diff --git a/packages/frontend/src/hooks/useXmlParser.ts b/packages/frontend/src/hooks/useXmlParser.ts index 5be052c..9510f93 100644 --- a/packages/frontend/src/hooks/useXmlParser.ts +++ b/packages/frontend/src/hooks/useXmlParser.ts @@ -39,13 +39,13 @@ export const useXmlParser = () => { const { songs: parsedSongs, playlists: parsedPlaylists } = await parseXmlFile(xmlText); // Save to backend - const [savedSongs, savedPlaylists] = await Promise.all([ + await Promise.all([ api.saveSongs(parsedSongs), api.savePlaylists(parsedPlaylists) ]); - setSongs(savedSongs); - setPlaylists(savedPlaylists); + // Refresh the page to ensure all data is reloaded + window.location.reload(); } catch (err) { console.error("Error processing XML:", err); } diff --git a/packages/frontend/src/utils/formatters.ts b/packages/frontend/src/utils/formatters.ts new file mode 100644 index 0000000..2988c53 --- /dev/null +++ b/packages/frontend/src/utils/formatters.ts @@ -0,0 +1,22 @@ +export const formatDuration = (totalTime?: string): string => { + if (!totalTime) return '--:--'; + + // Convert to number and handle milliseconds if present + const seconds = Math.floor(Number(totalTime) / (totalTime.length > 4 ? 1000 : 1)); + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; +}; + +export const formatTotalDuration = (totalSeconds: number): string => { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +}; \ No newline at end of file