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,
|
Input,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputLeftElement,
|
InputLeftElement,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
MenuDivider,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { Search2Icon } from '@chakra-ui/icons';
|
import { Search2Icon, ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
import { FiPlay } from 'react-icons/fi';
|
import { FiPlay } from 'react-icons/fi';
|
||||||
import type { Song, PlaylistNode } from '../types/interfaces';
|
import type { Song, PlaylistNode } from '../types/interfaces';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
@ -131,7 +136,7 @@ const SongItem = memo<{
|
|||||||
</Box>
|
</Box>
|
||||||
<Box textAlign="right" ml={2}>
|
<Box textAlign="right" ml={2}>
|
||||||
<Text fontSize="xs" color="gray.500">
|
<Text fontSize="xs" color="gray.500">
|
||||||
{formattedDuration}
|
{formattedDuration}{song.tonality ? ` - ${song.tonality}` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="gray.600">
|
<Text fontSize="xs" color="gray.600">
|
||||||
{song.averageBpm} BPM
|
{song.averageBpm} BPM
|
||||||
@ -203,6 +208,13 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
onLoadMoreRef.current = onLoadMore;
|
onLoadMoreRef.current = onLoadMore;
|
||||||
}, [hasMore, loading, onLoadMore]);
|
}, [hasMore, loading, onLoadMore]);
|
||||||
|
|
||||||
|
// Clear selection when switching playlists
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSwitchingPlaylist) {
|
||||||
|
setSelectedSongs(new Set());
|
||||||
|
}
|
||||||
|
}, [isSwitchingPlaylist]);
|
||||||
|
|
||||||
// Debounce search to prevent excessive API calls
|
// Debounce search to prevent excessive API calls
|
||||||
const debouncedSearchQuery = useDebounce(localSearchQuery, 300);
|
const debouncedSearchQuery = useDebounce(localSearchQuery, 300);
|
||||||
|
|
||||||
@ -522,27 +534,32 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{selectedSongs.size > 0 && (
|
{selectedSongs.size > 0 && (
|
||||||
<HStack spacing={2}>
|
<Menu>
|
||||||
<Button
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
size="sm"
|
size="sm"
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
onClick={onPlaylistModalOpen}
|
rightIcon={<ChevronDownIcon />}
|
||||||
>
|
>
|
||||||
Add to Playlist...
|
Actions
|
||||||
</Button>
|
</MenuButton>
|
||||||
{currentPlaylist !== "All Songs" && onRemoveFromPlaylist && (
|
<MenuList>
|
||||||
<Button
|
<MenuItem onClick={onPlaylistModalOpen}>
|
||||||
size="sm"
|
Add to Playlist...
|
||||||
variant="outline"
|
</MenuItem>
|
||||||
colorScheme="red"
|
{currentPlaylist !== "All Songs" && onRemoveFromPlaylist && (
|
||||||
onClick={() => {
|
<>
|
||||||
handleBulkRemoveFromPlaylist();
|
<MenuDivider />
|
||||||
}}
|
<MenuItem
|
||||||
>
|
color="red.300"
|
||||||
Remove from {currentPlaylist}
|
onClick={handleBulkRemoveFromPlaylist}
|
||||||
</Button>
|
>
|
||||||
)}
|
Remove from {currentPlaylist}
|
||||||
</HStack>
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { Input, InputGroup, InputLeftElement } from "@chakra-ui/input";
|
|||||||
import { Search2Icon, ChevronDownIcon } from "@chakra-ui/icons";
|
import { Search2Icon, ChevronDownIcon } from "@chakra-ui/icons";
|
||||||
import { FiPlay, FiMusic } from 'react-icons/fi';
|
import { FiPlay, FiMusic } from 'react-icons/fi';
|
||||||
import type { Song, PlaylistNode } from "../types/interfaces";
|
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';
|
import { formatDuration, formatTotalDuration } from '../utils/formatters';
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ interface SongListProps {
|
|||||||
currentPlaylist: string | null;
|
currentPlaylist: string | null;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
onPlaySong?: (song: Song) => void;
|
onPlaySong?: (song: Song) => void;
|
||||||
|
isSwitchingPlaylist?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SongList: React.FC<SongListProps> = ({
|
export const SongList: React.FC<SongListProps> = ({
|
||||||
@ -44,11 +45,19 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
selectedSongId,
|
selectedSongId,
|
||||||
currentPlaylist,
|
currentPlaylist,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
onPlaySong
|
onPlaySong,
|
||||||
|
isSwitchingPlaylist = false
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
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
|
// Helper function to get all playlists (excluding folders) from the playlist tree
|
||||||
const getAllPlaylists = useCallback((nodes: PlaylistNode[]): PlaylistNode[] => {
|
const getAllPlaylists = useCallback((nodes: PlaylistNode[]): PlaylistNode[] => {
|
||||||
let result: PlaylistNode[] = [];
|
let result: PlaylistNode[] = [];
|
||||||
@ -270,7 +279,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} • {formatDuration(song.totalTime)}
|
{song.artist} • {formatDuration(song.totalTime)}{song.tonality ? ` - ${song.tonality}` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
{song.location && (
|
{song.location && (
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@ -266,7 +266,7 @@ export function Configuration() {
|
|||||||
{/* Song Matching Tab */}
|
{/* Song Matching Tab */}
|
||||||
<TabPanel bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
|
<TabPanel bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
|
||||||
<VStack spacing={6} align="stretch">
|
<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}>
|
<HStack spacing={3}>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<FiRefreshCw />}
|
leftIcon={<FiRefreshCw />}
|
||||||
@ -274,7 +274,7 @@ export function Configuration() {
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={() => api.startStorageSync()}
|
onClick={() => api.startStorageSync()}
|
||||||
>
|
>
|
||||||
Sync {storageProvider} (incremental)
|
Sync (incremental)
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<FiRefreshCw />}
|
leftIcon={<FiRefreshCw />}
|
||||||
@ -282,7 +282,7 @@ export function Configuration() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => api.startStorageSync({ force: true })}
|
onClick={() => api.startStorageSync({ force: true })}
|
||||||
>
|
>
|
||||||
Force {storageProvider} Sync (rescan all)
|
Force Sync (rescan all)
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<FiTrash2 />}
|
leftIcon={<FiTrash2 />}
|
||||||
@ -290,7 +290,7 @@ export function Configuration() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => api.startStorageSync({ clearLinks: true, force: true })}
|
onClick={() => api.startStorageSync({ clearLinks: true, force: true })}
|
||||||
>
|
>
|
||||||
Clear Links + Force {storageProvider} Sync
|
Clear Links + Force Sync
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
<SongMatching />
|
<SongMatching />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user