Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
e32351b6fa
commit
eca5a8d7e1
@ -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`);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user