Some extra ui improvements

This commit is contained in:
Geert Rademakes 2025-04-25 09:38:53 +02:00
parent 7e1f4e1cd4
commit ab531462c2
4 changed files with 73 additions and 14 deletions

View File

@ -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<SongDetailsProps> = ({ song }) => {
if (!song) {
return (
<Box h="full" p={4}>
<Text color="gray.500">Select a song to view details</Text>
<Box p={4} bg="gray.800" borderRadius="md">
<Text color="gray.400">Select a song to view details</Text>
</Box>
);
}
// 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<SongDetailsProps> = ({ 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 (
<Box h="full">
<VStack align="stretch" spacing={4} p={4}>
<Box p={4} bg="gray.800" borderRadius="md">
<VStack align="stretch" spacing={4}>
<Box>
<Text fontSize="lg" fontWeight="bold" color="white">
{song.title}

View File

@ -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<SongListProps> = ({
}
};
// 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 (
<Box>
{/* Search Bar */}
@ -115,7 +126,7 @@ export const SongList: React.FC<SongListProps> = ({
{/* Bulk Actions Toolbar */}
<Flex justify="space-between" align="center" mb={4} p={2} bg="gray.800" borderRadius="md">
<HStack>
<HStack spacing={4}>
<Checkbox
isChecked={selectedSongs.size === filteredSongs.length}
isIndeterminate={selectedSongs.size > 0 && selectedSongs.size < filteredSongs.length}
@ -133,6 +144,9 @@ export const SongList: React.FC<SongListProps> = ({
? "Select All"
: `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`}
</Checkbox>
<Text color="gray.400" fontSize="sm">
{filteredSongs.length} song{filteredSongs.length === 1 ? '' : 's'} {formatTotalDuration(totalDuration)}
</Text>
</HStack>
{selectedSongs.size > 0 && (
@ -149,9 +163,8 @@ export const SongList: React.FC<SongListProps> = ({
{allPlaylists.map((playlist) => (
<MenuItem
key={playlist.id}
onClick={(e) => {
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<SongListProps> = ({
<MenuDivider />
<MenuItem
color="red.300"
onClick={(e) => {
e.stopPropagation();
onClick={() => {
onRemoveFromPlaylist(Array.from(selectedSongs));
setSelectedSongs(new Set());
}}
>
Remove from {currentPlaylist}
@ -212,7 +225,7 @@ export const SongList: React.FC<SongListProps> = ({
fontSize={depth > 0 ? "xs" : "sm"}
color={selectedSongId === song.id ? "gray.300" : "gray.500"}
>
{song.artist}
{song.artist} {formatDuration(song.totalTime)}
</Text>
</Box>
<Menu>

View File

@ -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);
}

View File

@ -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')}`;
};