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