diff --git a/src/components/common/SongInfo.tsx b/src/components/common/SongInfo.tsx index a512819..ae4d367 100644 --- a/src/components/common/SongInfo.tsx +++ b/src/components/common/SongInfo.tsx @@ -8,7 +8,7 @@ import { } from 'ionicons/icons'; import { useAppSelector } from '../../redux'; import { selectIsAdmin, selectFavorites, selectSongs, selectQueue } from '../../redux'; -import { useActionHandlers } from '../../hooks/useActionHandlers'; +import { useActions } from '../../hooks/useActions'; import { useModal } from '../../hooks/useModalContext'; import { ModalHeader } from './ModalHeader'; @@ -26,7 +26,7 @@ const SongInfo: React.FC = ({ isOpen, onClose, song }) => { const favorites = useAppSelector(selectFavorites); const allSongs = useAppSelector(selectSongs); const queue = useAppSelector(selectQueue); - const { handleToggleFavorite, handleToggleDisabled, isSongDisabled } = useActionHandlers(); + const { handleToggleFavorite, handleToggleDisabled, isSongDisabled } = useActions(); const { openSelectSinger } = useModal(); const [showArtistSongs, setShowArtistSongs] = useState(false); diff --git a/src/components/common/SongItem.tsx b/src/components/common/SongItem.tsx index b4146da..55e8e93 100644 --- a/src/components/common/SongItem.tsx +++ b/src/components/common/SongItem.tsx @@ -3,8 +3,7 @@ import { IonItem, IonLabel } from '@ionic/react'; import ActionButton from './ActionButton'; import { useAppSelector } from '../../redux'; import { selectQueue, selectFavorites } from '../../redux'; -import { useSongOperations } from '../../hooks/useSongOperations'; -import { useToast } from '../../hooks/useToast'; +import { useActions } from '../../hooks/useActions'; import { useModal } from '../../hooks/useModalContext'; import { debugLog } from '../../utils/logger'; import type { SongItemProps, QueueItem, Song } from '../../types'; @@ -214,9 +213,8 @@ const SongItem: React.FC = ({ const queue = useAppSelector(selectQueue); const favorites = useAppSelector(selectFavorites); - // Get song operations and hooks - const { addToQueue, removeFromQueue, toggleFavorite } = useSongOperations(); - const { showSuccess, showError } = useToast(); + // Get unified action handlers + const { handleAddToQueue, handleToggleFavorite, handleRemoveFromQueue } = useActions(); const { openSongInfo } = useModal(); // Check if song is in queue or favorites based on path @@ -248,33 +246,21 @@ const SongItem: React.FC = ({ const shouldShowDeleteButton = showDeleteButton !== undefined ? showDeleteButton : context === 'history' && isAdmin; 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'); - } + // Create wrapper functions for the unified handlers + const handleAddToQueueClick = async () => { + await handleAddToQueue(song); }; - const handleRemoveFromQueue = async () => { + const handleToggleFavoriteClick = async () => { + await handleToggleFavorite(song); + }; + + const handleRemoveFromQueueClick = 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'); + // Find the queue item by key + const queueItem = (Object.values(queue) as QueueItem[]).find(item => item.key === queueItemKey); + if (queueItem) { + await handleRemoveFromQueue(queueItem); } }; @@ -302,9 +288,9 @@ const SongItem: React.FC = ({ showDeleteButton={shouldShowDeleteButton} showFavoriteButton={shouldShowFavoriteButton} onDeleteItem={onDeleteItem} - onAddToQueue={context === 'queue' ? handleRemoveFromQueue : handleAddToQueue} - onRemoveFromQueue={context === 'queue' ? handleRemoveFromQueue : onDeleteItem} - onToggleFavorite={context === 'favorites' ? onDeleteItem : handleToggleFavorite} + onAddToQueue={context === 'queue' ? handleRemoveFromQueueClick : handleAddToQueueClick} + onRemoveFromQueue={context === 'queue' ? handleRemoveFromQueueClick : onDeleteItem} + onToggleFavorite={context === 'favorites' ? onDeleteItem : handleToggleFavoriteClick} onShowSongInfo={handleSelectSinger} /> diff --git a/src/features/Queue/Queue.tsx b/src/features/Queue/Queue.tsx index 29ccf06..0c56fd7 100644 --- a/src/features/Queue/Queue.tsx +++ b/src/features/Queue/Queue.tsx @@ -1,48 +1,43 @@ import React, { useState, useEffect } from 'react'; import { IonItem, IonLabel, IonItemSliding, IonItemOptions, IonItemOption, IonIcon, IonReorderGroup, IonReorder } from '@ionic/react'; import { reorderThreeOutline, reorderTwoOutline, list } from 'ionicons/icons'; -import { useQueue } from '../../hooks'; +import { useQueue, useActions } from '../../hooks'; import { useAppSelector } from '../../redux'; -import { selectQueueLength, selectPlayerStateMemoized, selectIsAdmin, selectControllerName } from '../../redux'; +import { selectQueueLength } from '../../redux'; import { ActionButton, NumberDisplay, EmptyState } from '../../components/common'; import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types'; import { Icons } from '../../constants'; import { SongInfoDisplay } from '../../components/common/SongItem'; -import { queueService } from '../../firebase/services'; import { debugLog } from '../../utils/logger'; -import { PlayerState } from '../../types'; import type { QueueItem } from '../../types'; -type QueueMode = 'delete' | 'reorder'; - const Queue: React.FC = () => { - const [queueMode, setQueueMode] = useState('delete'); const [listItems, setListItems] = useState([]); const { queueItems, - canReorder, handleRemoveFromQueue, } = useQueue(); + const { + queueMode, + canDeleteItems, + canDeleteFirstItem, + toggleQueueMode, + handleReorder, + } = useActions(); + + // Check if reordering is allowed + const canReorder = canDeleteItems && queueItems.length > 1; + const queueCount = useAppSelector(selectQueueLength); - const playerState = useAppSelector(selectPlayerStateMemoized); - const isAdmin = useAppSelector(selectIsAdmin); - const controllerName = useAppSelector(selectControllerName); // Debug logging debugLog('Queue component - queue count:', queueCount); debugLog('Queue component - queue items:', queueItems); - debugLog('Queue component - player state:', playerState); - debugLog('Queue component - isAdmin:', isAdmin); debugLog('Queue component - canReorder:', canReorder); debugLog('Queue component - queueMode:', queueMode); - - // Check if items can be deleted (admin can delete any item when not playing) - const canDeleteItems = isAdmin && (playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused); - debugLog('Queue component - canDeleteItems:', canDeleteItems); - debugLog('Queue component - canReorder:', canReorder); // Update list items when queue changes @@ -55,52 +50,15 @@ const Queue: React.FC = () => { } }, [queueItems]); - // Toggle between modes - const toggleQueueMode = () => { - setQueueMode(prevMode => prevMode === 'delete' ? 'reorder' : 'delete'); - }; - // Handle reorder event from IonReorderGroup const doReorder = async (event: CustomEvent) => { - debugLog('Reorder event:', event.detail); - const { from, to, complete } = event.detail; - - if (listItems && controllerName) { - const copy = [...listItems]; - const draggedItem = copy.splice(from, 1)[0]; - copy.splice(to, 0, draggedItem); - - // Complete the reorder animation - complete(); - - // Create the new queue order (first item + reordered items) - const newQueueItems = [queueItems[0], ...copy]; - debugLog('New queue order:', newQueueItems); - - try { - // Update all items with their new order values - const updatePromises = newQueueItems.map((item, index) => { - const newOrder = index + 1; - if (item.key && item.order !== newOrder) { - debugLog(`Updating item ${item.key} from order ${item.order} to ${newOrder}`); - return queueService.updateQueueItem(controllerName, item.key, { order: newOrder }); - } - return Promise.resolve(); - }); - - await Promise.all(updatePromises); - debugLog('Queue reorder completed successfully'); - } catch (error) { - console.error('Failed to reorder queue:', error); - // You might want to show an error toast here - } - } + await handleReorder(event, queueItems); }; // Render queue item const renderQueueItem = (queueItem: QueueItem, index: number) => { debugLog(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`); - const canDelete = isAdmin && queueMode === 'delete'; // Only allow delete in delete mode + const canDelete = canDeleteItems && queueMode === 'delete'; // Only allow delete in delete mode return ( @@ -167,7 +125,6 @@ const Queue: React.FC = () => { if (queueItems.length === 0) return null; const firstItem = queueItems[0]; - const canDeleteFirstItem = isAdmin && (playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused); return ( @@ -236,7 +193,7 @@ const Queue: React.FC = () => { <>
- {isAdmin && ( + {canDeleteItems && ( { - const { addToQueue, toggleFavorite } = useSongOperations(); - const { showSuccess, showError } = useToast(); - const { isSongDisabled, addDisabledSong, removeDisabledSong } = useDisabledSongs(); - const controllerName = useAppSelector(selectControllerName); - - 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]); - - const handleToggleFavorite = useCallback(async (song: Song) => { - try { - await toggleFavorite(song); - showSuccess(song.favorite ? 'Removed from favorites' : 'Added to favorites'); - } catch { - showError('Failed to update favorites'); - } - }, [toggleFavorite, showSuccess, showError]); - - const handleToggleDisabled = useCallback(async (song: Song) => { - try { - if (isSongDisabled(song)) { - await removeDisabledSong(song); - showSuccess('Song enabled'); - } else { - await addDisabledSong(song); - showSuccess('Song disabled'); - } - } catch { - showError('Failed to update song disabled status'); - } - }, [isSongDisabled, addDisabledSong, removeDisabledSong, showSuccess, showError]); - - const handleDeleteFromHistory = useCallback(async (song: Song) => { - if (!controllerName || !song.key) { - showError('Cannot delete history item - missing data'); - return; - } - - try { - await historyService.removeFromHistory(controllerName, song.key); - showSuccess('Removed from history'); - } catch { - showError('Failed to remove from history'); - } - }, [controllerName, showSuccess, showError]); - - return { - handleAddToQueue, - handleToggleFavorite, - handleToggleDisabled, - handleDeleteFromHistory, - isSongDisabled, - }; -}; \ No newline at end of file diff --git a/src/hooks/useActions.ts b/src/hooks/useActions.ts new file mode 100644 index 0000000..ee99d96 --- /dev/null +++ b/src/hooks/useActions.ts @@ -0,0 +1,153 @@ +import { useState, useCallback } from 'react'; +import { useAppSelector } from '../redux'; +import { selectControllerName, selectPlayerStateMemoized, selectIsAdmin } from '../redux'; +import { useSongOperations } from './useSongOperations'; +import { useToast } from './useToast'; +import { useDisabledSongs } from './useDisabledSongs'; +import { queueService, historyService } from '../firebase/services'; +import { debugLog } from '../utils/logger'; +import { PlayerState } from '../types'; +import type { Song, QueueItem } from '../types'; + +export type QueueMode = 'delete' | 'reorder'; + +export const useActions = () => { + const [queueMode, setQueueMode] = useState('delete'); + + const controllerName = useAppSelector(selectControllerName); + const playerState = useAppSelector(selectPlayerStateMemoized); + const isAdmin = useAppSelector(selectIsAdmin); + const { addToQueue, removeFromQueue, toggleFavorite } = useSongOperations(); + const { showSuccess, showError } = useToast(); + const { isSongDisabled, addDisabledSong, removeDisabledSong } = useDisabledSongs(); + + // Queue permissions + const canDeleteItems = isAdmin && (playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused); + const canDeleteFirstItem = isAdmin && (playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused); + + // Song operations + 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]); + + const handleRemoveFromQueue = useCallback(async (queueItem: QueueItem) => { + if (!queueItem.key) return; + + try { + await removeFromQueue(queueItem.key); + showSuccess('Song removed from queue'); + } catch { + showError('Failed to remove song from queue'); + } + }, [removeFromQueue, showSuccess, showError]); + + const handleToggleFavorite = useCallback(async (song: Song) => { + try { + await toggleFavorite(song); + showSuccess(song.favorite ? 'Removed from favorites' : 'Added to favorites'); + } catch { + showError('Failed to update favorites'); + } + }, [toggleFavorite, showSuccess, showError]); + + const handleToggleDisabled = useCallback(async (song: Song) => { + try { + if (isSongDisabled(song)) { + await removeDisabledSong(song); + showSuccess('Song enabled'); + } else { + await addDisabledSong(song); + showSuccess('Song disabled'); + } + } catch { + showError('Failed to update song disabled status'); + } + }, [isSongDisabled, addDisabledSong, removeDisabledSong, showSuccess, showError]); + + const handleDeleteFromHistory = useCallback(async (song: Song) => { + if (!controllerName || !song.key) { + showError('Cannot delete history item - missing data'); + return; + } + + try { + await historyService.removeFromHistory(controllerName, song.key); + showSuccess('Removed from history'); + } catch { + showError('Failed to remove from history'); + } + }, [controllerName, showSuccess, showError]); + + // Queue UI operations + const toggleQueueMode = useCallback(() => { + setQueueMode(prevMode => prevMode === 'delete' ? 'reorder' : 'delete'); + }, []); + + // Queue operations + const handleReorder = useCallback(async (event: CustomEvent, queueItems: QueueItem[]) => { + debugLog('Reorder event:', event.detail); + const { from, to, complete } = event.detail; + + if (!controllerName) { + showError('Cannot reorder - controller not available'); + return; + } + + try { + // Create the new queue order (first item + reordered items) + const listItems = queueItems.slice(1); // Skip first item for reordering + const copy = [...listItems]; + const draggedItem = copy.splice(from, 1)[0]; + copy.splice(to, 0, draggedItem); + + // Complete the reorder animation + complete(); + + const newQueueItems = [queueItems[0], ...copy]; + debugLog('New queue order:', newQueueItems); + + // Update all items with their new order values + const updatePromises = newQueueItems.map((item, index) => { + const newOrder = index + 1; + if (item.key && item.order !== newOrder) { + debugLog(`Updating item ${item.key} from order ${item.order} to ${newOrder}`); + return queueService.updateQueueItem(controllerName, item.key, { order: newOrder }); + } + return Promise.resolve(); + }); + + await Promise.all(updatePromises); + debugLog('Queue reorder completed successfully'); + showSuccess('Queue reordered successfully'); + } catch (error) { + console.error('Failed to reorder queue:', error); + showError('Failed to reorder queue'); + } + }, [controllerName, showSuccess, showError]); + + return { + // Song operations + handleAddToQueue, + handleRemoveFromQueue, + handleToggleFavorite, + handleToggleDisabled, + handleDeleteFromHistory, + + // Queue UI state + queueMode, + toggleQueueMode, + + // Queue operations + handleReorder, + + // Permissions + canDeleteItems, + canDeleteFirstItem, + isSongDisabled, + }; +}; \ No newline at end of file diff --git a/src/hooks/useFavorites.ts b/src/hooks/useFavorites.ts index ca68368..e77f90a 100644 --- a/src/hooks/useFavorites.ts +++ b/src/hooks/useFavorites.ts @@ -1,14 +1,14 @@ import { useCallback, useMemo, useState } from 'react'; import { useAppSelector, selectFavoritesArray } from '../redux'; import { debugLog } from '../utils/logger'; -import { useActionHandlers } from './useActionHandlers'; +import { useActions } from './useActions'; import { useDisabledSongs } from './useDisabledSongs'; const ITEMS_PER_PAGE = 20; export const useFavorites = () => { const allFavoritesItems = useAppSelector(selectFavoritesArray); - const { handleAddToQueue, handleToggleFavorite, handleToggleDisabled, isSongDisabled } = useActionHandlers(); + const { handleAddToQueue, handleToggleFavorite, handleToggleDisabled, isSongDisabled } = useActions(); const { disabledSongPaths, loading: disabledSongsLoading } = useDisabledSongs(); const [currentPage, setCurrentPage] = useState(1); diff --git a/src/hooks/useHistory.ts b/src/hooks/useHistory.ts index ccad200..cfcde6d 100644 --- a/src/hooks/useHistory.ts +++ b/src/hooks/useHistory.ts @@ -1,14 +1,14 @@ import { useCallback, useMemo, useState } from 'react'; import { useAppSelector, selectHistoryArray } from '../redux'; import { debugLog } from '../utils/logger'; -import { useActionHandlers } from './useActionHandlers'; +import { useActions } from './useActions'; import { useDisabledSongs } from './useDisabledSongs'; const ITEMS_PER_PAGE = 20; export const useHistory = () => { const allHistoryItems = useAppSelector(selectHistoryArray); - const { handleAddToQueue, handleToggleFavorite, handleToggleDisabled, handleDeleteFromHistory, isSongDisabled } = useActionHandlers(); + const { handleAddToQueue, handleToggleFavorite, handleToggleDisabled, handleDeleteFromHistory, isSongDisabled } = useActions(); const { disabledSongPaths, loading: disabledSongsLoading } = useDisabledSongs(); const [currentPage, setCurrentPage] = useState(1); diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts index 1cb1983..76cf9d9 100644 --- a/src/hooks/useSearch.ts +++ b/src/hooks/useSearch.ts @@ -1,6 +1,6 @@ import { useState, useCallback, useMemo } from 'react'; import { useAppSelector, selectSongsArray } from '../redux'; -import { useActionHandlers } from './useActionHandlers'; +import { useActions } from './useActions'; import { useDisabledSongs } from './useDisabledSongs'; import { UI_CONSTANTS } from '../constants'; import { filterSongs } from '../utils/dataProcessing'; @@ -11,7 +11,7 @@ const ITEMS_PER_PAGE = 20; export const useSearch = () => { const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); - const { handleAddToQueue, handleToggleFavorite, handleToggleDisabled, isSongDisabled } = useActionHandlers(); + const { handleAddToQueue, handleToggleFavorite, handleToggleDisabled, isSongDisabled } = useActions(); const { disabledSongPaths, loading: disabledSongsLoading } = useDisabledSongs(); // Get all songs from Redux (this is memoized)