feat: UX improvements for frontend

- Clear selected song when changing playlists (already implemented)
- Replace 'Add to playlist...' and 'Remove from playlist' buttons with 'Actions' dropdown in PaginatedSongList
- Add key (tonality) next to duration in song list display format (e.g., 1:16 - A8)
- Update song matching page title to include storage provider (e.g., 'Sync and Matching (WEBDAV)')
- Remove storage provider text from sync buttons for cleaner interface
- Fix bulk selection state clearing when switching playlists to prevent stale selections
This commit is contained in:
Geert Rademakes 2025-09-19 10:19:40 +02:00
parent e58d42bea2
commit aa04849442
3 changed files with 53 additions and 27 deletions

View File

@ -12,8 +12,13 @@ import {
Input,
InputGroup,
InputLeftElement,
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
} from '@chakra-ui/react';
import { Search2Icon } from '@chakra-ui/icons';
import { Search2Icon, ChevronDownIcon } from '@chakra-ui/icons';
import { FiPlay } from 'react-icons/fi';
import type { Song, PlaylistNode } from '../types/interfaces';
import { api } from '../services/api';
@ -131,7 +136,7 @@ const SongItem = memo<{
</Box>
<Box textAlign="right" ml={2}>
<Text fontSize="xs" color="gray.500">
{formattedDuration}
{formattedDuration}{song.tonality ? ` - ${song.tonality}` : ''}
</Text>
<Text fontSize="xs" color="gray.600">
{song.averageBpm} BPM
@ -202,6 +207,13 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
loadingRef_state.current = loading;
onLoadMoreRef.current = onLoadMore;
}, [hasMore, loading, onLoadMore]);
// Clear selection when switching playlists
useEffect(() => {
if (isSwitchingPlaylist) {
setSelectedSongs(new Set());
}
}, [isSwitchingPlaylist]);
// Debounce search to prevent excessive API calls
const debouncedSearchQuery = useDebounce(localSearchQuery, 300);
@ -522,27 +534,32 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
</HStack>
{selectedSongs.size > 0 && (
<HStack spacing={2}>
<Button
<Menu>
<MenuButton
as={Button}
size="sm"
colorScheme="blue"
onClick={onPlaylistModalOpen}
rightIcon={<ChevronDownIcon />}
>
Add to Playlist...
</Button>
{currentPlaylist !== "All Songs" && onRemoveFromPlaylist && (
<Button
size="sm"
variant="outline"
colorScheme="red"
onClick={() => {
handleBulkRemoveFromPlaylist();
}}
>
Remove from {currentPlaylist}
</Button>
)}
</HStack>
Actions
</MenuButton>
<MenuList>
<MenuItem onClick={onPlaylistModalOpen}>
Add to Playlist...
</MenuItem>
{currentPlaylist !== "All Songs" && onRemoveFromPlaylist && (
<>
<MenuDivider />
<MenuItem
color="red.300"
onClick={handleBulkRemoveFromPlaylist}
>
Remove from {currentPlaylist}
</MenuItem>
</>
)}
</MenuList>
</Menu>
)}
</Flex>
</Box>

View File

@ -19,7 +19,7 @@ import { Input, InputGroup, InputLeftElement } from "@chakra-ui/input";
import { Search2Icon, ChevronDownIcon } from "@chakra-ui/icons";
import { FiPlay, FiMusic } from 'react-icons/fi';
import type { Song, PlaylistNode } from "../types/interfaces";
import { useState, useCallback, useMemo } from "react";
import { useState, useCallback, useMemo, useEffect } from "react";
import { formatDuration, formatTotalDuration } from '../utils/formatters';
@ -33,6 +33,7 @@ interface SongListProps {
currentPlaylist: string | null;
depth?: number;
onPlaySong?: (song: Song) => void;
isSwitchingPlaylist?: boolean;
}
export const SongList: React.FC<SongListProps> = ({
@ -44,11 +45,19 @@ export const SongList: React.FC<SongListProps> = ({
selectedSongId,
currentPlaylist,
depth = 0,
onPlaySong
onPlaySong,
isSwitchingPlaylist = false
}) => {
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState("");
// Clear selection when switching playlists
useEffect(() => {
if (isSwitchingPlaylist) {
setSelectedSongs(new Set());
}
}, [isSwitchingPlaylist]);
// Helper function to get all playlists (excluding folders) from the playlist tree
const getAllPlaylists = useCallback((nodes: PlaylistNode[]): PlaylistNode[] => {
let result: PlaylistNode[] = [];
@ -270,7 +279,7 @@ export const SongList: React.FC<SongListProps> = ({
fontSize={depth > 0 ? "xs" : "sm"}
color={selectedSongId === song.id ? "gray.300" : "gray.500"}
>
{song.artist} {formatDuration(song.totalTime)}
{song.artist} {formatDuration(song.totalTime)}{song.tonality ? ` - ${song.tonality}` : ''}
</Text>
{song.location && (
<Text

View File

@ -266,7 +266,7 @@ export function Configuration() {
{/* Song Matching Tab */}
<TabPanel bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
<VStack spacing={6} align="stretch">
<Heading size="md" color="white">Sync and Matching</Heading>
<Heading size="md" color="white">Sync and Matching ({storageProvider})</Heading>
<HStack spacing={3}>
<Button
leftIcon={<FiRefreshCw />}
@ -274,7 +274,7 @@ export function Configuration() {
variant="solid"
onClick={() => api.startStorageSync()}
>
Sync {storageProvider} (incremental)
Sync (incremental)
</Button>
<Button
leftIcon={<FiRefreshCw />}
@ -282,7 +282,7 @@ export function Configuration() {
variant="outline"
onClick={() => api.startStorageSync({ force: true })}
>
Force {storageProvider} Sync (rescan all)
Force Sync (rescan all)
</Button>
<Button
leftIcon={<FiTrash2 />}
@ -290,7 +290,7 @@ export function Configuration() {
variant="outline"
onClick={() => api.startStorageSync({ clearLinks: true, force: true })}
>
Clear Links + Force {storageProvider} Sync
Clear Links + Force Sync
</Button>
</HStack>
<SongMatching />