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 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`);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user