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 { Box, VStack, Text, Divider } from "@chakra-ui/react";
import { Song } from "../types/interfaces"; import { Song } from "../types/interfaces";
import { formatDuration } from '../utils/formatters';
interface SongDetailsProps { interface SongDetailsProps {
song: Song | null; 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 }) => { export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => {
if (!song) { if (!song) {
return ( return (
<Box h="full" p={4}> <Box p={4} bg="gray.800" borderRadius="md">
<Text color="gray.500">Select a song to view details</Text> <Text color="gray.400">Select a song to view details</Text>
</Box> </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 = [ const details = [
{ label: "Title", value: song.title }, { label: "Title", value: song.title },
{ label: "Artist", value: song.artist }, { label: "Artist", value: song.artist },
{ label: "Duration", value: formatDuration(song.totalTime) },
{ label: "Album", value: song.album }, { label: "Album", value: song.album },
{ label: "Genre", value: song.genre }, { label: "Genre", value: song.genre },
{ label: "BPM", value: song.averageBpm }, { label: "BPM", value: song.averageBpm },
@ -25,12 +48,13 @@ export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => {
{ label: "Label", value: song.label }, { label: "Label", value: song.label },
{ label: "Mix", value: song.mix }, { label: "Mix", value: song.mix },
{ label: "Rating", value: song.rating }, { label: "Rating", value: song.rating },
{ label: "Bitrate", value: displayBitrate },
{ label: "Comments", value: song.comments }, { label: "Comments", value: song.comments },
].filter(detail => detail.value); ].filter(detail => detail.value);
return ( return (
<Box h="full"> <Box p={4} bg="gray.800" borderRadius="md">
<VStack align="stretch" spacing={4} p={4}> <VStack align="stretch" spacing={4}>
<Box> <Box>
<Text fontSize="lg" fontWeight="bold" color="white"> <Text fontSize="lg" fontWeight="bold" color="white">
{song.title} {song.title}

View File

@ -19,6 +19,7 @@ import type { Song, PlaylistNode } from "../types/interfaces";
import { useState, useCallback, useMemo } from "react"; import { useState, useCallback, useMemo } from "react";
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { formatDuration, formatTotalDuration } from '../utils/formatters';
interface SongListProps { interface SongListProps {
songs: Song[]; 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 ( return (
<Box> <Box>
{/* Search Bar */} {/* Search Bar */}
@ -115,7 +126,7 @@ export const SongList: React.FC<SongListProps> = ({
{/* Bulk Actions Toolbar */} {/* Bulk Actions Toolbar */}
<Flex justify="space-between" align="center" mb={4} p={2} bg="gray.800" borderRadius="md"> <Flex justify="space-between" align="center" mb={4} p={2} bg="gray.800" borderRadius="md">
<HStack> <HStack spacing={4}>
<Checkbox <Checkbox
isChecked={selectedSongs.size === filteredSongs.length} isChecked={selectedSongs.size === filteredSongs.length}
isIndeterminate={selectedSongs.size > 0 && selectedSongs.size < filteredSongs.length} isIndeterminate={selectedSongs.size > 0 && selectedSongs.size < filteredSongs.length}
@ -133,6 +144,9 @@ export const SongList: React.FC<SongListProps> = ({
? "Select All" ? "Select All"
: `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`} : `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`}
</Checkbox> </Checkbox>
<Text color="gray.400" fontSize="sm">
{filteredSongs.length} song{filteredSongs.length === 1 ? '' : 's'} {formatTotalDuration(totalDuration)}
</Text>
</HStack> </HStack>
{selectedSongs.size > 0 && ( {selectedSongs.size > 0 && (
@ -149,9 +163,8 @@ export const SongList: React.FC<SongListProps> = ({
{allPlaylists.map((playlist) => ( {allPlaylists.map((playlist) => (
<MenuItem <MenuItem
key={playlist.id} key={playlist.id}
onClick={(e) => { onClick={() => {
e.stopPropagation(); handleBulkAddToPlaylist(playlist.name);
onAddToPlaylist([Array.from(selectedSongs)[0]], playlist.name);
}} }}
> >
Add to {playlist.name} Add to {playlist.name}
@ -162,9 +175,9 @@ export const SongList: React.FC<SongListProps> = ({
<MenuDivider /> <MenuDivider />
<MenuItem <MenuItem
color="red.300" color="red.300"
onClick={(e) => { onClick={() => {
e.stopPropagation();
onRemoveFromPlaylist(Array.from(selectedSongs)); onRemoveFromPlaylist(Array.from(selectedSongs));
setSelectedSongs(new Set());
}} }}
> >
Remove from {currentPlaylist} Remove from {currentPlaylist}
@ -212,7 +225,7 @@ export const SongList: React.FC<SongListProps> = ({
fontSize={depth > 0 ? "xs" : "sm"} fontSize={depth > 0 ? "xs" : "sm"}
color={selectedSongId === song.id ? "gray.300" : "gray.500"} color={selectedSongId === song.id ? "gray.300" : "gray.500"}
> >
{song.artist} {song.artist} {formatDuration(song.totalTime)}
</Text> </Text>
</Box> </Box>
<Menu> <Menu>

View File

@ -39,13 +39,13 @@ export const useXmlParser = () => {
const { songs: parsedSongs, playlists: parsedPlaylists } = await parseXmlFile(xmlText); const { songs: parsedSongs, playlists: parsedPlaylists } = await parseXmlFile(xmlText);
// Save to backend // Save to backend
const [savedSongs, savedPlaylists] = await Promise.all([ await Promise.all([
api.saveSongs(parsedSongs), api.saveSongs(parsedSongs),
api.savePlaylists(parsedPlaylists) api.savePlaylists(parsedPlaylists)
]); ]);
setSongs(savedSongs); // Refresh the page to ensure all data is reloaded
setPlaylists(savedPlaylists); window.location.reload();
} catch (err) { } catch (err) {
console.error("Error processing XML:", 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')}`;
};