fixed history issues
Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
7fde4cb0cc
commit
b8e8866681
@ -10,6 +10,7 @@ import { useAppSelector } from '../../redux';
|
|||||||
import { selectSingersArray, selectControllerName, selectQueueObject } from '../../redux';
|
import { selectSingersArray, selectControllerName, selectQueueObject } from '../../redux';
|
||||||
import { queueService } from '../../firebase/services';
|
import { queueService } from '../../firebase/services';
|
||||||
import { useToast } from '../../hooks/useToast';
|
import { useToast } from '../../hooks/useToast';
|
||||||
|
import { useActions } from '../../hooks';
|
||||||
import { ModalHeader } from './ModalHeader';
|
import { ModalHeader } from './ModalHeader';
|
||||||
import { NumberDisplay } from './NumberDisplay';
|
import { NumberDisplay } from './NumberDisplay';
|
||||||
import { SongInfoDisplay } from './SongItem';
|
import { SongInfoDisplay } from './SongItem';
|
||||||
@ -29,6 +30,7 @@ const SelectSinger: React.FC<SelectSingerProps> = ({ isOpen, onClose, song }) =>
|
|||||||
const showSuccess = toast?.showSuccess;
|
const showSuccess = toast?.showSuccess;
|
||||||
const showError = toast?.showError;
|
const showError = toast?.showError;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { handleAddToQueue } = useActions();
|
||||||
|
|
||||||
const handleSelectSinger = async (singer: Singer) => {
|
const handleSelectSinger = async (singer: Singer) => {
|
||||||
if (!controllerName) {
|
if (!controllerName) {
|
||||||
@ -38,23 +40,7 @@ const SelectSinger: React.FC<SelectSingerProps> = ({ isOpen, onClose, song }) =>
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// Calculate the next order by finding the highest order value and adding 1
|
await handleAddToQueue(song, singer);
|
||||||
const queueItems = Object.values(currentQueue) as QueueItem[];
|
|
||||||
const maxOrder = queueItems.length > 0
|
|
||||||
? Math.max(...queueItems.map(item => item.order || 0))
|
|
||||||
: 0;
|
|
||||||
const nextOrder = maxOrder + 1;
|
|
||||||
|
|
||||||
const queueItem: Omit<QueueItem, 'key'> = {
|
|
||||||
order: nextOrder,
|
|
||||||
singer: {
|
|
||||||
name: singer.name,
|
|
||||||
lastLogin: singer.lastLogin || '',
|
|
||||||
},
|
|
||||||
song: song,
|
|
||||||
};
|
|
||||||
|
|
||||||
await queueService.addToQueue(controllerName, queueItem);
|
|
||||||
if (showSuccess) showSuccess(`${song.title} added to queue for ${singer.name}`);
|
if (showSuccess) showSuccess(`${song.title} added to queue for ${singer.name}`);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React, { useMemo, useCallback } from 'react';
|
|||||||
import { IonItem, IonLabel } from '@ionic/react';
|
import { IonItem, IonLabel } from '@ionic/react';
|
||||||
import ActionButton from './ActionButton';
|
import ActionButton from './ActionButton';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectQueue, selectFavorites } from '../../redux';
|
import { selectQueue, selectFavorites, selectCurrentSinger } from '../../redux';
|
||||||
import { useActions } from '../../hooks/useActions';
|
import { useActions } from '../../hooks/useActions';
|
||||||
import { useModal } from '../../hooks/useModalContext';
|
import { useModal } from '../../hooks/useModalContext';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
@ -209,6 +209,7 @@ const SongItem: React.FC<SongItemProps> = React.memo(({
|
|||||||
// Get current state from Redux
|
// Get current state from Redux
|
||||||
const queue = useAppSelector(selectQueue);
|
const queue = useAppSelector(selectQueue);
|
||||||
const favorites = useAppSelector(selectFavorites);
|
const favorites = useAppSelector(selectFavorites);
|
||||||
|
const currentSingerName = useAppSelector(selectCurrentSinger);
|
||||||
|
|
||||||
// Get unified action handlers
|
// Get unified action handlers
|
||||||
const { handleAddToQueue, handleToggleFavorite, handleRemoveFromQueue } = useActions();
|
const { handleAddToQueue, handleToggleFavorite, handleRemoveFromQueue } = useActions();
|
||||||
@ -255,8 +256,15 @@ const SongItem: React.FC<SongItemProps> = React.memo(({
|
|||||||
|
|
||||||
// Memoized handler functions for performance
|
// Memoized handler functions for performance
|
||||||
const handleAddToQueueClick = useCallback(async () => {
|
const handleAddToQueueClick = useCallback(async () => {
|
||||||
await handleAddToQueue(song);
|
// Find the current singer object from the queue or create a minimal one
|
||||||
}, [handleAddToQueue, song]);
|
let singer = undefined;
|
||||||
|
if (currentSingerName) {
|
||||||
|
// Try to find a matching singer in the queue (for lastLogin)
|
||||||
|
const queueSingers = (Object.values(queue) as QueueItem[]).map(item => item.singer);
|
||||||
|
singer = queueSingers.find(s => s.name === currentSingerName) || { name: currentSingerName, lastLogin: '' };
|
||||||
|
}
|
||||||
|
await handleAddToQueue(song, singer);
|
||||||
|
}, [handleAddToQueue, song, currentSingerName, queue]);
|
||||||
|
|
||||||
const handleToggleFavoriteClick = useCallback(async () => {
|
const handleToggleFavoriteClick = useCallback(async () => {
|
||||||
await handleToggleFavorite(song);
|
await handleToggleFavorite(song);
|
||||||
|
|||||||
@ -279,51 +279,86 @@ export const playerService = {
|
|||||||
|
|
||||||
// History operations
|
// History operations
|
||||||
export const historyService = {
|
export const historyService = {
|
||||||
// Add song to history
|
// Add song to history (by path, with count)
|
||||||
addToHistory: async (controllerName: string, song: Omit<Song, 'key'>) => {
|
addToHistory: async (controllerName: string, song: Omit<Song, 'key'>) => {
|
||||||
const historyRef = ref(database, `controllers/${controllerName}/history`);
|
const historyRef = ref(database, `controllers/${controllerName}/history`);
|
||||||
const historySnapshot = await get(historyRef);
|
const historySnapshot = await get(historyRef);
|
||||||
const currentHistory = historySnapshot.exists() ? historySnapshot.val() : {};
|
const currentHistory = historySnapshot.exists() ? historySnapshot.val() : {};
|
||||||
|
const now = Date.now();
|
||||||
// Find the next available sequential key
|
// Find if song with same path exists
|
||||||
const nextKey = findNextSequentialKey(Object.keys(currentHistory));
|
const existingEntry = Object.entries(currentHistory).find(
|
||||||
debugLog('addToHistory - existing keys:', Object.keys(currentHistory));
|
([, item]) => typeof item === 'object' && item !== null && 'path' in item && (item as { path: string }).path === song.path
|
||||||
debugLog('addToHistory - next key:', nextKey);
|
);
|
||||||
|
if (existingEntry) {
|
||||||
const newHistoryRef = ref(database, `controllers/${controllerName}/history/${nextKey}`);
|
const [key, item] = existingEntry;
|
||||||
await set(newHistoryRef, song);
|
await update(ref(database, `controllers/${controllerName}/history/${key}`), {
|
||||||
return { key: nextKey.toString() };
|
count: (item.count || 1) + 1,
|
||||||
|
lastPlayed: now,
|
||||||
|
});
|
||||||
|
// Move this entry to the most recent by updating lastPlayed
|
||||||
|
// (No need to reorder keys, just use lastPlayed for recency)
|
||||||
|
// Cap size after update
|
||||||
|
} else {
|
||||||
|
// Add new entry with count: 1 and lastPlayed
|
||||||
|
const nextKey = findNextSequentialKey(Object.keys(currentHistory));
|
||||||
|
await set(ref(database, `controllers/${controllerName}/history/${nextKey}`), {
|
||||||
|
...song,
|
||||||
|
count: 1,
|
||||||
|
lastPlayed: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Cap history size (remove oldest by lastPlayed if over 250)
|
||||||
|
const updatedSnapshot = await get(historyRef);
|
||||||
|
const updatedHistory = updatedSnapshot.exists() ? updatedSnapshot.val() : {};
|
||||||
|
const entries = Object.entries(updatedHistory);
|
||||||
|
if (entries.length > 250) {
|
||||||
|
// Find the oldest entry by lastPlayed using a for loop for type safety
|
||||||
|
let oldestKey: string | null = null;
|
||||||
|
let oldestTime: number | null = null;
|
||||||
|
for (const [key, item] of entries) {
|
||||||
|
if (typeof item === 'object' && item !== null && 'lastPlayed' in item && typeof (item as { lastPlayed?: number }).lastPlayed === 'number') {
|
||||||
|
const lastPlayed = (item as { lastPlayed: number }).lastPlayed;
|
||||||
|
if (oldestTime === null || lastPlayed < oldestTime) {
|
||||||
|
oldestTime = lastPlayed;
|
||||||
|
oldestKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldestKey) {
|
||||||
|
await remove(ref(database, `controllers/${controllerName}/history/${oldestKey}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Remove song from history
|
// Remove song from history (by path, with count logic)
|
||||||
removeFromHistory: async (controllerName: string, historyItemKey: string) => {
|
removeFromHistory: async (controllerName: string, songPath: string) => {
|
||||||
const historyRef = ref(database, `controllers/${controllerName}/history`);
|
const historyRef = ref(database, `controllers/${controllerName}/history`);
|
||||||
const historySnapshot = await get(historyRef);
|
const historySnapshot = await get(historyRef);
|
||||||
|
|
||||||
if (!historySnapshot.exists()) {
|
if (!historySnapshot.exists()) {
|
||||||
throw new Error('History not found');
|
throw new Error('History not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = historySnapshot.val();
|
const history = historySnapshot.val();
|
||||||
debugLog('removeFromHistory - original history:', history);
|
// Find entry by path
|
||||||
|
const existingEntry = Object.entries(history).find(
|
||||||
// Find the item to remove and get its key
|
([, item]) => typeof item === 'object' && item !== null && 'path' in item && (item as { path: string }).path === songPath
|
||||||
const itemToRemove = Object.entries(history).find(([key, item]) =>
|
|
||||||
key === historyItemKey && item
|
|
||||||
);
|
);
|
||||||
|
if (!existingEntry) {
|
||||||
if (!itemToRemove) {
|
|
||||||
throw new Error('History item not found');
|
throw new Error('History item not found');
|
||||||
}
|
}
|
||||||
|
const [key, item] = existingEntry;
|
||||||
const [removedKey, removedItem] = itemToRemove;
|
let count = 1;
|
||||||
debugLog('removeFromHistory - removing item:', removedItem, 'with key:', removedKey);
|
if (typeof item === 'object' && item !== null && 'count' in item) {
|
||||||
|
count = (item as { count?: number }).count ?? 1;
|
||||||
// Use utility function to create shift-down updates
|
}
|
||||||
const updates = shiftDownAfterDeletion(history, removedKey, 'history item');
|
const now = Date.now();
|
||||||
|
if (count > 1) {
|
||||||
// Apply all updates atomically
|
await update(ref(database, `controllers/${controllerName}/history/${key}`), {
|
||||||
await update(historyRef, updates);
|
count: count - 1,
|
||||||
|
lastPlayed: now,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await remove(ref(database, `controllers/${controllerName}/history/${key}`));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Listen to history changes
|
// Listen to history changes
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import { useState, useCallback } from 'react';
|
|||||||
import { useAppSelector, useAppDispatch } 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 { reorderQueueAsync } from '../redux/queueSlice';
|
||||||
|
import { addToQueue as addToQueueThunk } 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 { 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, Singer } from '../types';
|
||||||
|
|
||||||
export type QueueMode = 'delete' | 'reorder';
|
export type QueueMode = 'delete' | 'reorder';
|
||||||
|
|
||||||
@ -31,14 +32,56 @@ export const useActions = () => {
|
|||||||
const canDeleteFirstItem = isAdmin && (playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused); // Only allow deleting first item if not playing
|
const canDeleteFirstItem = isAdmin && (playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused); // Only allow deleting first item if not playing
|
||||||
|
|
||||||
// Song operations
|
// Song operations
|
||||||
const handleAddToQueue = useCallback(async (song: Song) => {
|
const handleAddToQueue = useCallback(async (song: Song, singerOverride?: Singer) => {
|
||||||
try {
|
try {
|
||||||
await addToQueue(song);
|
// If a singer is provided, use it; otherwise, use the current singer from state
|
||||||
|
let singer = singerOverride;
|
||||||
|
if (!singer) {
|
||||||
|
// Try to get from Redux state
|
||||||
|
const state = (window as unknown as { store?: { getState?: () => unknown } }).store?.getState?.();
|
||||||
|
if (state && typeof state === 'object' && 'auth' in state) {
|
||||||
|
const authState = (state as { auth?: { data?: { singer?: Singer } } }).auth;
|
||||||
|
if (authState && authState.data && authState.data.singer) {
|
||||||
|
singer = authState.data.singer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!singer) throw new Error('No singer specified');
|
||||||
|
// Calculate order
|
||||||
|
const state = (window as unknown as { store?: { getState?: () => unknown } }).store?.getState?.();
|
||||||
|
let queueItems: Array<QueueItem & { key: string }> = [];
|
||||||
|
if (state && typeof state === 'object' && 'queue' in state) {
|
||||||
|
const queueState = (state as { queue?: { data?: Record<string, QueueItem> } }).queue;
|
||||||
|
if (queueState && queueState.data && typeof queueState.data === 'object') {
|
||||||
|
queueItems = Object.entries(queueState.data).map(([key, item]) => ({ ...item, key }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const maxOrder = queueItems.length > 0
|
||||||
|
? Math.max(...queueItems.map(item => item.order || 0))
|
||||||
|
: 0;
|
||||||
|
const nextOrder = maxOrder + 1;
|
||||||
|
const queueItem: Omit<QueueItem, 'key'> = {
|
||||||
|
order: nextOrder,
|
||||||
|
singer: {
|
||||||
|
name: singer.name,
|
||||||
|
lastLogin: singer.lastLogin || '',
|
||||||
|
},
|
||||||
|
song: song,
|
||||||
|
};
|
||||||
|
await dispatch(addToQueueThunk({ controllerName, queueItem })).unwrap();
|
||||||
|
if (controllerName) {
|
||||||
|
try {
|
||||||
|
await historyService.addToHistory(controllerName, song);
|
||||||
|
if (showSuccess) showSuccess('Song added to history');
|
||||||
|
} catch {
|
||||||
|
if (showError) showError('Failed to add song to history');
|
||||||
|
}
|
||||||
|
}
|
||||||
if (showSuccess) showSuccess('Song added to queue');
|
if (showSuccess) showSuccess('Song added to queue');
|
||||||
} catch {
|
} catch {
|
||||||
if (showError) showError('Failed to add song to queue');
|
if (showError) showError('Failed to add song to queue');
|
||||||
}
|
}
|
||||||
}, [addToQueue, showSuccess, showError]);
|
}, [addToQueue, showSuccess, showError, controllerName]);
|
||||||
|
|
||||||
// Utility to fix queue order after deletes
|
// Utility to fix queue order after deletes
|
||||||
const fixQueueOrder = useCallback(async () => {
|
const fixQueueOrder = useCallback(async () => {
|
||||||
@ -69,13 +112,21 @@ export const useActions = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await removeFromQueue(queueItem.key);
|
await removeFromQueue(queueItem.key);
|
||||||
|
if (controllerName && queueItem.song && queueItem.song.path) {
|
||||||
|
try {
|
||||||
|
await historyService.removeFromHistory(controllerName, queueItem.song.path);
|
||||||
|
if (showSuccess) showSuccess('Song removed from history');
|
||||||
|
} catch {
|
||||||
|
if (showError) showError('Failed to remove song from history');
|
||||||
|
}
|
||||||
|
}
|
||||||
if (showSuccess) showSuccess('Song removed from queue');
|
if (showSuccess) showSuccess('Song removed from queue');
|
||||||
// After removal, fix the order of all items
|
// After removal, fix the order of all items
|
||||||
await fixQueueOrder();
|
await fixQueueOrder();
|
||||||
} catch {
|
} catch {
|
||||||
if (showError) showError('Failed to remove song from queue');
|
if (showError) showError('Failed to remove song from queue');
|
||||||
}
|
}
|
||||||
}, [removeFromQueue, showSuccess, showError, fixQueueOrder]);
|
}, [removeFromQueue, showSuccess, showError, fixQueueOrder, controllerName]);
|
||||||
|
|
||||||
const handleToggleFavorite = useCallback(async (song: Song) => {
|
const handleToggleFavorite = useCallback(async (song: Song) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user