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

This commit is contained in:
mbrucedogs 2025-07-21 19:06:07 -05:00
parent e32351b6fa
commit eca5a8d7e1
4 changed files with 154 additions and 32 deletions

View File

@ -54,13 +54,24 @@ export const queueService = {
const snapshot = await get(queueRef); const snapshot = await get(queueRef);
const currentQueue = snapshot.exists() ? snapshot.val() : {}; 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) const existingKeys = Object.keys(currentQueue)
.filter(key => /^\d+$/.test(key)) // Only consider numerical keys .filter(key => /^\d+$/.test(key)) // Only consider numerical keys
.map(key => parseInt(key, 10)) .map(key => parseInt(key, 10))
.sort((a, b) => a - b); .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 // Add the item with the sequential key
const newItemRef = ref(database, `controllers/${controllerName}/player/queue/${nextKey}`); const newItemRef = ref(database, `controllers/${controllerName}/player/queue/${nextKey}`);
@ -71,8 +82,44 @@ export const queueService = {
// Remove song from queue // Remove song from queue
removeFromQueue: async (controllerName: string, queueItemKey: string) => { removeFromQueue: async (controllerName: string, queueItemKey: string) => {
const queueItemRef = ref(database, `controllers/${controllerName}/player/queue/${queueItemKey}`); const queueRef = ref(database, `controllers/${controllerName}/player/queue`);
await remove(queueItemRef); 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<string, QueueItem | null> = {};
// 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 // Update queue item
@ -81,6 +128,43 @@ export const queueService = {
await update(queueItemRef, updates); 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<string, QueueItem | null> = {};
// 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) // Clean up queue with inconsistent keys (migrate push ID keys to sequential numerical keys)
cleanupQueueKeys: async (controllerName: string) => { cleanupQueueKeys: async (controllerName: string) => {
const queueRef = ref(database, `controllers/${controllerName}/player/queue`); const queueRef = ref(database, `controllers/${controllerName}/player/queue`);

View File

@ -1,10 +1,11 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useAppSelector } from '../redux'; import { useAppSelector, useAppDispatch } from '../redux';
import { selectControllerName, selectPlayerStateMemoized, selectIsAdmin } from '../redux'; import { selectControllerName, selectPlayerStateMemoized, selectIsAdmin } from '../redux';
import { reorderQueueAsync } from '../redux/queueSlice';
import { useSongOperations } from './useSongOperations'; import { useSongOperations } from './useSongOperations';
import { useToast } from './useToast'; import { useToast } from './useToast';
import { useDisabledSongs } from './useDisabledSongs'; import { useDisabledSongs } from './useDisabledSongs';
import { queueService, historyService } from '../firebase/services'; import { historyService } from '../firebase/services';
import { debugLog } from '../utils/logger'; import { debugLog } from '../utils/logger';
import { PlayerState } from '../types'; import { PlayerState } from '../types';
import type { Song, QueueItem } from '../types'; import type { Song, QueueItem } from '../types';
@ -17,6 +18,7 @@ export const useActions = () => {
const controllerName = useAppSelector(selectControllerName); const controllerName = useAppSelector(selectControllerName);
const playerState = useAppSelector(selectPlayerStateMemoized); const playerState = useAppSelector(selectPlayerStateMemoized);
const isAdmin = useAppSelector(selectIsAdmin); const isAdmin = useAppSelector(selectIsAdmin);
const dispatch = useAppDispatch();
const { addToQueue, removeFromQueue, toggleFavorite } = useSongOperations(); const { addToQueue, removeFromQueue, toggleFavorite } = useSongOperations();
const toast = useToast(); const toast = useToast();
const showSuccess = toast?.showSuccess; const showSuccess = toast?.showSuccess;
@ -113,24 +115,15 @@ export const useActions = () => {
const newQueueItems = [queueItems[0], ...copy]; const newQueueItems = [queueItems[0], ...copy];
debugLog('New queue order:', newQueueItems); debugLog('New queue order:', newQueueItems);
// Update all items with their new order values // Use the Redux thunk for reordering
const updatePromises = newQueueItems.map((item, index) => { await dispatch(reorderQueueAsync({ controllerName, newOrder: newQueueItems })).unwrap();
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'); debugLog('Queue reorder completed successfully');
if (showSuccess) showSuccess('Queue reordered successfully'); if (showSuccess) showSuccess('Queue reordered successfully');
} catch (error) { } catch (error) {
console.error('Failed to reorder queue:', error); console.error('Failed to reorder queue:', error);
if (showError) showError('Failed to reorder queue'); if (showError) showError('Failed to reorder queue');
} }
}, [controllerName, showSuccess, showError]); }, [controllerName, dispatch, showSuccess, showError]);
return { return {
// Song operations // Song operations

View File

@ -1,17 +1,19 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useAppSelector } from '../redux'; import { useAppSelector, useAppDispatch } from '../redux';
import { selectControllerName, selectCurrentSinger, selectQueueObject } from '../redux'; import { selectControllerName, selectCurrentSinger, selectQueue } from '../redux';
import { queueService, favoritesService } from '../firebase/services'; 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 { ref, get } from 'firebase/database';
import { database } from '../firebase/config'; import { database } from '../firebase/config';
import { debugLog } from '../utils/logger';
import { useErrorHandler } from './index';
import type { Song, QueueItem } from '../types'; import type { Song, QueueItem } from '../types';
export const useSongOperations = () => { export const useSongOperations = () => {
const controllerName = useAppSelector(selectControllerName); const controllerName = useAppSelector(selectControllerName);
const currentSinger = useAppSelector(selectCurrentSinger); const currentSinger = useAppSelector(selectCurrentSinger);
const currentQueue = useAppSelector(selectQueueObject); const currentQueue = useAppSelector(selectQueue);
const dispatch = useAppDispatch();
const { handleFirebaseError } = useErrorHandler({ context: 'useSongOperations' }); const { handleFirebaseError } = useErrorHandler({ context: 'useSongOperations' });
const addToQueue = useCallback(async (song: Song) => { const addToQueue = useCallback(async (song: Song) => {
@ -41,12 +43,12 @@ export const useSongOperations = () => {
song, song,
}; };
await queueService.addToQueue(controllerName, queueItem); await dispatch(addToQueueThunk({ controllerName, queueItem })).unwrap();
} catch (error) { } catch (error) {
handleFirebaseError(error, 'add song to queue'); handleFirebaseError(error, 'add song to queue');
throw error; throw error;
} }
}, [controllerName, currentSinger, currentQueue, handleFirebaseError]); }, [controllerName, currentSinger, currentQueue, dispatch, handleFirebaseError]);
const removeFromQueue = useCallback(async (queueItemKey: string) => { const removeFromQueue = useCallback(async (queueItemKey: string) => {
if (!controllerName) { if (!controllerName) {
@ -54,12 +56,12 @@ export const useSongOperations = () => {
} }
try { try {
await queueService.removeFromQueue(controllerName, queueItemKey); await dispatch(removeFromQueueThunk({ controllerName, queueItemKey })).unwrap();
} catch (error) { } catch (error) {
handleFirebaseError(error, 'remove song from queue'); handleFirebaseError(error, 'remove song from queue');
throw error; throw error;
} }
}, [controllerName, handleFirebaseError]); }, [controllerName, dispatch, handleFirebaseError]);
const toggleFavorite = useCallback(async (song: Song) => { const toggleFavorite = useCallback(async (song: Song) => {
if (!controllerName) { if (!controllerName) {

View File

@ -23,8 +23,16 @@ export const addToQueue = createAsyncThunk(
export const removeFromQueue = createAsyncThunk( export const removeFromQueue = createAsyncThunk(
'queue/removeFromQueue', 'queue/removeFromQueue',
async ({ controllerName, queueItemKey }: { controllerName: string; queueItemKey: string }) => { async ({ controllerName, queueItemKey }: { controllerName: string; queueItemKey: string }) => {
await queueService.removeFromQueue(controllerName, queueItemKey); const result = await queueService.removeFromQueue(controllerName, queueItemKey);
return 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(); state.lastUpdated = Date.now();
}, },
updateQueueItemsBulk: (state, action: PayloadAction<Record<string, QueueItem | null>>) => {
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 }>) => { reorderQueue: (state, action: PayloadAction<{ fromIndex: number; toIndex: number }>) => {
const { fromIndex, toIndex } = action.payload; const { fromIndex, toIndex } = action.payload;
const items = Object.values(state.data).sort((a, b) => a.order - b.order); 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) => { .addCase(removeFromQueue.fulfilled, (state, action) => {
state.loading = false; state.loading = false;
const key = action.payload; const { key } = action.payload;
delete state.data[key]; 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.lastUpdated = Date.now();
state.error = null; state.error = null;
}) })
@ -180,6 +203,25 @@ const queueSlice = createSlice({
.addCase(updateQueueItem.rejected, (state, action) => { .addCase(updateQueueItem.rejected, (state, action) => {
state.loading = false; state.loading = false;
state.error = action.error.message || 'Failed to update queue item'; 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, addQueueItem,
updateQueueItemSync, updateQueueItemSync,
removeQueueItem, removeQueueItem,
updateQueueItemsBulk,
reorderQueue, reorderQueue,
clearError, clearError,
resetQueue, resetQueue,