Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>

This commit is contained in:
mbrucedogs 2025-07-23 07:37:23 -05:00
parent 98e3633d31
commit 683050f271
6 changed files with 131 additions and 15 deletions

View File

@ -0,0 +1,40 @@
import React from 'react';
import { IonChip } from '@ionic/react';
import { getSongCountByArtistTitle } from '../../utils/dataProcessing';
import type { Song } from '../../types';
interface SongCountDisplayProps {
songs: Song[];
artist: string;
title: string;
showLabel?: boolean;
color?: 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger' | 'medium' | 'light' | 'dark';
}
export const SongCountDisplay: React.FC<SongCountDisplayProps> = ({
songs,
artist,
title,
showLabel = true,
color = 'primary'
}) => {
const count = getSongCountByArtistTitle(songs, artist, title);
if (count === 0) {
return null;
}
const label = showLabel
? `${count} version${count !== 1 ? 's' : ''}`
: count.toString();
return (
<IonChip
color={color}
>
{label}
</IonChip>
);
};
export default SongCountDisplay;

View File

@ -12,3 +12,4 @@ export { default as ListItem } from './ListItem';
export { NumberDisplay } from './NumberDisplay'; export { NumberDisplay } from './NumberDisplay';
export { ModalHeader } from './ModalHeader'; export { ModalHeader } from './ModalHeader';
export { default as VirtualizedList } from './VirtualizedList'; export { default as VirtualizedList } from './VirtualizedList';
export { default as SongCountDisplay } from './SongCountDisplay';

View File

@ -33,7 +33,7 @@ export const UI_CONSTANTS = {
MAX_ITEMS: 100, MAX_ITEMS: 100,
}, },
HISTORY: { HISTORY: {
MAX_ITEMS: 50, MAX_ITEMS: 250, // Increased from 50 to show more history items
}, },
TOP_PLAYED: { TOP_PLAYED: {
MAX_ITEMS: 20, MAX_ITEMS: 20,

View File

@ -16,6 +16,7 @@ const SongLists: React.FC = () => {
hasMore, hasMore,
loadMore, loadMore,
checkSongAvailability, checkSongAvailability,
getSongCountForSongListSong,
} = useSongLists(); } = useSongLists();
const songListData = useAppSelector(selectSongList); const songListData = useAppSelector(selectSongList);
@ -94,7 +95,8 @@ const SongLists: React.FC = () => {
<IonAccordionGroup value={expandedSongKey}> <IonAccordionGroup value={expandedSongKey}>
{selectedListWithAvailability?.songs.map((songListSong: SongListSong & { availableSongs: Song[] }, index) => { {selectedListWithAvailability?.songs.map((songListSong: SongListSong & { availableSongs: Song[] }, index) => {
const availableSongs = songListSong.availableSongs; const availableSongs = songListSong.availableSongs;
const isAvailable = availableSongs.length > 0; const songCount = getSongCountForSongListSong(songListSong);
const isAvailable = songCount > 0;
const songKey = songListSong.key || `${songListSong.title}-${songListSong.position}-${index}`; const songKey = songListSong.key || `${songListSong.title}-${songListSong.position}-${index}`;
if (isAvailable) { if (isAvailable) {
@ -114,7 +116,7 @@ const SongLists: React.FC = () => {
onClick={() => handleSongItemClick(songKey)} onClick={() => handleSongItemClick(songKey)}
endContent={ endContent={
<IonChip color="primary"> <IonChip color="primary">
{availableSongs.length} version{availableSongs.length !== 1 ? 's' : ''} {songCount} version{songCount !== 1 ? 's' : ''}
</IonChip> </IonChip>
} }
/> />

View File

@ -1,33 +1,66 @@
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { useAppSelector, selectSongListArray, selectSongsArray } from '../redux'; import { useAppSelector, selectSongListArray, selectSongsArray } from '../redux';
import { useActions } from './useActions'; import { useActions } from './useActions';
import { usePaginatedData } from './index'; import { usePaginatedData } from './index';
import type { SongListSong } from '../types'; import type { SongListSong, Song } from '../types';
export const useSongLists = () => { export const useSongLists = () => {
const allSongLists = useAppSelector(selectSongListArray); const allSongLists = useAppSelector(selectSongListArray);
const allSongs = useAppSelector(selectSongsArray); const allSongs = useAppSelector(selectSongsArray);
const { handleAddToQueue, handleToggleFavorite } = useActions(); const { handleAddToQueue, handleToggleFavorite } = useActions();
// Pre-compute songs by artist and title combination for performance
const songsByArtistTitle = useMemo(() => {
const songsMap = new Map<string, Song[]>();
const countsMap = new Map<string, number>();
allSongs.forEach(song => {
const artist = (song.artist || '').toLowerCase();
const title = (song.title || '').toLowerCase();
const key = `${artist}|${title}`;
if (!songsMap.has(key)) {
songsMap.set(key, []);
countsMap.set(key, 0);
}
songsMap.get(key)!.push(song);
countsMap.set(key, countsMap.get(key)! + 1);
});
return { songsMap, countsMap };
}, [allSongs]);
// Use the composable pagination hook // Use the composable pagination hook
const pagination = usePaginatedData(allSongLists, { const pagination = usePaginatedData(allSongLists, {
itemsPerPage: 20 // Default pagination size itemsPerPage: 20 // Default pagination size
}); });
// Check if a song exists in the catalog // Get songs by artist and title (now using cached data)
const getSongsByArtistTitle = useCallback((artist: string, title: string) => {
const key = `${(artist || '').toLowerCase()}|${(title || '').toLowerCase()}`;
return songsByArtistTitle.songsMap.get(key) || [];
}, [songsByArtistTitle.songsMap]);
// Get song count by artist and title (now using cached data)
const getSongCountByArtistTitle = useCallback((artist: string, title: string) => {
const key = `${(artist || '').toLowerCase()}|${(title || '').toLowerCase()}`;
return songsByArtistTitle.countsMap.get(key) || 0;
}, [songsByArtistTitle.countsMap]);
// Check if a song exists in the catalog (enhanced version)
const checkSongAvailability = useCallback((songListSong: SongListSong) => { const checkSongAvailability = useCallback((songListSong: SongListSong) => {
if (songListSong.foundSongs && songListSong.foundSongs.length > 0) { if (songListSong.foundSongs && songListSong.foundSongs.length > 0) {
return songListSong.foundSongs; return songListSong.foundSongs;
} }
// Search for songs by artist and title // Use the pre-computed data for better performance
const matchingSongs = allSongs.filter(song => return getSongsByArtistTitle(songListSong.artist, songListSong.title);
(song.artist || '').toLowerCase() === (songListSong.artist || '').toLowerCase() && }, [getSongsByArtistTitle]);
(song.title || '').toLowerCase() === (songListSong.title || '').toLowerCase()
);
return matchingSongs; // Get song count for a song list song
}, [allSongs]); const getSongCountForSongListSong = useCallback((songListSong: SongListSong) => {
return getSongCountByArtistTitle(songListSong.artist, songListSong.title);
}, [getSongCountByArtistTitle]);
return { return {
songLists: pagination.items, songLists: pagination.items,
@ -37,6 +70,9 @@ export const useSongLists = () => {
currentPage: pagination.currentPage, currentPage: pagination.currentPage,
totalPages: pagination.totalPages, totalPages: pagination.totalPages,
checkSongAvailability, checkSongAvailability,
getSongCountForSongListSong,
getSongsByArtistTitle,
getSongCountByArtistTitle,
handleAddToQueue, handleAddToQueue,
handleToggleFavorite, handleToggleFavorite,
isLoading: pagination.isLoading, isLoading: pagination.isLoading,

View File

@ -122,3 +122,40 @@ export const getQueueStats = (queue: Record<string, QueueItem>) => {
estimatedDuration: queueArray.length * 3, // Rough estimate: 3 minutes per song estimatedDuration: queueArray.length * 3, // Rough estimate: 3 minutes per song
}; };
}; };
// Get songs by artist and title combination
export const getSongsByArtistTitle = (songs: Song[], artist: string, title: string): Song[] => {
const artistLower = (artist || '').toLowerCase();
const titleLower = (title || '').toLowerCase();
return songs.filter(song =>
(song.artist || '').toLowerCase() === artistLower &&
(song.title || '').toLowerCase() === titleLower
);
};
// Get song count by artist and title combination
export const getSongCountByArtistTitle = (songs: Song[], artist: string, title: string): number => {
return getSongsByArtistTitle(songs, artist, title).length;
};
// Create a map of song counts by artist and title for performance
export const createSongCountMapByArtistTitle = (songs: Song[]): Map<string, number> => {
const countsMap = new Map<string, number>();
songs.forEach(song => {
const artist = (song.artist || '').toLowerCase();
const title = (song.title || '').toLowerCase();
const key = `${artist}|${title}`;
countsMap.set(key, (countsMap.get(key) || 0) + 1);
});
return countsMap;
};
// Get song count using a pre-computed map
export const getSongCountFromMap = (countsMap: Map<string, number>, artist: string, title: string): number => {
const key = `${(artist || '').toLowerCase()}|${(title || '').toLowerCase()}`;
return countsMap.get(key) || 0;
};