Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
0b383504ae
commit
1918185841
@ -1,26 +1,31 @@
|
||||
import React from 'react';
|
||||
import { IonItem, IonItemSliding, IonItemOptions, IonItemOption, IonIcon, IonLabel } from '@ionic/react';
|
||||
import { trash, arrowUp, arrowDown } from 'ionicons/icons';
|
||||
import { ActionButton, InfiniteScrollList, PageHeader } from '../../components/common';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IonButton, IonIcon, IonReorderGroup, IonReorder, IonItem, IonLabel, IonItemSliding, IonItemOptions, IonItemOption } from '@ionic/react';
|
||||
import { trash, reorderThreeOutline, reorderTwoOutline, playCircle } from 'ionicons/icons';
|
||||
import { PageHeader, ActionButton } from '../../components/common';
|
||||
import { useQueue } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectQueue, selectPlayerState, selectIsAdmin } from '../../redux';
|
||||
import { selectQueue, selectPlayerState, selectIsAdmin, selectControllerName } from '../../redux';
|
||||
import { PlayerState } from '../../types';
|
||||
import { queueService } from '../../firebase/services';
|
||||
import type { QueueItem } from '../../types';
|
||||
|
||||
type QueueMode = 'delete' | 'reorder';
|
||||
|
||||
const Queue: React.FC = () => {
|
||||
const [queueMode, setQueueMode] = useState<QueueMode>('delete');
|
||||
const [listItems, setListItems] = useState<QueueItem[]>([]);
|
||||
|
||||
const {
|
||||
queueItems,
|
||||
queueStats,
|
||||
canReorder,
|
||||
handleRemoveFromQueue,
|
||||
handleMoveUp,
|
||||
handleMoveDown,
|
||||
} = useQueue();
|
||||
|
||||
const queue = useAppSelector(selectQueue);
|
||||
const playerState = useAppSelector(selectPlayerState);
|
||||
const isAdmin = useAppSelector(selectIsAdmin);
|
||||
const controllerName = useAppSelector(selectControllerName);
|
||||
const queueCount = Object.keys(queue).length;
|
||||
|
||||
// Debug logging
|
||||
@ -29,6 +34,7 @@ const Queue: React.FC = () => {
|
||||
console.log('Queue component - player state:', playerState);
|
||||
console.log('Queue component - isAdmin:', isAdmin);
|
||||
console.log('Queue component - canReorder:', canReorder);
|
||||
console.log('Queue component - queueMode:', queueMode);
|
||||
|
||||
// Check if items can be deleted (admin can delete any item when not playing)
|
||||
const canDeleteItems = isAdmin && (playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused);
|
||||
@ -36,25 +42,81 @@ const Queue: React.FC = () => {
|
||||
console.log('Queue component - canDeleteItems:', canDeleteItems);
|
||||
console.log('Queue component - canReorder:', canReorder);
|
||||
|
||||
// Render queue item for InfiniteScrollList
|
||||
// Determine if currently playing
|
||||
const isPlaying = playerState?.state === PlayerState.playing;
|
||||
|
||||
// Update list items when queue changes
|
||||
useEffect(() => {
|
||||
if (queueItems.length > 0) {
|
||||
// Skip the first item (currently playing) for reordering
|
||||
setListItems(queueItems.slice(1));
|
||||
} else {
|
||||
setListItems([]);
|
||||
}
|
||||
}, [queueItems]);
|
||||
|
||||
// Toggle between modes
|
||||
const toggleQueueMode = () => {
|
||||
setQueueMode(prevMode => prevMode === 'delete' ? 'reorder' : 'delete');
|
||||
};
|
||||
|
||||
// Handle reorder event from IonReorderGroup
|
||||
const doReorder = async (event: CustomEvent) => {
|
||||
console.log('Reorder event:', event.detail);
|
||||
const { from, to, complete } = event.detail;
|
||||
|
||||
if (listItems && controllerName) {
|
||||
const copy = [...listItems];
|
||||
const draggedItem = copy.splice(from, 1)[0];
|
||||
copy.splice(to, 0, draggedItem);
|
||||
|
||||
// Complete the reorder animation
|
||||
complete();
|
||||
|
||||
// Create the new queue order (first item + reordered items)
|
||||
const newQueueItems = [queueItems[0], ...copy];
|
||||
console.log('New queue order:', newQueueItems);
|
||||
|
||||
try {
|
||||
// Update all items with their new order values
|
||||
const updatePromises = newQueueItems.map((item, index) => {
|
||||
const newOrder = index + 1;
|
||||
if (item.key && item.order !== newOrder) {
|
||||
console.log(`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);
|
||||
console.log('Queue reorder completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to reorder queue:', error);
|
||||
// You might want to show an error toast here
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Render queue item
|
||||
const renderQueueItem = (queueItem: QueueItem, index: number) => {
|
||||
console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`);
|
||||
const canDelete = isAdmin; // Allow admin to delete any item
|
||||
const canDelete = isAdmin && queueMode === 'delete'; // Only allow delete in delete mode
|
||||
const isFirstItem = index === 0;
|
||||
|
||||
return (
|
||||
<IonItemSliding key={queueItem.key}>
|
||||
<IonItem>
|
||||
<IonItem className={`${canReorder && queueMode === 'reorder' ? 'border-l-4 border-blue-200 bg-blue-50' : ''}`}>
|
||||
{/* Order Number */}
|
||||
<div slot="start" className="flex-shrink-0 w-12 h-12 flex items-center justify-center bg-gray-100 text-gray-600 font-medium rounded-full">
|
||||
<div slot="start" className={`relative flex-shrink-0 w-12 h-12 flex items-center justify-center font-medium rounded-full bg-gray-100 text-gray-600 !border-none`}>
|
||||
{queueItem.order}
|
||||
</div>
|
||||
|
||||
{/* Song Info */}
|
||||
<IonLabel>
|
||||
<p className="text-sm bold-title truncate">
|
||||
<p className="text-sm font-semibold truncate">
|
||||
{queueItem.singer.name}
|
||||
</p>
|
||||
<h3 className="text-sm bold-title truncate">
|
||||
<h3 className="text-sm font-semibold truncate">
|
||||
{queueItem.song.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
@ -62,30 +124,10 @@ const Queue: React.FC = () => {
|
||||
</p>
|
||||
</IonLabel>
|
||||
|
||||
{/* Admin Controls */}
|
||||
{(canReorder || isAdmin) && (
|
||||
<div slot="end" className="flex items-center gap-2 ml-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
{queueItem.order > 2 && (
|
||||
<ActionButton
|
||||
onClick={() => handleMoveUp(queueItem)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
<IonIcon icon={arrowUp} />
|
||||
</ActionButton>
|
||||
)}
|
||||
{queueItem.order > 1 && queueItem.order < queueItems.length && (
|
||||
<ActionButton
|
||||
onClick={() => handleMoveDown(queueItem)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
<IonIcon icon={arrowDown} />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
{canDelete && (
|
||||
{/* Delete Button or Drag Handle */}
|
||||
<div slot="end" className="flex items-center gap-2 ml-2">
|
||||
{canDelete && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ActionButton
|
||||
onClick={() => handleRemoveFromQueue(queueItem)}
|
||||
variant="danger"
|
||||
@ -93,9 +135,14 @@ const Queue: React.FC = () => {
|
||||
>
|
||||
<IonIcon icon={trash} />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{canReorder && queueMode === 'reorder' && (
|
||||
<div className="text-gray-400">
|
||||
<IonIcon icon={reorderTwoOutline} size="small" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</IonItem>
|
||||
|
||||
{/* Swipe Actions */}
|
||||
@ -113,6 +160,66 @@ const Queue: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Render first item (currently playing) separately
|
||||
const renderFirstItem = () => {
|
||||
if (queueItems.length === 0) return null;
|
||||
|
||||
const firstItem = queueItems[0];
|
||||
const canDeleteFirstItem = isAdmin && (playerState?.state === PlayerState.stopped || playerState?.state === PlayerState.paused);
|
||||
|
||||
return (
|
||||
<IonItemSliding key={firstItem.key}>
|
||||
<IonItem>
|
||||
{/* Order Number */}
|
||||
<div slot="start" className={`relative flex-shrink-0 w-12 h-12 flex items-center justify-center font-medium rounded-full bg-gray-100 text-gray-600 !border-none`}>
|
||||
{firstItem.order}
|
||||
</div>
|
||||
|
||||
{/* Song Info */}
|
||||
<IonLabel>
|
||||
<p className="text-sm font-semibold truncate">
|
||||
{firstItem.singer.name}
|
||||
</p>
|
||||
<h3 className="text-sm font-semibold truncate">
|
||||
{firstItem.song.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{firstItem.song.artist}
|
||||
</p>
|
||||
</IonLabel>
|
||||
|
||||
{/* Delete Button */}
|
||||
<div slot="end" className="flex items-center gap-2 ml-2">
|
||||
{canDeleteFirstItem && queueMode === 'delete' && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ActionButton
|
||||
onClick={() => handleRemoveFromQueue(firstItem)}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
className="opacity-100"
|
||||
>
|
||||
<IonIcon icon={trash} />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</IonItem>
|
||||
|
||||
{/* Swipe Actions */}
|
||||
{canDeleteFirstItem && queueMode === 'delete' && (
|
||||
<IonItemOptions side="end">
|
||||
<IonItemOption
|
||||
color="danger"
|
||||
onClick={() => handleRemoveFromQueue(firstItem)}
|
||||
>
|
||||
<IonIcon icon={trash} slot="icon-only" />
|
||||
</IonItemOption>
|
||||
</IonItemOptions>
|
||||
)}
|
||||
</IonItemSliding>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
@ -120,21 +227,54 @@ const Queue: React.FC = () => {
|
||||
subtitle={`${queueStats.totalSongs} song${queueStats.totalSongs !== 1 ? 's' : ''} in queue`}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Queue List */}
|
||||
<InfiniteScrollList<QueueItem>
|
||||
items={queueItems}
|
||||
isLoading={queueCount === 0}
|
||||
hasMore={false}
|
||||
onLoadMore={() => {}}
|
||||
renderItem={renderQueueItem}
|
||||
emptyTitle="Queue is empty"
|
||||
emptyMessage="Add songs from search, history, or favorites to get started"
|
||||
loadingTitle="Loading queue..."
|
||||
loadingMessage="Please wait while queue data is being loaded"
|
||||
/>
|
||||
{/* Mode Toggle Button */}
|
||||
{canReorder && (
|
||||
<div className="mb-4 flex justify-end">
|
||||
<IonButton
|
||||
onClick={toggleQueueMode}
|
||||
fill="outline"
|
||||
size="small"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<IonIcon icon={queueMode === 'delete' ? reorderThreeOutline : trash} />
|
||||
{queueMode === 'delete' ? 'Reorder Mode' : 'Delete Mode'}
|
||||
</IonButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mode Instructions */}
|
||||
{canReorder && (
|
||||
<div className="mb-4 p-3 bg-blue-50 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
{queueMode === 'reorder'
|
||||
? (isPlaying
|
||||
? "💡 Drag and drop to reorder songs (first song is locked while playing)"
|
||||
: "💡 Drag and drop to reorder songs"
|
||||
)
|
||||
: "🗑️ Click the trash icon to delete songs from the queue"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* First Item (Currently Playing) */}
|
||||
{renderFirstItem()}
|
||||
|
||||
{/* Queue List with Reorder */}
|
||||
{canReorder && queueMode === 'reorder' ? (
|
||||
<IonReorderGroup disabled={false} onIonItemReorder={doReorder}>
|
||||
{listItems.map((queueItem, index) => (
|
||||
<IonReorder key={queueItem.key} style={{ minHeight: '60px' }}>
|
||||
{renderQueueItem(queueItem, index)}
|
||||
</IonReorder>
|
||||
))}
|
||||
</IonReorderGroup>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{listItems.map((queueItem, index) => renderQueueItem(queueItem, index))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -171,6 +171,76 @@ export const useQueue = () => {
|
||||
}
|
||||
}, [controllerName, queueItems, showSuccess, showError]);
|
||||
|
||||
const handleReorder = useCallback(async (oldIndex: number, newIndex: number) => {
|
||||
console.log('handleReorder called with:', { oldIndex, newIndex });
|
||||
console.log('Current queueItems:', queueItems);
|
||||
console.log('Controller name:', controllerName);
|
||||
|
||||
if (!controllerName || oldIndex === newIndex) {
|
||||
console.log('Early return - conditions not met:', {
|
||||
controllerName: !!controllerName,
|
||||
oldIndex,
|
||||
newIndex
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const itemToMove = queueItems[oldIndex];
|
||||
if (!itemToMove || !itemToMove.key) {
|
||||
console.log('No item to move found');
|
||||
showError('Cannot reorder item');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Moving item:', {
|
||||
item: { key: itemToMove.key, order: itemToMove.order },
|
||||
fromIndex: oldIndex,
|
||||
toIndex: newIndex
|
||||
});
|
||||
|
||||
// Calculate the new order for the moved item
|
||||
const newOrder = newIndex + 1;
|
||||
|
||||
// Update all affected items' orders
|
||||
const updatePromises: Promise<void>[] = [];
|
||||
|
||||
if (oldIndex < newIndex) {
|
||||
// Moving down: shift items between oldIndex and newIndex up by 1
|
||||
for (let i = oldIndex + 1; i <= newIndex; i++) {
|
||||
const item = queueItems[i];
|
||||
if (item && item.key) {
|
||||
updatePromises.push(
|
||||
queueService.updateQueueItem(controllerName, item.key, { order: item.order - 1 })
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Moving up: shift items between newIndex and oldIndex down by 1
|
||||
for (let i = newIndex; i < oldIndex; i++) {
|
||||
const item = queueItems[i];
|
||||
if (item && item.key) {
|
||||
updatePromises.push(
|
||||
queueService.updateQueueItem(controllerName, item.key, { order: item.order + 1 })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the moved item's order
|
||||
updatePromises.push(
|
||||
queueService.updateQueueItem(controllerName, itemToMove.key, { order: newOrder })
|
||||
);
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
console.log('Reorder completed successfully');
|
||||
showSuccess('Queue reordered successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to reorder queue:', error);
|
||||
showError('Failed to reorder queue');
|
||||
}
|
||||
}, [controllerName, queueItems, showSuccess, showError]);
|
||||
|
||||
return {
|
||||
queueItems,
|
||||
queueStats,
|
||||
@ -179,5 +249,6 @@ export const useQueue = () => {
|
||||
handleToggleFavorite,
|
||||
handleMoveUp,
|
||||
handleMoveDown,
|
||||
handleReorder,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user