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 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<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
@ -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<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)
cleanupQueueKeys: async (controllerName: string) => {
const queueRef = ref(database, `controllers/${controllerName}/player/queue`);

View File

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

View File

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

View File

@ -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<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 }>) => {
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,