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

This commit is contained in:
Matt Bruce 2025-07-17 13:53:39 -05:00
parent cf78d4d4a5
commit 1c714ec341
12 changed files with 579 additions and 83 deletions

View File

@ -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<AuthInitializerProps> = ({ 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 (
<LoginPrompt
isAdmin={false}
isAdmin={isAdminMode}
onComplete={() => setShowLogin(false)}
/>
);

View File

@ -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<PlayerControlsProps> = ({ 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 (
<div className={`bg-white rounded-lg shadow p-4 ${className}`}>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<h3 className="text-lg font-medium text-gray-900">Player Controls</h3>
<span className={`px-2 py-1 text-xs rounded-full ${
currentState === PlayerState.playing
? 'bg-green-100 text-green-800'
: currentState === PlayerState.paused
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`}>
{currentState}
</span>
</div>
</div>
<div className="mt-4 flex items-center justify-center space-x-3">
{currentState === PlayerState.playing ? (
<ActionButton
onClick={handlePause}
variant="primary"
size="lg"
>
Pause
</ActionButton>
) : (
<ActionButton
onClick={handlePlay}
variant="primary"
size="lg"
disabled={!hasSongsInQueue}
>
Play
</ActionButton>
)}
{currentState !== PlayerState.stopped && (
<ActionButton
onClick={handleStop}
variant="danger"
size="sm"
>
Stop
</ActionButton>
)}
</div>
<div className="mt-3 text-xs text-gray-500 text-center">
Admin controls - Only visible to admin users
{!hasSongsInQueue && (
<div className="mt-1 text-orange-600">
Add songs to queue to enable playback controls
</div>
)}
</div>
</div>
);
};
export default PlayerControls;

View File

@ -37,9 +37,9 @@ const SongItem: React.FC<SongItemProps> = ({
case 'queue':
return (
<div className="flex gap-2">
{isAdmin && (
{isAdmin && onRemoveFromQueue && (
<ActionButton
onClick={onRemoveFromQueue || (() => {})}
onClick={onRemoveFromQueue}
variant="danger"
size="sm"
>

View File

@ -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';
export { default as PlayerControls } from './PlayerControls';

View File

@ -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<string | null>(null);
const observerRef = useRef<HTMLDivElement>(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 */}
<div className="mt-2 text-sm text-gray-500">
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}"
</div>
</div>
@ -95,7 +128,7 @@ const Artists: React.FC = () => {
{artist}
</h3>
<p className="text-sm text-gray-500">
{getSongsByArtist(artist).length} song{getSongsByArtist(artist).length !== 1 ? 's' : ''}
{getSongCountByArtist(artist)} song{getSongCountByArtist(artist) !== 1 ? 's' : ''}
</p>
</div>
<div className="flex-shrink-0 ml-4">
@ -109,14 +142,30 @@ const Artists: React.FC = () => {
</div>
</div>
))}
{/* Infinite scroll trigger */}
{hasMore && (
<div
ref={observerRef}
className="py-4 text-center text-gray-500"
>
<div className="inline-flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading more artists...
</div>
</div>
)}
</div>
)}
</div>
{/* Artist Songs Modal */}
{selectedArtist && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden">
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]" style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0 }}>
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden" style={{ backgroundColor: 'white', zIndex: 10000 }}>
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">
@ -133,19 +182,38 @@ const Artists: React.FC = () => {
</div>
<div className="overflow-y-auto max-h-[60vh]">
<InfiniteScrollList
items={selectedArtistSongs}
isLoading={false}
hasMore={false}
onLoadMore={() => {}}
onAddToQueue={handleAddToQueue}
onToggleFavorite={handleToggleFavorite}
context="search"
title=""
emptyTitle="No songs found"
emptyMessage="No songs found for this artist"
debugInfo=""
/>
<div className="divide-y divide-gray-200">
{selectedArtistSongs.map((song) => (
<div key={song.key} className="p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="text-sm font-medium text-gray-900">
{song.title}
</h3>
<p className="text-sm text-gray-500">
{song.artist}
</p>
</div>
<div className="flex-shrink-0 ml-4 flex items-center space-x-2">
<ActionButton
onClick={() => handleAddToQueue(song)}
variant="primary"
size="sm"
>
Add to Queue
</ActionButton>
<ActionButton
onClick={() => handleToggleFavorite(song)}
variant="secondary"
size="sm"
>
{song.favorite ? 'Remove from Favorites' : 'Add to Favorites'}
</ActionButton>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>

View File

@ -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 (
<div className="max-w-4xl mx-auto p-6">
@ -36,6 +45,11 @@ const Queue: React.FC = () => {
</div>
</div>
{/* Player Controls - Only visible to admin users */}
<div className="mb-6">
<PlayerControls />
</div>
{/* Queue List */}
<div className="bg-white rounded-lg shadow">
{queueCount === 0 ? (
@ -60,7 +74,9 @@ const Queue: React.FC = () => {
/>
) : (
<div className="divide-y divide-gray-200">
{queueItems.map((queueItem, index) => (
{queueItems.map((queueItem, index) => {
console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`);
return (
<div key={queueItem.key} className="flex items-center">
{/* Order Number */}
<div className="flex-shrink-0 w-12 h-12 flex items-center justify-center bg-gray-100 text-gray-600 font-medium">
@ -72,7 +88,12 @@ const Queue: React.FC = () => {
<SongItem
song={queueItem.song}
context="queue"
onRemoveFromQueue={() => 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 && (
<div className="flex-shrink-0 px-4 py-2 flex flex-col gap-1">
<ActionButton
onClick={() => handleMoveUp(queueItem)}
variant="secondary"
size="sm"
disabled={index === 0}
>
</ActionButton>
<ActionButton
onClick={() => handleMoveDown(queueItem)}
variant="secondary"
size="sm"
disabled={index === queueItems.length - 1}
>
</ActionButton>
{queueItem.order > 2 && (
<ActionButton
onClick={() => handleMoveUp(queueItem)}
variant="secondary"
size="sm"
>
</ActionButton>
)}
{queueItem.order > 1 && queueItem.order < queueItems.length && (
<ActionButton
onClick={() => handleMoveDown(queueItem)}
variant="secondary"
size="sm"
>
</ActionButton>
)}
</div>
)}
</div>
))}
);
})}
</div>
)}
</div>

View File

@ -178,7 +178,7 @@ const SongLists: React.FC = () => {
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">
TEST MODAL - {finalSelectedList.title}
{finalSelectedList.title}
</h2>
<ActionButton
onClick={handleCloseSongList}

View File

@ -49,7 +49,24 @@ export const queueService = {
// Add song to queue
addToQueue: async (controllerName: string, queueItem: Omit<QueueItem, 'key'>) => {
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<string, QueueItem | null> = {};
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<string, QueueItem>) => 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`);

View File

@ -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<string, Song[]>();
const countsMap = new Map<string, number>();
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,
};

View File

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

View File

@ -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<QueueItem, 'key'> = {
order: nextOrder,

View File

@ -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 || {};