From 683050f271d749d4bf910cd4214ab7df838ae6c4 Mon Sep 17 00:00:00 2001 From: mbrucedogs Date: Wed, 23 Jul 2025 07:37:23 -0500 Subject: [PATCH] Signed-off-by: mbrucedogs --- src/components/common/SongCountDisplay.tsx | 40 +++++++++++++++ src/components/common/index.ts | 3 +- src/constants/index.ts | 2 +- src/features/SongLists/SongLists.tsx | 6 ++- src/hooks/useSongLists.ts | 58 ++++++++++++++++++---- src/utils/dataProcessing.ts | 37 ++++++++++++++ 6 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 src/components/common/SongCountDisplay.tsx diff --git a/src/components/common/SongCountDisplay.tsx b/src/components/common/SongCountDisplay.tsx new file mode 100644 index 0000000..ad0c870 --- /dev/null +++ b/src/components/common/SongCountDisplay.tsx @@ -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 = ({ + 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 ( + + {label} + + ); +}; + +export default SongCountDisplay; \ No newline at end of file diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 06c8136..66be473 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -11,4 +11,5 @@ export { TwoLineDisplay } from './TwoLineDisplay'; export { default as ListItem } from './ListItem'; export { NumberDisplay } from './NumberDisplay'; export { ModalHeader } from './ModalHeader'; -export { default as VirtualizedList } from './VirtualizedList'; \ No newline at end of file +export { default as VirtualizedList } from './VirtualizedList'; +export { default as SongCountDisplay } from './SongCountDisplay'; \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index 58811d9..ee690e3 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -33,7 +33,7 @@ export const UI_CONSTANTS = { MAX_ITEMS: 100, }, HISTORY: { - MAX_ITEMS: 50, + MAX_ITEMS: 250, // Increased from 50 to show more history items }, TOP_PLAYED: { MAX_ITEMS: 20, diff --git a/src/features/SongLists/SongLists.tsx b/src/features/SongLists/SongLists.tsx index d0ad67f..501a3e8 100644 --- a/src/features/SongLists/SongLists.tsx +++ b/src/features/SongLists/SongLists.tsx @@ -16,6 +16,7 @@ const SongLists: React.FC = () => { hasMore, loadMore, checkSongAvailability, + getSongCountForSongListSong, } = useSongLists(); const songListData = useAppSelector(selectSongList); @@ -94,7 +95,8 @@ const SongLists: React.FC = () => { {selectedListWithAvailability?.songs.map((songListSong: SongListSong & { availableSongs: Song[] }, index) => { 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}`; if (isAvailable) { @@ -114,7 +116,7 @@ const SongLists: React.FC = () => { onClick={() => handleSongItemClick(songKey)} endContent={ - {availableSongs.length} version{availableSongs.length !== 1 ? 's' : ''} + {songCount} version{songCount !== 1 ? 's' : ''} } /> diff --git a/src/hooks/useSongLists.ts b/src/hooks/useSongLists.ts index a9e0227..dcc86a9 100644 --- a/src/hooks/useSongLists.ts +++ b/src/hooks/useSongLists.ts @@ -1,33 +1,66 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useAppSelector, selectSongListArray, selectSongsArray } from '../redux'; import { useActions } from './useActions'; import { usePaginatedData } from './index'; -import type { SongListSong } from '../types'; +import type { SongListSong, Song } from '../types'; export const useSongLists = () => { const allSongLists = useAppSelector(selectSongListArray); const allSongs = useAppSelector(selectSongsArray); const { handleAddToQueue, handleToggleFavorite } = useActions(); + // Pre-compute songs by artist and title combination for performance + const songsByArtistTitle = useMemo(() => { + const songsMap = new Map(); + const countsMap = new Map(); + + 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 const pagination = usePaginatedData(allSongLists, { 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) => { if (songListSong.foundSongs && songListSong.foundSongs.length > 0) { return songListSong.foundSongs; } - // Search for songs by artist and title - const matchingSongs = allSongs.filter(song => - (song.artist || '').toLowerCase() === (songListSong.artist || '').toLowerCase() && - (song.title || '').toLowerCase() === (songListSong.title || '').toLowerCase() - ); - - return matchingSongs; - }, [allSongs]); + // Use the pre-computed data for better performance + return getSongsByArtistTitle(songListSong.artist, songListSong.title); + }, [getSongsByArtistTitle]); + + // Get song count for a song list song + const getSongCountForSongListSong = useCallback((songListSong: SongListSong) => { + return getSongCountByArtistTitle(songListSong.artist, songListSong.title); + }, [getSongCountByArtistTitle]); return { songLists: pagination.items, @@ -37,6 +70,9 @@ export const useSongLists = () => { currentPage: pagination.currentPage, totalPages: pagination.totalPages, checkSongAvailability, + getSongCountForSongListSong, + getSongsByArtistTitle, + getSongCountByArtistTitle, handleAddToQueue, handleToggleFavorite, isLoading: pagination.isLoading, diff --git a/src/utils/dataProcessing.ts b/src/utils/dataProcessing.ts index 48105c8..3dc0845 100644 --- a/src/utils/dataProcessing.ts +++ b/src/utils/dataProcessing.ts @@ -121,4 +121,41 @@ export const getQueueStats = (queue: Record) => { singers: [...new Set(queueArray.map(item => item.singer.name))], 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 => { + const countsMap = new Map(); + + 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, artist: string, title: string): number => { + const key = `${(artist || '').toLowerCase()}|${(title || '').toLowerCase()}`; + return countsMap.get(key) || 0; }; \ No newline at end of file