Some extra ui improvements
This commit is contained in:
parent
7e1f4e1cd4
commit
ab531462c2
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
22
packages/frontend/src/utils/formatters.ts
Normal file
22
packages/frontend/src/utils/formatters.ts
Normal 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')}`;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user