From 1c714ec3414dd82503a8337ce35e96a29f84c62f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 17 Jul 2025 13:53:39 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- src/components/Auth/AuthInitializer.tsx | 40 ++++--- src/components/common/PlayerControls.tsx | 133 +++++++++++++++++++++ src/components/common/SongItem.tsx | 4 +- src/components/common/index.ts | 5 +- src/features/Artists/Artists.tsx | 112 ++++++++++++++---- src/features/Queue/Queue.tsx | 66 +++++++---- src/features/SongLists/SongLists.tsx | 2 +- src/firebase/services.ts | 65 +++++++++- src/hooks/useArtists.ts | 70 +++++++++-- src/hooks/useQueue.ts | 144 +++++++++++++++++++++-- src/hooks/useSongOperations.ts | 7 +- src/redux/controllerSlice.ts | 14 ++- 12 files changed, 579 insertions(+), 83 deletions(-) create mode 100644 src/components/common/PlayerControls.tsx diff --git a/src/components/Auth/AuthInitializer.tsx b/src/components/Auth/AuthInitializer.tsx index 0b53a7d..d06cf42 100644 --- a/src/components/Auth/AuthInitializer.tsx +++ b/src/components/Auth/AuthInitializer.tsx @@ -1,11 +1,8 @@ import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; -import { setAuth } from '../../redux/authSlice'; import { selectIsAuthenticated } from '../../redux/authSlice'; -import { CONTROLLER_NAME } from '../../constants'; import { LoginPrompt } from './index'; -import type { Authentication } from '../../types'; interface AuthInitializerProps { children: React.ReactNode; @@ -14,40 +11,47 @@ interface AuthInitializerProps { const AuthInitializer: React.FC = ({ children }) => { const [searchParams] = useSearchParams(); const [showLogin, setShowLogin] = useState(false); + const [isAdminMode, setIsAdminMode] = useState(false); + const [hasProcessedAdminParam, setHasProcessedAdminParam] = useState(false); const dispatch = useAppDispatch(); const isAuthenticated = useAppSelector(selectIsAuthenticated); useEffect(() => { + // Only process admin parameter once + if (hasProcessedAdminParam) return; + // Check for admin parameter in URL const isAdmin = searchParams.get('admin') === 'true'; - // If admin parameter is present, auto-authenticate if (isAdmin) { - const auth: Authentication = { - authenticated: true, - singer: 'Admin', - isAdmin: true, - controller: CONTROLLER_NAME, - }; - dispatch(setAuth(auth)); - - // Clean up URL + // Set admin mode but don't auto-authenticate + setIsAdminMode(true); + setHasProcessedAdminParam(true); + } + + // Show login prompt if not authenticated (for both admin and regular users) + if (!isAuthenticated) { + setShowLogin(true); + } + }, [searchParams, dispatch, isAuthenticated, hasProcessedAdminParam]); + + // Clean up admin parameter after successful authentication + useEffect(() => { + if (isAuthenticated && isAdminMode && hasProcessedAdminParam) { + // Clean up URL after successful admin login if (window.history.replaceState) { const newUrl = new URL(window.location.href); newUrl.searchParams.delete('admin'); window.history.replaceState({}, '', newUrl.toString()); } - } else if (!isAuthenticated) { - // Show login prompt for regular users - setShowLogin(true); } - }, [searchParams, dispatch, isAuthenticated]); + }, [isAuthenticated, isAdminMode, hasProcessedAdminParam]); // Show login prompt if not authenticated if (showLogin && !isAuthenticated) { return ( setShowLogin(false)} /> ); diff --git a/src/components/common/PlayerControls.tsx b/src/components/common/PlayerControls.tsx new file mode 100644 index 0000000..1c8ecd8 --- /dev/null +++ b/src/components/common/PlayerControls.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import ActionButton from './ActionButton'; +import { useAppSelector } from '../../redux'; +import { selectPlayerState, selectIsAdmin, selectQueue } from '../../redux'; +import { playerService } from '../../firebase/services'; +import { selectControllerName } from '../../redux'; +import { useToast } from '../../hooks/useToast'; +import { PlayerState } from '../../types'; + +interface PlayerControlsProps { + className?: string; +} + +const PlayerControls: React.FC = ({ className = '' }) => { + const playerState = useAppSelector(selectPlayerState); + const isAdmin = useAppSelector(selectIsAdmin); + const controllerName = useAppSelector(selectControllerName); + const queue = useAppSelector(selectQueue); + const { showSuccess, showError } = useToast(); + + // Debug logging + console.log('PlayerControls - playerState:', playerState); + console.log('PlayerControls - isAdmin:', isAdmin); + console.log('PlayerControls - queue length:', Object.keys(queue).length); + + const handlePlay = async () => { + if (!controllerName) return; + + try { + await playerService.updatePlayerStateValue(controllerName, PlayerState.playing); + showSuccess('Playback started'); + } catch (error) { + console.error('Failed to start playback:', error); + showError('Failed to start playback'); + } + }; + + const handlePause = async () => { + if (!controllerName) return; + + try { + await playerService.updatePlayerStateValue(controllerName, PlayerState.paused); + showSuccess('Playback paused'); + } catch (error) { + console.error('Failed to pause playback:', error); + showError('Failed to pause playback'); + } + }; + + const handleStop = async () => { + if (!controllerName) return; + + try { + await playerService.updatePlayerStateValue(controllerName, PlayerState.stopped); + showSuccess('Playback stopped'); + } catch (error) { + console.error('Failed to stop playback:', error); + showError('Failed to stop playback'); + } + }; + + // Only show controls for admin users + if (!isAdmin) { + return null; + } + + const currentState = playerState?.state || PlayerState.stopped; + const hasSongsInQueue = Object.keys(queue).length > 0; + + console.log('PlayerControls - currentState:', currentState); + console.log('PlayerControls - hasSongsInQueue:', hasSongsInQueue); + + return ( +
+
+
+

Player Controls

+ + {currentState} + +
+
+ +
+ {currentState === PlayerState.playing ? ( + + ⏸️ Pause + + ) : ( + + ▶️ Play + + )} + + {currentState !== PlayerState.stopped && ( + + ⏹️ Stop + + )} +
+ +
+ Admin controls - Only visible to admin users + {!hasSongsInQueue && ( +
+ Add songs to queue to enable playback controls +
+ )} +
+
+ ); +}; + +export default PlayerControls; \ No newline at end of file diff --git a/src/components/common/SongItem.tsx b/src/components/common/SongItem.tsx index 4764db4..6d58aea 100644 --- a/src/components/common/SongItem.tsx +++ b/src/components/common/SongItem.tsx @@ -37,9 +37,9 @@ const SongItem: React.FC = ({ case 'queue': return (
- {isAdmin && ( + {isAdmin && onRemoveFromQueue && ( {})} + onClick={onRemoveFromQueue} variant="danger" size="sm" > diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 89f35e4..017d369 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,6 +1,7 @@ +export { default as ActionButton } from './ActionButton'; export { default as EmptyState } from './EmptyState'; export { default as Toast } from './Toast'; -export { default as ActionButton } from './ActionButton'; +export { default as ErrorBoundary } from './ErrorBoundary'; export { default as InfiniteScrollList } from './InfiniteScrollList'; export { default as SongItem } from './SongItem'; -export { default as ErrorBoundary } from './ErrorBoundary'; \ No newline at end of file +export { default as PlayerControls } from './PlayerControls'; \ No newline at end of file diff --git a/src/features/Artists/Artists.tsx b/src/features/Artists/Artists.tsx index 9e2441f..3db93d9 100644 --- a/src/features/Artists/Artists.tsx +++ b/src/features/Artists/Artists.tsx @@ -1,16 +1,19 @@ -import React, { useState } from 'react'; -import { InfiniteScrollList, ActionButton } from '../../components/common'; +import React, { useState, useEffect, useRef } from 'react'; +import { ActionButton } from '../../components/common'; import { useArtists } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectSongs } from '../../redux'; - const Artists: React.FC = () => { const { artists, + allArtists, searchTerm, + hasMore, + loadMore, handleSearchChange, getSongsByArtist, + getSongCountByArtist, handleAddToQueue, handleToggleFavorite, } = useArtists(); @@ -18,10 +21,40 @@ const Artists: React.FC = () => { const songs = useAppSelector(selectSongs); const songsCount = Object.keys(songs).length; const [selectedArtist, setSelectedArtist] = useState(null); + const observerRef = useRef(null); + + // Intersection Observer for infinite scrolling + useEffect(() => { + console.log('Artists - Setting up observer:', { hasMore, songsCount, itemsLength: artists.length }); + + const observer = new IntersectionObserver( + (entries) => { + console.log('Artists - Intersection detected:', { + isIntersecting: entries[0].isIntersecting, + hasMore, + songsCount + }); + + if (entries[0].isIntersecting && hasMore && songsCount > 0) { + console.log('Artists - Loading more items'); + loadMore(); + } + }, + { threshold: 0.1 } + ); + + if (observerRef.current) { + observer.observe(observerRef.current); + } + + return () => observer.disconnect(); + }, [loadMore, hasMore, songsCount, artists.length]); // Debug logging - console.log('Artists component - artists count:', artists.length); - console.log('Artists component - selected artist:', selectedArtist); + useEffect(() => { + console.log('Artists component - artists count:', artists.length); + console.log('Artists component - selected artist:', selectedArtist); + }, [artists.length, selectedArtist]); const handleArtistClick = (artist: string) => { setSelectedArtist(artist); @@ -56,7 +89,7 @@ const Artists: React.FC = () => { {/* Debug info */}
- Total songs loaded: {songsCount} | Showing: {artists.length} artists | Search term: "{searchTerm}" + Total songs loaded: {songsCount} | Showing: {artists.length} of {allArtists.length} artists | Search term: "{searchTerm}"
@@ -95,7 +128,7 @@ const Artists: React.FC = () => { {artist}

- {getSongsByArtist(artist).length} song{getSongsByArtist(artist).length !== 1 ? 's' : ''} + {getSongCountByArtist(artist)} song{getSongCountByArtist(artist) !== 1 ? 's' : ''}

@@ -109,14 +142,30 @@ const Artists: React.FC = () => {
))} + + {/* Infinite scroll trigger */} + {hasMore && ( +
+
+ + + + + Loading more artists... +
+
+ )} )} {/* Artist Songs Modal */} {selectedArtist && ( -
-
+
+

@@ -133,19 +182,38 @@ const Artists: React.FC = () => {

- {}} - onAddToQueue={handleAddToQueue} - onToggleFavorite={handleToggleFavorite} - context="search" - title="" - emptyTitle="No songs found" - emptyMessage="No songs found for this artist" - debugInfo="" - /> +
+ {selectedArtistSongs.map((song) => ( +
+
+
+

+ {song.title} +

+

+ {song.artist} +

+
+
+ handleAddToQueue(song)} + variant="primary" + size="sm" + > + Add to Queue + + handleToggleFavorite(song)} + variant="secondary" + size="sm" + > + {song.favorite ? 'Remove from Favorites' : 'Add to Favorites'} + +
+
+
+ ))} +
diff --git a/src/features/Queue/Queue.tsx b/src/features/Queue/Queue.tsx index 5fc36ac..a5962b1 100644 --- a/src/features/Queue/Queue.tsx +++ b/src/features/Queue/Queue.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import { SongItem, EmptyState, ActionButton } from '../../components/common'; +import { SongItem, EmptyState, ActionButton, PlayerControls } from '../../components/common'; import { useQueue } from '../../hooks'; import { useAppSelector } from '../../redux'; -import { selectQueue } from '../../redux'; +import { selectQueue, selectPlayerState } from '../../redux'; +import { PlayerState } from '../../types'; const Queue: React.FC = () => { const { @@ -16,11 +17,19 @@ const Queue: React.FC = () => { } = useQueue(); const queue = useAppSelector(selectQueue); + const playerState = useAppSelector(selectPlayerState); const queueCount = Object.keys(queue).length; // Debug logging console.log('Queue component - queue count:', queueCount); console.log('Queue component - queue items:', queueItems); + console.log('Queue component - player state:', playerState); + + // Check if first item can be deleted (only when stopped or paused) + const canDeleteFirstItem = playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused; + + console.log('Queue component - canDeleteFirstItem:', canDeleteFirstItem); + console.log('Queue component - canReorder:', canReorder); return (
@@ -36,6 +45,11 @@ const Queue: React.FC = () => {
+ {/* Player Controls - Only visible to admin users */} +
+ +
+ {/* Queue List */}
{queueCount === 0 ? ( @@ -60,7 +74,9 @@ const Queue: React.FC = () => { /> ) : (
- {queueItems.map((queueItem, index) => ( + {queueItems.map((queueItem, index) => { + console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`); + return (
{/* Order Number */}
@@ -72,7 +88,12 @@ const Queue: React.FC = () => { handleRemoveFromQueue(queueItem)} + onRemoveFromQueue={ + // Only allow removal of first item when stopped or paused + index === 0 && !canDeleteFirstItem + ? undefined + : () => handleRemoveFromQueue(queueItem) + } onToggleFavorite={() => handleToggleFavorite(queueItem.song)} isAdmin={canReorder} /> @@ -89,26 +110,29 @@ const Queue: React.FC = () => { {/* Admin Controls */} {canReorder && (
- handleMoveUp(queueItem)} - variant="secondary" - size="sm" - disabled={index === 0} - > - ↑ - - handleMoveDown(queueItem)} - variant="secondary" - size="sm" - disabled={index === queueItems.length - 1} - > - ↓ - + {queueItem.order > 2 && ( + handleMoveUp(queueItem)} + variant="secondary" + size="sm" + > + ↑ + + )} + {queueItem.order > 1 && queueItem.order < queueItems.length && ( + handleMoveDown(queueItem)} + variant="secondary" + size="sm" + > + ↓ + + )}
)}
- ))} + ); + })}
)}
diff --git a/src/features/SongLists/SongLists.tsx b/src/features/SongLists/SongLists.tsx index 1ac637c..7fe96be 100644 --- a/src/features/SongLists/SongLists.tsx +++ b/src/features/SongLists/SongLists.tsx @@ -178,7 +178,7 @@ const SongLists: React.FC = () => {

- TEST MODAL - {finalSelectedList.title} + {finalSelectedList.title}

) => { const queueRef = ref(database, `controllers/${controllerName}/player/queue`); - return await push(queueRef, queueItem); + + // Get current queue to find the next sequential key + const snapshot = await get(queueRef); + const currentQueue = snapshot.exists() ? snapshot.val() : {}; + + // Find the next available numerical key + const existingKeys = Object.keys(currentQueue) + .filter(key => /^\d+$/.test(key)) // Only consider numerical keys + .map(key => parseInt(key, 10)) + .sort((a, b) => a - b); + + const nextKey = existingKeys.length > 0 ? Math.max(...existingKeys) + 1 : 0; + + // Add the item with the sequential key + const newItemRef = ref(database, `controllers/${controllerName}/player/queue/${nextKey}`); + await set(newItemRef, queueItem); + + return { key: nextKey.toString() }; }, // Remove song from queue @@ -64,6 +81,46 @@ export const queueService = { await update(queueItemRef, updates); }, + // Clean up queue with inconsistent keys (migrate push ID keys to sequential numerical keys) + cleanupQueueKeys: async (controllerName: string) => { + const queueRef = ref(database, `controllers/${controllerName}/player/queue`); + const snapshot = await get(queueRef); + + if (!snapshot.exists()) return; + + const queue = snapshot.val(); + const updates: Record = {}; + let hasChanges = false; + + // Find all push ID keys (non-numerical keys) + const pushIdKeys = Object.keys(queue).filter(key => !/^\d+$/.test(key)); + + if (pushIdKeys.length === 0) return; // No cleanup needed + + // Get existing numerical keys to find the next available key + const existingNumericalKeys = Object.keys(queue) + .filter(key => /^\d+$/.test(key)) + .map(key => parseInt(key, 10)) + .sort((a, b) => a - b); + + let nextKey = existingNumericalKeys.length > 0 ? Math.max(...existingNumericalKeys) + 1 : 0; + + // Migrate push ID items to sequential numerical keys + pushIdKeys.forEach(pushIdKey => { + const item = queue[pushIdKey]; + // Remove the old item with push ID + updates[pushIdKey] = null; + // Add the item with sequential numerical key + updates[nextKey.toString()] = item; + nextKey++; + hasChanges = true; + }); + + if (hasChanges) { + await update(queueRef, updates); + } + }, + // Listen to queue changes subscribeToQueue: (controllerName: string, callback: (data: Record) => void) => { const queueRef = ref(database, `controllers/${controllerName}/player/queue`); @@ -83,6 +140,12 @@ export const playerService = { await update(playerRef, state); }, + // Update just the player state value + updatePlayerStateValue: async (controllerName: string, stateValue: string) => { + const stateRef = ref(database, `controllers/${controllerName}/player/state`); + await set(stateRef, stateValue); + }, + // Listen to player state changes subscribeToPlayerState: (controllerName: string, callback: (data: Controller['player']) => void) => { const playerRef = ref(database, `controllers/${controllerName}/player`); diff --git a/src/hooks/useArtists.ts b/src/hooks/useArtists.ts index 2e7811c..31878bc 100644 --- a/src/hooks/useArtists.ts +++ b/src/hooks/useArtists.ts @@ -4,6 +4,8 @@ import { useSongOperations } from './useSongOperations'; import { useToast } from './useToast'; import type { Song } from '../types'; +const ITEMS_PER_PAGE = 20; + export const useArtists = () => { const allArtists = useAppSelector(selectArtistsArray); const allSongs = useAppSelector(selectSongsArray); @@ -11,6 +13,25 @@ export const useArtists = () => { const { showSuccess, showError } = useToast(); const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + + // Pre-compute songs by artist and song counts for performance + const songsByArtist = useMemo(() => { + const songsMap = new Map(); + const countsMap = new Map(); + + allSongs.forEach(song => { + const artist = song.artist.toLowerCase(); + if (!songsMap.has(artist)) { + songsMap.set(artist, []); + countsMap.set(artist, 0); + } + songsMap.get(artist)!.push(song); + countsMap.set(artist, countsMap.get(artist)! + 1); + }); + + return { songsMap, countsMap }; + }, [allSongs]); // Filter artists by search term const filteredArtists = useMemo(() => { @@ -22,15 +43,45 @@ export const useArtists = () => { ); }, [allArtists, searchTerm]); - // Get songs by artist + // Paginate the filtered artists - show all items up to current page + const artists = useMemo(() => { + const endIndex = currentPage * ITEMS_PER_PAGE; + return filteredArtists.slice(0, endIndex); + }, [filteredArtists, currentPage]); + + const hasMore = useMemo(() => { + // Show "hasMore" if there are more items than currently loaded + return filteredArtists.length > ITEMS_PER_PAGE && artists.length < filteredArtists.length; + }, [artists.length, filteredArtists.length]); + + const loadMore = useCallback(() => { + console.log('useArtists - loadMore called:', { + hasMore, + currentPage, + filteredArtistsLength: filteredArtists.length, + artistsLength: artists.length + }); + if (hasMore) { + console.log('useArtists - Incrementing page from', currentPage, 'to', currentPage + 1); + setCurrentPage(prev => prev + 1); + } else { + console.log('useArtists - Not loading more because hasMore is false'); + } + }, [hasMore, currentPage, filteredArtists.length, artists.length]); + + // Get songs by artist (now using cached data) const getSongsByArtist = useCallback((artistName: string) => { - return allSongs.filter(song => - song.artist.toLowerCase() === artistName.toLowerCase() - ); - }, [allSongs]); + return songsByArtist.songsMap.get(artistName.toLowerCase()) || []; + }, [songsByArtist.songsMap]); + + // Get song count by artist (now using cached data) + const getSongCountByArtist = useCallback((artistName: string) => { + return songsByArtist.countsMap.get(artistName.toLowerCase()) || 0; + }, [songsByArtist.countsMap]); const handleSearchChange = useCallback((value: string) => { setSearchTerm(value); + setCurrentPage(1); // Reset to first page when searching }, []); const handleAddToQueue = useCallback(async (song: Song) => { @@ -52,11 +103,16 @@ export const useArtists = () => { }, [toggleFavorite, showSuccess, showError]); return { - artists: filteredArtists, - allArtists, + artists, + allArtists: filteredArtists, searchTerm, + hasMore, + loadMore, + currentPage, + totalPages: Math.ceil(filteredArtists.length / ITEMS_PER_PAGE), handleSearchChange, getSongsByArtist, + getSongCountByArtist, handleAddToQueue, handleToggleFavorite, }; diff --git a/src/hooks/useQueue.ts b/src/hooks/useQueue.ts index 12e58c6..73ba501 100644 --- a/src/hooks/useQueue.ts +++ b/src/hooks/useQueue.ts @@ -1,16 +1,67 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { useAppSelector, selectQueueWithUserInfo, selectQueueStats, selectCanReorderQueue } from '../redux'; import { useSongOperations } from './useSongOperations'; import { useToast } from './useToast'; +import { queueService } from '../firebase/services'; +import { selectControllerName } from '../redux'; import type { QueueItem } from '../types'; export const useQueue = () => { const queueItems = useAppSelector(selectQueueWithUserInfo); const queueStats = useAppSelector(selectQueueStats); const canReorder = useAppSelector(selectCanReorderQueue); + const controllerName = useAppSelector(selectControllerName); const { removeFromQueue, toggleFavorite } = useSongOperations(); const { showSuccess, showError } = useToast(); + // Fix queue order if needed + const fixQueueOrder = useCallback(async () => { + if (!controllerName || queueItems.length === 0) return; + + // Check if any items are missing order or have inconsistent order + const needsFix = queueItems.some((item, index) => { + const expectedOrder = index + 1; + return !item.order || item.order !== expectedOrder; + }); + + if (needsFix) { + console.log('Fixing queue order...'); + try { + // Update all items with sequential order + const updatePromises = queueItems.map((item, index) => { + const newOrder = index + 1; + if (item.key && item.order !== newOrder) { + return queueService.updateQueueItem(controllerName, item.key, { order: newOrder }); + } + return Promise.resolve(); + }); + + await Promise.all(updatePromises); + console.log('Queue order fixed successfully'); + } catch (error) { + console.error('Failed to fix queue order:', error); + } + } + }, [controllerName, queueItems]); + + // Fix queue order and cleanup keys on mount if needed + useEffect(() => { + const initializeQueue = async () => { + if (controllerName) { + try { + // First cleanup any inconsistent keys + await queueService.cleanupQueueKeys(controllerName); + // Then fix the order + await fixQueueOrder(); + } catch (error) { + console.error('Failed to initialize queue:', error); + } + } + }; + + initializeQueue(); + }, [controllerName, fixQueueOrder]); + const handleRemoveFromQueue = useCallback(async (queueItem: QueueItem) => { if (!queueItem.key) return; @@ -32,14 +83,93 @@ export const useQueue = () => { }, [toggleFavorite, showSuccess, showError]); const handleMoveUp = useCallback(async (queueItem: QueueItem) => { - // TODO: Implement move up logic - console.log('Move up:', queueItem); - }, []); + console.log('handleMoveUp called with:', queueItem); + console.log('Current queueItems:', queueItems); + console.log('Controller name:', controllerName); + + if (!controllerName || !queueItem.key || queueItem.order <= 1) { + console.log('Early return - conditions not met:', { + controllerName: !!controllerName, + queueItemKey: !!queueItem.key, + order: queueItem.order + }); + return; // Can't move up if already at the top + } + + try { + // Find the item above this one + const itemAbove = queueItems.find(item => item.order === queueItem.order - 1); + console.log('Item above:', itemAbove); + + if (!itemAbove || !itemAbove.key) { + console.log('No item above found'); + showError('Cannot move item up'); + return; + } + + console.log('Swapping orders:', { + currentItem: { key: queueItem.key, order: queueItem.order }, + itemAbove: { key: itemAbove.key, order: itemAbove.order } + }); + + // Swap the order values + await Promise.all([ + queueService.updateQueueItem(controllerName, queueItem.key, { order: queueItem.order - 1 }), + queueService.updateQueueItem(controllerName, itemAbove.key, { order: queueItem.order }) + ]); + + console.log('Move up completed successfully'); + showSuccess('Song moved up in queue'); + } catch (error) { + console.error('Failed to move song up:', error); + showError('Failed to move song up'); + } + }, [controllerName, queueItems, showSuccess, showError]); const handleMoveDown = useCallback(async (queueItem: QueueItem) => { - // TODO: Implement move down logic - console.log('Move down:', queueItem); - }, []); + console.log('handleMoveDown called with:', queueItem); + console.log('Current queueItems:', queueItems); + console.log('Controller name:', controllerName); + + if (!controllerName || !queueItem.key || queueItem.order >= queueItems.length) { + console.log('Early return - conditions not met:', { + controllerName: !!controllerName, + queueItemKey: !!queueItem.key, + order: queueItem.order, + queueLength: queueItems.length + }); + return; // Can't move down if already at the bottom + } + + try { + // Find the item below this one + const itemBelow = queueItems.find(item => item.order === queueItem.order + 1); + console.log('Item below:', itemBelow); + + if (!itemBelow || !itemBelow.key) { + console.log('No item below found'); + showError('Cannot move item down'); + return; + } + + console.log('Swapping orders:', { + currentItem: { key: queueItem.key, order: queueItem.order }, + itemBelow: { key: itemBelow.key, order: itemBelow.order } + }); + + // Swap the order values + await Promise.all([ + queueService.updateQueueItem(controllerName, queueItem.key, { order: queueItem.order + 1 }), + queueService.updateQueueItem(controllerName, itemBelow.key, { order: queueItem.order }) + ]); + + console.log('Move down completed successfully'); + showSuccess('Song moved down in queue'); + } catch (error) { + console.error('Failed to move song down:', error); + showError('Failed to move song down'); + } + }, [controllerName, queueItems, showSuccess, showError]); return { queueItems, diff --git a/src/hooks/useSongOperations.ts b/src/hooks/useSongOperations.ts index c3f1342..2d4f982 100644 --- a/src/hooks/useSongOperations.ts +++ b/src/hooks/useSongOperations.ts @@ -15,7 +15,12 @@ export const useSongOperations = () => { } try { - const nextOrder = Object.keys(currentQueue).length + 1; + // Calculate the next order by finding the highest order value and adding 1 + const queueItems = Object.values(currentQueue); + const maxOrder = queueItems.length > 0 + ? Math.max(...queueItems.map(item => item.order || 0)) + : 0; + const nextOrder = maxOrder + 1; const queueItem: Omit = { order: nextOrder, diff --git a/src/redux/controllerSlice.ts b/src/redux/controllerSlice.ts index 0c3a51e..2a6637c 100644 --- a/src/redux/controllerSlice.ts +++ b/src/redux/controllerSlice.ts @@ -159,7 +159,19 @@ export const selectHistory = (state: { controller: ControllerState }) => state.c export const selectTopPlayed = (state: { controller: ControllerState }) => state.controller.data?.topPlayed || {}; export const selectNewSongs = (state: { controller: ControllerState }) => state.controller.data?.newSongs || {}; export const selectSongList = (state: { controller: ControllerState }) => state.controller.data?.songList || {}; -export const selectPlayerState = (state: { controller: ControllerState }) => state.controller.data?.player?.state; +export const selectPlayerState = (state: { controller: ControllerState }) => { + const playerState = state.controller.data?.player?.state; + + // Handle both structures: + // 1. player.state: "Playing" (direct string - what's actually in Firebase) + // 2. player.state: { state: "Playing" } (nested object - what types expect) + + if (typeof playerState === 'string') { + return { state: playerState }; + } + + return playerState; +}; export const selectSettings = (state: { controller: ControllerState }) => state.controller.data?.player?.settings; export const selectSingers = (state: { controller: ControllerState }) => state.controller.data?.player?.singers || {};