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:
parent
e58d42bea2
commit
aa04849442
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user