Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
50eba457f6
commit
3aee1fc44e
35
src/App.tsx
35
src/App.tsx
@ -5,6 +5,7 @@ import TopPlayed from './features/TopPlayed/Top100';
|
|||||||
import { FirebaseProvider } from './firebase/FirebaseProvider';
|
import { FirebaseProvider } from './firebase/FirebaseProvider';
|
||||||
import { ErrorBoundary } from './components/common';
|
import { ErrorBoundary } from './components/common';
|
||||||
import { AuthInitializer } from './components/Auth';
|
import { AuthInitializer } from './components/Auth';
|
||||||
|
import { SongInfoProvider } from './components/common/SongInfoProvider';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -12,22 +13,24 @@ function App() {
|
|||||||
<FirebaseProvider>
|
<FirebaseProvider>
|
||||||
<Router>
|
<Router>
|
||||||
<AuthInitializer>
|
<AuthInitializer>
|
||||||
<Layout>
|
<SongInfoProvider>
|
||||||
<Routes>
|
<Layout>
|
||||||
<Route path="/" element={<Navigate to="/queue" replace />} />
|
<Routes>
|
||||||
<Route path="/search" element={<Search />} />
|
<Route path="/" element={<Navigate to="/queue" replace />} />
|
||||||
<Route path="/queue" element={<Queue />} />
|
<Route path="/search" element={<Search />} />
|
||||||
<Route path="/favorites" element={<Favorites />} />
|
<Route path="/queue" element={<Queue />} />
|
||||||
<Route path="/new-songs" element={<NewSongs />} />
|
<Route path="/favorites" element={<Favorites />} />
|
||||||
<Route path="/artists" element={<Artists />} />
|
<Route path="/new-songs" element={<NewSongs />} />
|
||||||
<Route path="/song-lists" element={<SongLists />} />
|
<Route path="/artists" element={<Artists />} />
|
||||||
<Route path="/history" element={<History />} />
|
<Route path="/song-lists" element={<SongLists />} />
|
||||||
<Route path="/top-played" element={<TopPlayed />} />
|
<Route path="/history" element={<History />} />
|
||||||
<Route path="/singers" element={<Singers />} />
|
<Route path="/top-played" element={<TopPlayed />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/singers" element={<Singers />} />
|
||||||
<Route path="*" element={<Navigate to="/queue" replace />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
</Routes>
|
<Route path="*" element={<Navigate to="/queue" replace />} />
|
||||||
</Layout>
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
</SongInfoProvider>
|
||||||
</AuthInitializer>
|
</AuthInitializer>
|
||||||
</Router>
|
</Router>
|
||||||
</FirebaseProvider>
|
</FirebaseProvider>
|
||||||
|
|||||||
48
src/components/common/SongInfoProvider.tsx
Normal file
48
src/components/common/SongInfoProvider.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import SongInfo from './SongInfo';
|
||||||
|
import { SongInfoContext, type SongInfoContextType } from '../../contexts/SongInfoContext';
|
||||||
|
import type { Song } from '../../types';
|
||||||
|
|
||||||
|
interface SongInfoProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SongInfoProvider: React.FC<SongInfoProviderProps> = ({ children }) => {
|
||||||
|
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const openSongInfo = useCallback((song: Song) => {
|
||||||
|
setSelectedSong(song);
|
||||||
|
setIsOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeSongInfo = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectedSong(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue: SongInfoContextType = {
|
||||||
|
openSongInfo,
|
||||||
|
closeSongInfo,
|
||||||
|
isOpen,
|
||||||
|
selectedSong,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SongInfoContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* Song Info Modal */}
|
||||||
|
{selectedSong && (
|
||||||
|
<SongInfo
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={closeSongInfo}
|
||||||
|
song={selectedSong}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SongInfoContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -3,6 +3,9 @@ import { IonItem, IonLabel } from '@ionic/react';
|
|||||||
import ActionButton from './ActionButton';
|
import ActionButton from './ActionButton';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectQueue, selectFavorites } from '../../redux';
|
import { selectQueue, selectFavorites } from '../../redux';
|
||||||
|
import { useSongOperations } from '../../hooks/useSongOperations';
|
||||||
|
import { useToast } from '../../hooks/useToast';
|
||||||
|
import { useSongInfo } from '../../hooks/useSongInfoContext';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
import type { SongItemProps, QueueItem, Song } from '../../types';
|
import type { SongItemProps, QueueItem, Song } from '../../types';
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
@ -92,11 +95,11 @@ export const SongActionButtons: React.FC<{
|
|||||||
showRemoveButton?: boolean;
|
showRemoveButton?: boolean;
|
||||||
showDeleteButton?: boolean;
|
showDeleteButton?: boolean;
|
||||||
showFavoriteButton?: boolean;
|
showFavoriteButton?: boolean;
|
||||||
|
onDeleteItem?: () => void;
|
||||||
onAddToQueue?: () => void;
|
onAddToQueue?: () => void;
|
||||||
onRemoveFromQueue?: () => void;
|
onRemoveFromQueue?: () => void;
|
||||||
onToggleFavorite?: () => void;
|
onToggleFavorite?: () => void;
|
||||||
onDeleteItem?: () => void;
|
onShowSongInfo?: () => void;
|
||||||
onSelectSinger?: () => void;
|
|
||||||
}> = ({
|
}> = ({
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isInQueue,
|
isInQueue,
|
||||||
@ -106,20 +109,20 @@ export const SongActionButtons: React.FC<{
|
|||||||
showRemoveButton = false,
|
showRemoveButton = false,
|
||||||
showDeleteButton = false,
|
showDeleteButton = false,
|
||||||
showFavoriteButton = false,
|
showFavoriteButton = false,
|
||||||
|
onDeleteItem,
|
||||||
onAddToQueue,
|
onAddToQueue,
|
||||||
onRemoveFromQueue,
|
onRemoveFromQueue,
|
||||||
onToggleFavorite,
|
onToggleFavorite,
|
||||||
onDeleteItem,
|
onShowSongInfo
|
||||||
onSelectSinger
|
|
||||||
}) => {
|
}) => {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
// Info button
|
// Info button
|
||||||
if (showInfoButton && onSelectSinger) {
|
if (showInfoButton && onShowSongInfo) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<ActionButton
|
<ActionButton
|
||||||
key="info"
|
key="info"
|
||||||
onClick={onSelectSinger}
|
onClick={onShowSongInfo}
|
||||||
variant={ActionButtonVariant.SECONDARY}
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
size={ActionButtonSize.SMALL}
|
size={ActionButtonSize.SMALL}
|
||||||
icon={Icons.INFORMATION_CIRCLE}
|
icon={Icons.INFORMATION_CIRCLE}
|
||||||
@ -129,11 +132,11 @@ export const SongActionButtons: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add to Queue button
|
// Add to Queue button
|
||||||
if (showAddButton && !isInQueue) {
|
if (showAddButton && !isInQueue && onAddToQueue) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<ActionButton
|
<ActionButton
|
||||||
key="add"
|
key="add"
|
||||||
onClick={onAddToQueue || (() => {})}
|
onClick={onAddToQueue}
|
||||||
variant={ActionButtonVariant.PRIMARY}
|
variant={ActionButtonVariant.PRIMARY}
|
||||||
size={ActionButtonSize.SMALL}
|
size={ActionButtonSize.SMALL}
|
||||||
icon={Icons.ADD}
|
icon={Icons.ADD}
|
||||||
@ -171,11 +174,11 @@ export const SongActionButtons: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Toggle Favorite button
|
// Toggle Favorite button
|
||||||
if (showFavoriteButton) {
|
if (showFavoriteButton && onToggleFavorite) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<ActionButton
|
<ActionButton
|
||||||
key="favorite"
|
key="favorite"
|
||||||
onClick={onToggleFavorite || (() => {})}
|
onClick={onToggleFavorite}
|
||||||
variant={isInFavorites ? ActionButtonVariant.DANGER : ActionButtonVariant.SECONDARY}
|
variant={isInFavorites ? ActionButtonVariant.DANGER : ActionButtonVariant.SECONDARY}
|
||||||
size={ActionButtonSize.SMALL}
|
size={ActionButtonSize.SMALL}
|
||||||
icon={isInFavorites ? Icons.HEART : Icons.HEART_OUTLINE}
|
icon={isInFavorites ? Icons.HEART : Icons.HEART_OUTLINE}
|
||||||
@ -195,11 +198,7 @@ export const SongActionButtons: React.FC<{
|
|||||||
const SongItem: React.FC<SongItemProps> = ({
|
const SongItem: React.FC<SongItemProps> = ({
|
||||||
song,
|
song,
|
||||||
context,
|
context,
|
||||||
onAddToQueue,
|
|
||||||
onRemoveFromQueue,
|
|
||||||
onToggleFavorite,
|
|
||||||
onDeleteItem,
|
onDeleteItem,
|
||||||
onSelectSinger,
|
|
||||||
isAdmin = false,
|
isAdmin = false,
|
||||||
className = '',
|
className = '',
|
||||||
showActions = true,
|
showActions = true,
|
||||||
@ -215,10 +214,20 @@ const SongItem: React.FC<SongItemProps> = ({
|
|||||||
const queue = useAppSelector(selectQueue);
|
const queue = useAppSelector(selectQueue);
|
||||||
const favorites = useAppSelector(selectFavorites);
|
const favorites = useAppSelector(selectFavorites);
|
||||||
|
|
||||||
|
// Get song operations and hooks
|
||||||
|
const { addToQueue, removeFromQueue, toggleFavorite } = useSongOperations();
|
||||||
|
const { showSuccess, showError } = useToast();
|
||||||
|
const { openSongInfo } = useSongInfo();
|
||||||
|
|
||||||
// Check if song is in queue or favorites based on path
|
// Check if song is in queue or favorites based on path
|
||||||
const isInQueue = (Object.values(queue) as QueueItem[]).some(item => item.song.path === song.path);
|
const isInQueue = (Object.values(queue) as QueueItem[]).some(item => item.song.path === song.path);
|
||||||
const isInFavorites = (Object.values(favorites) as Song[]).some(favSong => favSong.path === song.path);
|
const isInFavorites = (Object.values(favorites) as Song[]).some(favSong => favSong.path === song.path);
|
||||||
|
|
||||||
|
// Find queue item key for removal (only needed for queue context)
|
||||||
|
const queueItemKey = context === 'queue'
|
||||||
|
? (Object.entries(queue) as [string, QueueItem][]).find(([, item]) => item.song.path === song.path)?.[0]
|
||||||
|
: null;
|
||||||
|
|
||||||
// Debug logging for favorites
|
// Debug logging for favorites
|
||||||
debugLog('SongItem render:', {
|
debugLog('SongItem render:', {
|
||||||
songTitle: song.title,
|
songTitle: song.title,
|
||||||
@ -239,6 +248,40 @@ const SongItem: React.FC<SongItemProps> = ({
|
|||||||
const shouldShowDeleteButton = showDeleteButton !== undefined ? showDeleteButton : context === 'history' && isAdmin;
|
const shouldShowDeleteButton = showDeleteButton !== undefined ? showDeleteButton : context === 'history' && isAdmin;
|
||||||
const shouldShowFavoriteButton = showFavoriteButton !== undefined ? showFavoriteButton : false; // Disabled for all contexts
|
const shouldShowFavoriteButton = showFavoriteButton !== undefined ? showFavoriteButton : false; // Disabled for all contexts
|
||||||
|
|
||||||
|
// Handle song operations internally
|
||||||
|
const handleAddToQueue = async () => {
|
||||||
|
try {
|
||||||
|
await addToQueue(song);
|
||||||
|
showSuccess('Song added to queue');
|
||||||
|
} catch {
|
||||||
|
showError('Failed to add song to queue');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromQueue = async () => {
|
||||||
|
if (!queueItemKey) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeFromQueue(queueItemKey);
|
||||||
|
showSuccess('Song removed from queue');
|
||||||
|
} catch {
|
||||||
|
showError('Failed to remove song from queue');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleFavorite = async () => {
|
||||||
|
try {
|
||||||
|
await toggleFavorite(song);
|
||||||
|
showSuccess(isInFavorites ? 'Removed from favorites' : 'Added to favorites');
|
||||||
|
} catch {
|
||||||
|
showError('Failed to update favorites');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectSinger = () => {
|
||||||
|
openSongInfo(song);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IonItem className={className}>
|
<IonItem className={className}>
|
||||||
<SongInfoDisplay
|
<SongInfoDisplay
|
||||||
@ -258,11 +301,11 @@ const SongItem: React.FC<SongItemProps> = ({
|
|||||||
showRemoveButton={shouldShowRemoveButton}
|
showRemoveButton={shouldShowRemoveButton}
|
||||||
showDeleteButton={shouldShowDeleteButton}
|
showDeleteButton={shouldShowDeleteButton}
|
||||||
showFavoriteButton={shouldShowFavoriteButton}
|
showFavoriteButton={shouldShowFavoriteButton}
|
||||||
onAddToQueue={onAddToQueue}
|
|
||||||
onRemoveFromQueue={onRemoveFromQueue}
|
|
||||||
onToggleFavorite={onToggleFavorite}
|
|
||||||
onDeleteItem={onDeleteItem}
|
onDeleteItem={onDeleteItem}
|
||||||
onSelectSinger={onSelectSinger}
|
onAddToQueue={context === 'queue' ? handleRemoveFromQueue : handleAddToQueue}
|
||||||
|
onRemoveFromQueue={context === 'queue' ? handleRemoveFromQueue : onDeleteItem}
|
||||||
|
onToggleFavorite={context === 'favorites' ? onDeleteItem : handleToggleFavorite}
|
||||||
|
onShowSongInfo={handleSelectSinger}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
11
src/contexts/SongInfoContext.tsx
Normal file
11
src/contexts/SongInfoContext.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import type { Song } from '../types';
|
||||||
|
|
||||||
|
export interface SongInfoContextType {
|
||||||
|
openSongInfo: (song: Song) => void;
|
||||||
|
closeSongInfo: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
selectedSong: Song | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SongInfoContext = createContext<SongInfoContextType | null>(null);
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { IonSearchbar, IonModal, IonHeader, IonToolbar, IonTitle, IonContent, IonItem } from '@ionic/react';
|
import { IonSearchbar, IonModal, IonHeader, IonToolbar, IonTitle, IonContent, IonItem } from '@ionic/react';
|
||||||
import { list } from 'ionicons/icons';
|
import { list } from 'ionicons/icons';
|
||||||
import { InfiniteScrollList, SongItem, ListItem, NumberDisplay, SongInfo, ActionButton } from '../../components/common';
|
import { InfiniteScrollList, SongItem, ListItem, NumberDisplay, ActionButton } from '../../components/common';
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
import { Icons } from '../../constants';
|
import { Icons } from '../../constants';
|
||||||
import { useArtists } from '../../hooks';
|
import { useArtists } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectSongs } from '../../redux';
|
import { selectSongs } from '../../redux';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
import type { Song } from '../../types';
|
|
||||||
|
|
||||||
const Artists: React.FC = () => {
|
const Artists: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@ -19,15 +19,11 @@ const Artists: React.FC = () => {
|
|||||||
handleSearchChange,
|
handleSearchChange,
|
||||||
getSongsByArtist,
|
getSongsByArtist,
|
||||||
getSongCountByArtist,
|
getSongCountByArtist,
|
||||||
handleAddToQueue,
|
|
||||||
} = useArtists();
|
} = useArtists();
|
||||||
|
|
||||||
const songs = useAppSelector(selectSongs);
|
const songs = useAppSelector(selectSongs);
|
||||||
const songsCount = Object.keys(songs).length;
|
const songsCount = Object.keys(songs).length;
|
||||||
const [selectedArtist, setSelectedArtist] = useState<string | null>(null);
|
const [selectedArtist, setSelectedArtist] = useState<string | null>(null);
|
||||||
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
|
||||||
const [isSongInfoOpen, setIsSongInfoOpen] = useState(false);
|
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
debugLog('Artists component - artists count:', artists.length);
|
debugLog('Artists component - artists count:', artists.length);
|
||||||
debugLog('Artists component - selected artist:', selectedArtist);
|
debugLog('Artists component - selected artist:', selectedArtist);
|
||||||
@ -42,16 +38,6 @@ const Artists: React.FC = () => {
|
|||||||
setSelectedArtist(null);
|
setSelectedArtist(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSongInfo = (song: Song) => {
|
|
||||||
setSelectedSong(song);
|
|
||||||
setIsSongInfoOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseSongInfo = () => {
|
|
||||||
setIsSongInfoOpen(false);
|
|
||||||
setSelectedSong(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : [];
|
const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : [];
|
||||||
|
|
||||||
// Render artist item for InfiniteScrollList
|
// Render artist item for InfiniteScrollList
|
||||||
@ -156,8 +142,6 @@ const Artists: React.FC = () => {
|
|||||||
<SongItem
|
<SongItem
|
||||||
song={song}
|
song={song}
|
||||||
context="search"
|
context="search"
|
||||||
onAddToQueue={() => handleAddToQueue(song)}
|
|
||||||
onSelectSinger={() => handleSongInfo(song)}
|
|
||||||
showAddButton={true}
|
showAddButton={true}
|
||||||
showInfoButton={true}
|
showInfoButton={true}
|
||||||
showFavoriteButton={false}
|
showFavoriteButton={false}
|
||||||
@ -173,14 +157,6 @@ const Artists: React.FC = () => {
|
|||||||
</IonContent>
|
</IonContent>
|
||||||
</IonModal>
|
</IonModal>
|
||||||
|
|
||||||
{/* Song Info Modal */}
|
|
||||||
{selectedSong && (
|
|
||||||
<SongInfo
|
|
||||||
isOpen={isSongInfoOpen}
|
|
||||||
onClose={handleCloseSongInfo}
|
|
||||||
song={selectedSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { InfiniteScrollList, SongItem, SongInfo } from '../../components/common';
|
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||||
import { useFavorites } from '../../hooks';
|
import { useFavorites } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectFavorites } from '../../redux';
|
import { selectFavorites } from '../../redux';
|
||||||
@ -14,9 +14,6 @@ const Favorites: React.FC = () => {
|
|||||||
handleToggleFavorite,
|
handleToggleFavorite,
|
||||||
} = useFavorites();
|
} = useFavorites();
|
||||||
|
|
||||||
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
|
||||||
const [isSongInfoOpen, setIsSongInfoOpen] = useState(false);
|
|
||||||
|
|
||||||
const favorites = useAppSelector(selectFavorites);
|
const favorites = useAppSelector(selectFavorites);
|
||||||
const favoritesCount = Object.keys(favorites).length;
|
const favoritesCount = Object.keys(favorites).length;
|
||||||
|
|
||||||
@ -24,16 +21,6 @@ const Favorites: React.FC = () => {
|
|||||||
debugLog('Favorites component - favorites count:', favoritesCount);
|
debugLog('Favorites component - favorites count:', favoritesCount);
|
||||||
debugLog('Favorites component - favorites items:', favoritesItems);
|
debugLog('Favorites component - favorites items:', favoritesItems);
|
||||||
|
|
||||||
const handleSongInfo = (song: Song) => {
|
|
||||||
setSelectedSong(song);
|
|
||||||
setIsSongInfoOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseSongInfo = () => {
|
|
||||||
setIsSongInfoOpen(false);
|
|
||||||
setSelectedSong(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InfiniteScrollList<Song>
|
<InfiniteScrollList<Song>
|
||||||
@ -48,7 +35,6 @@ const Favorites: React.FC = () => {
|
|||||||
showInfoButton={true}
|
showInfoButton={true}
|
||||||
showAddButton={false}
|
showAddButton={false}
|
||||||
showDeleteButton={true}
|
showDeleteButton={true}
|
||||||
onSelectSinger={() => handleSongInfo(song)}
|
|
||||||
onDeleteItem={() => handleToggleFavorite(song)}
|
onDeleteItem={() => handleToggleFavorite(song)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -58,14 +44,6 @@ const Favorites: React.FC = () => {
|
|||||||
loadingMessage="Please wait while favorites data is being loaded"
|
loadingMessage="Please wait while favorites data is being loaded"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Song Info Modal */}
|
|
||||||
{selectedSong && (
|
|
||||||
<SongInfo
|
|
||||||
isOpen={isSongInfoOpen}
|
|
||||||
onClose={handleCloseSongInfo}
|
|
||||||
song={selectedSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonChip, IonIcon } from '@ionic/react';
|
import { IonChip, IonIcon } from '@ionic/react';
|
||||||
import { time } from 'ionicons/icons';
|
import { time } from 'ionicons/icons';
|
||||||
import { InfiniteScrollList, SongItem, SongInfo } from '../../components/common';
|
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||||
import { useHistory } from '../../hooks';
|
import { useHistory } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectHistory, selectIsAdmin } from '../../redux';
|
import { selectHistory, selectIsAdmin } from '../../redux';
|
||||||
@ -14,30 +14,16 @@ const History: React.FC = () => {
|
|||||||
historyItems,
|
historyItems,
|
||||||
hasMore,
|
hasMore,
|
||||||
loadMore,
|
loadMore,
|
||||||
handleAddToQueue,
|
|
||||||
handleDeleteFromHistory,
|
handleDeleteFromHistory,
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
|
||||||
const history = useAppSelector(selectHistory);
|
const history = useAppSelector(selectHistory);
|
||||||
const isAdmin = useAppSelector(selectIsAdmin);
|
const isAdmin = useAppSelector(selectIsAdmin);
|
||||||
const historyCount = Object.keys(history).length;
|
const historyCount = Object.keys(history).length;
|
||||||
const [selectedSong, setSelectedSong] = React.useState<Song | null>(null);
|
|
||||||
const [isSongInfoOpen, setIsSongInfoOpen] = React.useState(false);
|
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
debugLog('History component - history count:', historyCount);
|
debugLog('History component - history count:', historyCount);
|
||||||
debugLog('History component - history items:', historyItems);
|
debugLog('History component - history items:', historyItems);
|
||||||
|
|
||||||
const handleSongInfo = (song: Song) => {
|
|
||||||
setSelectedSong(song);
|
|
||||||
setIsSongInfoOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseSongInfo = () => {
|
|
||||||
setIsSongInfoOpen(false);
|
|
||||||
setSelectedSong(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render extra content for history items (play date)
|
// Render extra content for history items (play date)
|
||||||
const renderExtraContent = (item: Song) => {
|
const renderExtraContent = (item: Song) => {
|
||||||
if (item.date) {
|
if (item.date) {
|
||||||
@ -64,8 +50,6 @@ const History: React.FC = () => {
|
|||||||
<SongItem
|
<SongItem
|
||||||
song={song}
|
song={song}
|
||||||
context="history"
|
context="history"
|
||||||
onAddToQueue={() => handleAddToQueue(song)}
|
|
||||||
onSelectSinger={() => handleSongInfo(song)}
|
|
||||||
onDeleteItem={() => handleDeleteFromHistory(song)}
|
onDeleteItem={() => handleDeleteFromHistory(song)}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
showAddButton={true}
|
showAddButton={true}
|
||||||
@ -83,14 +67,6 @@ const History: React.FC = () => {
|
|||||||
loadingMessage="Please wait while history data is being loaded"
|
loadingMessage="Please wait while history data is being loaded"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Song Info Modal */}
|
|
||||||
{selectedSong && (
|
|
||||||
<SongInfo
|
|
||||||
isOpen={isSongInfoOpen}
|
|
||||||
onClose={handleCloseSongInfo}
|
|
||||||
song={selectedSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { InfiniteScrollList, SongItem, SongInfo } from '../../components/common';
|
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||||
import { useNewSongs } from '../../hooks';
|
import { useNewSongs } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectNewSongsArray } from '../../redux';
|
import { selectNewSongsArray } from '../../redux';
|
||||||
@ -11,28 +11,15 @@ const NewSongs: React.FC = () => {
|
|||||||
newSongsItems,
|
newSongsItems,
|
||||||
hasMore,
|
hasMore,
|
||||||
loadMore,
|
loadMore,
|
||||||
handleAddToQueue,
|
|
||||||
} = useNewSongs();
|
} = useNewSongs();
|
||||||
|
|
||||||
const newSongsArray = useAppSelector(selectNewSongsArray);
|
const newSongsArray = useAppSelector(selectNewSongsArray);
|
||||||
const newSongsCount = newSongsArray.length;
|
const newSongsCount = newSongsArray.length;
|
||||||
const [selectedSong, setSelectedSong] = React.useState<Song | null>(null);
|
|
||||||
const [isSongInfoOpen, setIsSongInfoOpen] = React.useState(false);
|
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
debugLog('NewSongs component - new songs count:', newSongsCount);
|
debugLog('NewSongs component - new songs count:', newSongsCount);
|
||||||
debugLog('NewSongs component - new songs items:', newSongsItems);
|
debugLog('NewSongs component - new songs items:', newSongsItems);
|
||||||
|
|
||||||
const handleSongInfo = (song: Song) => {
|
|
||||||
setSelectedSong(song);
|
|
||||||
setIsSongInfoOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseSongInfo = () => {
|
|
||||||
setIsSongInfoOpen(false);
|
|
||||||
setSelectedSong(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InfiniteScrollList<Song>
|
<InfiniteScrollList<Song>
|
||||||
@ -44,8 +31,6 @@ const NewSongs: React.FC = () => {
|
|||||||
<SongItem
|
<SongItem
|
||||||
song={song}
|
song={song}
|
||||||
context="search"
|
context="search"
|
||||||
onAddToQueue={() => handleAddToQueue(song)}
|
|
||||||
onSelectSinger={() => handleSongInfo(song)}
|
|
||||||
showAddButton={true}
|
showAddButton={true}
|
||||||
showInfoButton={true}
|
showInfoButton={true}
|
||||||
showFavoriteButton={false}
|
showFavoriteButton={false}
|
||||||
@ -57,14 +42,6 @@ const NewSongs: React.FC = () => {
|
|||||||
loadingMessage="Please wait while new songs data is being loaded"
|
loadingMessage="Please wait while new songs data is being loaded"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Song Info Modal */}
|
|
||||||
{selectedSong && (
|
|
||||||
<SongInfo
|
|
||||||
isOpen={isSongInfoOpen}
|
|
||||||
onClose={handleCloseSongInfo}
|
|
||||||
song={selectedSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonSearchbar } from '@ionic/react';
|
import { IonSearchbar } from '@ionic/react';
|
||||||
import { InfiniteScrollList, SongItem, SongInfo } from '../../components/common';
|
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||||
import { useSearch, useSongInfo } from '../../hooks';
|
import { useSearch } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectIsAdmin, selectSongs } from '../../redux';
|
import { selectIsAdmin, selectSongs } from '../../redux';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
@ -12,12 +12,9 @@ const Search: React.FC = () => {
|
|||||||
searchTerm,
|
searchTerm,
|
||||||
searchResults,
|
searchResults,
|
||||||
handleSearchChange,
|
handleSearchChange,
|
||||||
handleAddToQueue,
|
|
||||||
loadMore,
|
loadMore,
|
||||||
} = useSearch();
|
} = useSearch();
|
||||||
|
|
||||||
const { isOpen, selectedSong, openSongInfo, closeSongInfo } = useSongInfo();
|
|
||||||
|
|
||||||
const isAdmin = useAppSelector(selectIsAdmin);
|
const isAdmin = useAppSelector(selectIsAdmin);
|
||||||
const songs = useAppSelector(selectSongs);
|
const songs = useAppSelector(selectSongs);
|
||||||
const songsCount = Object.keys(songs).length;
|
const songsCount = Object.keys(songs).length;
|
||||||
@ -63,8 +60,6 @@ const Search: React.FC = () => {
|
|||||||
<SongItem
|
<SongItem
|
||||||
song={song}
|
song={song}
|
||||||
context="search"
|
context="search"
|
||||||
onAddToQueue={() => handleAddToQueue(song)}
|
|
||||||
onSelectSinger={() => openSongInfo(song)}
|
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
showAddButton={true}
|
showAddButton={true}
|
||||||
showInfoButton={true}
|
showInfoButton={true}
|
||||||
@ -86,14 +81,6 @@ const Search: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Song Info Modal */}
|
|
||||||
{selectedSong && (
|
|
||||||
<SongInfo
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeSongInfo}
|
|
||||||
song={selectedSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import { IonItem, IonModal, IonHeader, IonToolbar, IonTitle, IonChip, IonContent, IonList, IonAccordionGroup, IonAccordion } from '@ionic/react';
|
import { IonItem, IonModal, IonHeader, IonToolbar, IonTitle, IonChip, IonContent, IonList, IonAccordionGroup, IonAccordion } from '@ionic/react';
|
||||||
import { list } from 'ionicons/icons';
|
import { list } from 'ionicons/icons';
|
||||||
import { InfiniteScrollList, SongItem, ListItem, TwoLineDisplay, NumberDisplay, SongInfo, ActionButton } from '../../components/common';
|
import { InfiniteScrollList, SongItem, ListItem, TwoLineDisplay, NumberDisplay, ActionButton } from '../../components/common';
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
import { Icons } from '../../constants';
|
import { Icons } from '../../constants';
|
||||||
import { useSongLists } from '../../hooks';
|
import { useSongLists } from '../../hooks';
|
||||||
@ -17,15 +17,13 @@ const SongLists: React.FC = () => {
|
|||||||
hasMore,
|
hasMore,
|
||||||
loadMore,
|
loadMore,
|
||||||
checkSongAvailability,
|
checkSongAvailability,
|
||||||
handleAddToQueue,
|
|
||||||
} = useSongLists();
|
} = useSongLists();
|
||||||
|
|
||||||
const songListData = useAppSelector(selectSongList);
|
const songListData = useAppSelector(selectSongList);
|
||||||
const songListCount = Object.keys(songListData).length;
|
const songListCount = Object.keys(songListData).length;
|
||||||
const [selectedSongList, setSelectedSongList] = useState<string | null>(null);
|
const [selectedSongList, setSelectedSongList] = useState<string | null>(null);
|
||||||
const [expandedSongKey, setExpandedSongKey] = useState<string | null>(null);
|
const [expandedSongKey, setExpandedSongKey] = useState<string | null>(null);
|
||||||
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
|
||||||
const [isSongInfoOpen, setIsSongInfoOpen] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -43,15 +41,7 @@ const SongLists: React.FC = () => {
|
|||||||
setExpandedSongKey(expandedSongKey === songKey ? null : songKey);
|
setExpandedSongKey(expandedSongKey === songKey ? null : songKey);
|
||||||
}, [expandedSongKey]);
|
}, [expandedSongKey]);
|
||||||
|
|
||||||
const handleSongInfo = useCallback((song: Song) => {
|
|
||||||
setSelectedSong(song);
|
|
||||||
setIsSongInfoOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCloseSongInfo = useCallback(() => {
|
|
||||||
setIsSongInfoOpen(false);
|
|
||||||
setSelectedSong(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const finalSelectedList = selectedSongList
|
const finalSelectedList = selectedSongList
|
||||||
? allSongLists.find(list => list.key === selectedSongList)
|
? allSongLists.find(list => list.key === selectedSongList)
|
||||||
@ -152,8 +142,6 @@ const SongLists: React.FC = () => {
|
|||||||
key={song.key || `${song.title}-${song.artist}-${sidx}`}
|
key={song.key || `${song.title}-${song.artist}-${sidx}`}
|
||||||
song={song}
|
song={song}
|
||||||
context="search"
|
context="search"
|
||||||
onAddToQueue={() => handleAddToQueue(song)}
|
|
||||||
onSelectSinger={() => handleSongInfo(song)}
|
|
||||||
showAddButton={true}
|
showAddButton={true}
|
||||||
showInfoButton={true}
|
showInfoButton={true}
|
||||||
showFavoriteButton={false}
|
showFavoriteButton={false}
|
||||||
@ -195,14 +183,6 @@ const SongLists: React.FC = () => {
|
|||||||
</IonContent>
|
</IonContent>
|
||||||
</IonModal>
|
</IonModal>
|
||||||
|
|
||||||
{/* Song Info Modal */}
|
|
||||||
{selectedSong && (
|
|
||||||
<SongInfo
|
|
||||||
isOpen={isSongInfoOpen}
|
|
||||||
onClose={handleCloseSongInfo}
|
|
||||||
song={selectedSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,14 +4,13 @@ import { list } from 'ionicons/icons';
|
|||||||
import { useTopPlayed } from '../../hooks';
|
import { useTopPlayed } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectTopPlayed, selectSongsArray } from '../../redux';
|
import { selectTopPlayed, selectSongsArray } from '../../redux';
|
||||||
import { InfiniteScrollList, SongItem, ListItem, SongInfo, ActionButton } from '../../components/common';
|
import { InfiniteScrollList, SongItem, ListItem, ActionButton } from '../../components/common';
|
||||||
import { filterSongs } from '../../utils/dataProcessing';
|
import { filterSongs } from '../../utils/dataProcessing';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
import { useSongOperations } from '../../hooks';
|
|
||||||
import { useToast } from '../../hooks';
|
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
import { Icons } from '../../constants';
|
import { Icons } from '../../constants';
|
||||||
import type { TopPlayed, Song } from '../../types';
|
import type { TopPlayed } from '../../types';
|
||||||
|
|
||||||
const Top100: React.FC = () => {
|
const Top100: React.FC = () => {
|
||||||
debugLog('Top100 component - RENDERING START');
|
debugLog('Top100 component - RENDERING START');
|
||||||
@ -26,11 +25,8 @@ const Top100: React.FC = () => {
|
|||||||
const topPlayed = useAppSelector(selectTopPlayed);
|
const topPlayed = useAppSelector(selectTopPlayed);
|
||||||
const topPlayedCount = Object.keys(topPlayed).length;
|
const topPlayedCount = Object.keys(topPlayed).length;
|
||||||
const allSongs = useAppSelector(selectSongsArray);
|
const allSongs = useAppSelector(selectSongsArray);
|
||||||
const { addToQueue } = useSongOperations();
|
|
||||||
const { showSuccess, showError } = useToast();
|
|
||||||
const [selectedTopPlayed, setSelectedTopPlayed] = useState<TopPlayed | null>(null);
|
const [selectedTopPlayed, setSelectedTopPlayed] = useState<TopPlayed | null>(null);
|
||||||
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
|
||||||
const [isSongInfoOpen, setIsSongInfoOpen] = useState(false);
|
|
||||||
|
|
||||||
debugLog('Top100 component - Redux data:', { topPlayedCount, topPlayedItems: topPlayedItems.length });
|
debugLog('Top100 component - Redux data:', { topPlayedCount, topPlayedItems: topPlayedItems.length });
|
||||||
|
|
||||||
@ -42,15 +38,7 @@ const Top100: React.FC = () => {
|
|||||||
setSelectedTopPlayed(null);
|
setSelectedTopPlayed(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSongInfo = useCallback((song: Song) => {
|
|
||||||
setSelectedSong(song);
|
|
||||||
setIsSongInfoOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCloseSongInfo = useCallback(() => {
|
|
||||||
setIsSongInfoOpen(false);
|
|
||||||
setSelectedSong(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Find songs that match the selected top played item
|
// Find songs that match the selected top played item
|
||||||
const selectedSongs = useMemo(() => {
|
const selectedSongs = useMemo(() => {
|
||||||
@ -75,15 +63,6 @@ const Top100: React.FC = () => {
|
|||||||
return filteredSongs;
|
return filteredSongs;
|
||||||
}, [selectedTopPlayed, allSongs]);
|
}, [selectedTopPlayed, allSongs]);
|
||||||
|
|
||||||
const handleAddToQueue = useCallback(async (song: Song) => {
|
|
||||||
try {
|
|
||||||
await addToQueue(song);
|
|
||||||
showSuccess('Song added to queue');
|
|
||||||
} catch {
|
|
||||||
showError('Failed to add song to queue');
|
|
||||||
}
|
|
||||||
}, [addToQueue, showSuccess, showError]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Use real Firebase data from the hook
|
// Use real Firebase data from the hook
|
||||||
@ -161,8 +140,6 @@ const Top100: React.FC = () => {
|
|||||||
key={song.key || `${song.title}-${song.artist}`}
|
key={song.key || `${song.title}-${song.artist}`}
|
||||||
song={song}
|
song={song}
|
||||||
context="search"
|
context="search"
|
||||||
onAddToQueue={() => handleAddToQueue(song)}
|
|
||||||
onSelectSinger={() => handleSongInfo(song)}
|
|
||||||
showAddButton={true}
|
showAddButton={true}
|
||||||
showInfoButton={true}
|
showInfoButton={true}
|
||||||
showFavoriteButton={false}
|
showFavoriteButton={false}
|
||||||
@ -172,14 +149,6 @@ const Top100: React.FC = () => {
|
|||||||
</IonContent>
|
</IonContent>
|
||||||
</IonModal>
|
</IonModal>
|
||||||
|
|
||||||
{/* Song Info Modal */}
|
|
||||||
{selectedSong && (
|
|
||||||
<SongInfo
|
|
||||||
isOpen={isSongInfoOpen}
|
|
||||||
onClose={handleCloseSongInfo}
|
|
||||||
song={selectedSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
10
src/hooks/useSongInfoContext.ts
Normal file
10
src/hooks/useSongInfoContext.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { SongInfoContext, type SongInfoContextType } from '../contexts/SongInfoContext';
|
||||||
|
|
||||||
|
export const useSongInfo = (): SongInfoContextType => {
|
||||||
|
const context = useContext(SongInfoContext);
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error('useSongInfo must be used within a SongInfoProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@ -165,11 +165,7 @@ export interface ActionButtonProps {
|
|||||||
export interface SongItemProps {
|
export interface SongItemProps {
|
||||||
song: Song;
|
song: Song;
|
||||||
context: 'search' | 'queue' | 'favorites' | 'history' | 'songlists' | 'top100' | 'new';
|
context: 'search' | 'queue' | 'favorites' | 'history' | 'songlists' | 'top100' | 'new';
|
||||||
onAddToQueue?: () => void;
|
|
||||||
onRemoveFromQueue?: () => void;
|
|
||||||
onToggleFavorite?: () => void;
|
|
||||||
onDeleteItem?: () => void;
|
onDeleteItem?: () => void;
|
||||||
onSelectSinger?: () => void;
|
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user