From eca5a8d7e1b2c76fa234c540d81d9bd7c5e39b69 Mon Sep 17 00:00:00 2001 From: mbrucedogs Date: Mon, 21 Jul 2025 19:06:07 -0500 Subject: [PATCH] Signed-off-by: mbrucedogs --- src/firebase/services.ts | 92 ++++++++++++++++++++++++++++++++-- src/hooks/useActions.ts | 21 +++----- src/hooks/useSongOperations.ts | 22 ++++---- src/redux/queueSlice.ts | 51 +++++++++++++++++-- 4 files changed, 154 insertions(+), 32 deletions(-) diff --git a/src/firebase/services.ts b/src/firebase/services.ts index 83dbeaf..fe04f80 100644 --- a/src/firebase/services.ts +++ b/src/firebase/services.ts @@ -54,13 +54,24 @@ export const queueService = { const snapshot = await get(queueRef); const currentQueue = snapshot.exists() ? snapshot.val() : {}; - // Find the next available numerical key + // Find the next available sequential key (0, 1, 2, etc.) 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; + // Find the first gap in the sequence, or use the next number after the highest + let nextKey = 0; + for (let i = 0; i < existingKeys.length; i++) { + if (existingKeys[i] !== i) { + nextKey = i; + break; + } + nextKey = i + 1; + } + + debugLog('addToQueue - existing keys:', existingKeys); + debugLog('addToQueue - next key:', nextKey); // Add the item with the sequential key const newItemRef = ref(database, `controllers/${controllerName}/player/queue/${nextKey}`); @@ -71,8 +82,44 @@ export const queueService = { // Remove song from queue removeFromQueue: async (controllerName: string, queueItemKey: string) => { - const queueItemRef = ref(database, `controllers/${controllerName}/player/queue/${queueItemKey}`); - await remove(queueItemRef); + const queueRef = ref(database, `controllers/${controllerName}/player/queue`); + const snapshot = await get(queueRef); + + if (!snapshot.exists()) return { updates: {} }; + + const queue = snapshot.val(); + debugLog('removeFromQueue - original queue:', queue); + + // Get all remaining items sorted by order + const remainingItems = Object.entries(queue) + .filter(([key]) => key !== queueItemKey) + .map(([key, item]) => ({ key, item: item as QueueItem })) + .sort((a, b) => a.item.order - b.item.order); + + debugLog('removeFromQueue - remaining items:', remainingItems); + + // Create a completely new queue with sequential keys + const updates: Record = {}; + + // First, remove all existing items + Object.keys(queue).forEach(key => { + updates[key] = null; + }); + + // Then, add back the remaining items with sequential keys and order + remainingItems.forEach(({ item }, index) => { + const newKey = index.toString(); + const newOrder = index + 1; + debugLog(`removeFromQueue - reindexing: old key ${item.key} -> new key ${newKey}, order ${item.order} -> ${newOrder}`); + updates[newKey] = { ...item, order: newOrder }; + }); + + debugLog('removeFromQueue - updates to apply:', updates); + + // Apply all updates atomically + await update(queueRef, updates); + + return { updates }; }, // Update queue item @@ -81,6 +128,43 @@ export const queueService = { await update(queueItemRef, updates); }, + // Reorder queue with zero-bound sequential ordering + reorderQueue: async (controllerName: string, newOrder: QueueItem[]) => { + const queueRef = ref(database, `controllers/${controllerName}/player/queue`); + const snapshot = await get(queueRef); + + if (!snapshot.exists()) return { updates: {} }; + + const queue = snapshot.val(); + debugLog('reorderQueue - original queue:', queue); + debugLog('reorderQueue - new order:', newOrder); + + // Create a completely new queue with sequential keys + const updates: Record = {}; + + // First, remove all existing items + Object.keys(queue).forEach(key => { + updates[key] = null; + }); + + // Then, add back the items in the new order with sequential keys + newOrder.forEach((item, index) => { + const newKey = index.toString(); + const newOrder = index + 1; + debugLog(`reorderQueue - reindexing: old key ${item.key} -> new key ${newKey}, order ${item.order} -> ${newOrder}`); + updates[newKey] = { ...item, order: newOrder }; + }); + + debugLog('reorderQueue - updates to apply:', updates); + + // Apply all updates atomically + if (Object.keys(updates).length > 0) { + await update(queueRef, updates); + } + + return { 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`); diff --git a/src/hooks/useActions.ts b/src/hooks/useActions.ts index 3d6f7fe..c3f4904 100644 --- a/src/hooks/useActions.ts +++ b/src/hooks/useActions.ts @@ -1,10 +1,11 @@ import { useState, useCallback } from 'react'; -import { useAppSelector } from '../redux'; +import { useAppSelector, useAppDispatch } from '../redux'; import { selectControllerName, selectPlayerStateMemoized, selectIsAdmin } from '../redux'; +import { reorderQueueAsync } from '../redux/queueSlice'; import { useSongOperations } from './useSongOperations'; import { useToast } from './useToast'; import { useDisabledSongs } from './useDisabledSongs'; -import { queueService, historyService } from '../firebase/services'; +import { historyService } from '../firebase/services'; import { debugLog } from '../utils/logger'; import { PlayerState } from '../types'; import type { Song, QueueItem } from '../types'; @@ -17,6 +18,7 @@ export const useActions = () => { const controllerName = useAppSelector(selectControllerName); const playerState = useAppSelector(selectPlayerStateMemoized); const isAdmin = useAppSelector(selectIsAdmin); + const dispatch = useAppDispatch(); const { addToQueue, removeFromQueue, toggleFavorite } = useSongOperations(); const toast = useToast(); const showSuccess = toast?.showSuccess; @@ -113,24 +115,15 @@ export const useActions = () => { 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); + // Use the Redux thunk for reordering + await dispatch(reorderQueueAsync({ controllerName, newOrder: newQueueItems })).unwrap(); debugLog('Queue reorder completed successfully'); if (showSuccess) showSuccess('Queue reordered successfully'); } catch (error) { console.error('Failed to reorder queue:', error); if (showError) showError('Failed to reorder queue'); } - }, [controllerName, showSuccess, showError]); + }, [controllerName, dispatch, showSuccess, showError]); return { // Song operations diff --git a/src/hooks/useSongOperations.ts b/src/hooks/useSongOperations.ts index 8d5e08f..75d442e 100644 --- a/src/hooks/useSongOperations.ts +++ b/src/hooks/useSongOperations.ts @@ -1,17 +1,19 @@ import { useCallback } from 'react'; -import { useAppSelector } from '../redux'; -import { selectControllerName, selectCurrentSinger, selectQueueObject } from '../redux'; -import { queueService, favoritesService } from '../firebase/services'; +import { useAppSelector, useAppDispatch } from '../redux'; +import { selectControllerName, selectCurrentSinger, selectQueue } from '../redux'; +import { addToQueue as addToQueueThunk, removeFromQueue as removeFromQueueThunk } from '../redux/queueSlice'; +import { useErrorHandler } from './useErrorHandler'; +import { favoritesService } from '../firebase/services'; +import { debugLog } from '../utils/logger'; import { ref, get } from 'firebase/database'; import { database } from '../firebase/config'; -import { debugLog } from '../utils/logger'; -import { useErrorHandler } from './index'; import type { Song, QueueItem } from '../types'; export const useSongOperations = () => { const controllerName = useAppSelector(selectControllerName); const currentSinger = useAppSelector(selectCurrentSinger); - const currentQueue = useAppSelector(selectQueueObject); + const currentQueue = useAppSelector(selectQueue); + const dispatch = useAppDispatch(); const { handleFirebaseError } = useErrorHandler({ context: 'useSongOperations' }); const addToQueue = useCallback(async (song: Song) => { @@ -41,12 +43,12 @@ export const useSongOperations = () => { song, }; - await queueService.addToQueue(controllerName, queueItem); + await dispatch(addToQueueThunk({ controllerName, queueItem })).unwrap(); } catch (error) { handleFirebaseError(error, 'add song to queue'); throw error; } - }, [controllerName, currentSinger, currentQueue, handleFirebaseError]); + }, [controllerName, currentSinger, currentQueue, dispatch, handleFirebaseError]); const removeFromQueue = useCallback(async (queueItemKey: string) => { if (!controllerName) { @@ -54,12 +56,12 @@ export const useSongOperations = () => { } try { - await queueService.removeFromQueue(controllerName, queueItemKey); + await dispatch(removeFromQueueThunk({ controllerName, queueItemKey })).unwrap(); } catch (error) { handleFirebaseError(error, 'remove song from queue'); throw error; } - }, [controllerName, handleFirebaseError]); + }, [controllerName, dispatch, handleFirebaseError]); const toggleFavorite = useCallback(async (song: Song) => { if (!controllerName) { diff --git a/src/redux/queueSlice.ts b/src/redux/queueSlice.ts index 59795e1..d930c6d 100644 --- a/src/redux/queueSlice.ts +++ b/src/redux/queueSlice.ts @@ -23,8 +23,16 @@ export const addToQueue = createAsyncThunk( export const removeFromQueue = createAsyncThunk( 'queue/removeFromQueue', async ({ controllerName, queueItemKey }: { controllerName: string; queueItemKey: string }) => { - await queueService.removeFromQueue(controllerName, queueItemKey); - return queueItemKey; + const result = await queueService.removeFromQueue(controllerName, queueItemKey); + return { key: queueItemKey, updates: result.updates }; + } +); + +export const reorderQueueAsync = createAsyncThunk( + 'queue/reorderQueueAsync', + async ({ controllerName, newOrder }: { controllerName: string; newOrder: QueueItem[] }) => { + const result = await queueService.reorderQueue(controllerName, newOrder); + return { updates: result.updates }; } ); @@ -83,6 +91,18 @@ const queueSlice = createSlice({ state.lastUpdated = Date.now(); }, + updateQueueItemsBulk: (state, action: PayloadAction>) => { + const updates = action.payload; + Object.entries(updates).forEach(([key, item]) => { + if (item === null) { + delete state.data[key]; + } else { + state.data[key] = item; + } + }); + state.lastUpdated = Date.now(); + }, + reorderQueue: (state, action: PayloadAction<{ fromIndex: number; toIndex: number }>) => { const { fromIndex, toIndex } = action.payload; const items = Object.values(state.data).sort((a, b) => a.order - b.order); @@ -154,8 +174,11 @@ const queueSlice = createSlice({ }) .addCase(removeFromQueue.fulfilled, (state, action) => { state.loading = false; - const key = action.payload; - delete state.data[key]; + const { key } = action.payload; + console.log('removeFromQueue.fulfilled - removing key:', key); + + // Clear the queue state - the real-time sync will update it with the new data + state.data = {}; state.lastUpdated = Date.now(); state.error = null; }) @@ -180,6 +203,25 @@ const queueSlice = createSlice({ .addCase(updateQueueItem.rejected, (state, action) => { state.loading = false; state.error = action.error.message || 'Failed to update queue item'; + }) + // reorderQueueAsync + .addCase(reorderQueueAsync.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(reorderQueueAsync.fulfilled, (state, action) => { + state.loading = false; + const { updates } = action.payload; + console.log('reorderQueueAsync.fulfilled - updates:', updates); + + // Clear the queue state - the real-time sync will update it with the new data + state.data = {}; + state.lastUpdated = Date.now(); + state.error = null; + }) + .addCase(reorderQueueAsync.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message || 'Failed to reorder queue'; }); }, }); @@ -190,6 +232,7 @@ export const { addQueueItem, updateQueueItemSync, removeQueueItem, + updateQueueItemsBulk, reorderQueue, clearError, resetQueue,