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

This commit is contained in:
mbrucedogs 2025-07-20 20:26:48 -05:00
parent debabcc9e7
commit 551b180f54
9 changed files with 196 additions and 169 deletions

View File

@ -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<SongInfoProps> = ({ 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);

View File

@ -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<SongItemProps> = ({
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<SongItemProps> = ({
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<SongItemProps> = ({
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}
/>
</div>

View File

@ -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<QueueMode>('delete');
const [listItems, setListItems] = useState<QueueItem[]>([]);
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 (
<IonItemSliding key={queueItem.key}>
@ -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 (
<IonItemSliding key={firstItem.key}>
@ -236,7 +193,7 @@ const Queue: React.FC = () => {
<>
<div className="ion-padding">
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem' }}>
{isAdmin && (
{canDeleteItems && (
<ActionButton
onClick={toggleQueueMode}
variant={ActionButtonVariant.SECONDARY}

View File

@ -11,6 +11,6 @@ export { useArtists } from './useArtists';
export { useSingers } from './useSingers';
export { useSongLists } from './useSongLists';
export { useDisabledSongs } from './useDisabledSongs';
export { useActionHandlers } from './useActionHandlers';
export { useActions } from './useActions';
export { useSongInfo } from './useSongInfo';

View File

@ -1,69 +0,0 @@
import { useCallback } from 'react';
import { useSongOperations } from './useSongOperations';
import { useToast } from './useToast';
import { useDisabledSongs } from './useDisabledSongs';
import { historyService } from '../firebase/services';
import { useAppSelector } from '../redux';
import { selectControllerName } from '../redux';
import type { Song } from '../types';
export const useActionHandlers = () => {
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,
};
};

153
src/hooks/useActions.ts Normal file
View File

@ -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<QueueMode>('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,
};
};

View File

@ -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);

View File

@ -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);

View File

@ -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)