fix: update previous_volume to 80 in CompactPlayer and Player components for consistency

This commit is contained in:
JSC
2025-08-10 21:55:20 +02:00
parent 0c7875cac5
commit d80d8588f6
3 changed files with 68 additions and 454 deletions

View File

@@ -27,7 +27,7 @@ export function CompactPlayer({ className }: CompactPlayerProps) {
status: 'stopped', status: 'stopped',
mode: 'continuous', mode: 'continuous',
volume: 80, volume: 80,
previous_volume: 50, previous_volume: 80,
position: 0 position: 0
}) })
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)

View File

@@ -46,7 +46,7 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
status: 'stopped', status: 'stopped',
mode: 'continuous', mode: 'continuous',
volume: 80, volume: 80,
previous_volume: 50, previous_volume: 80,
position: 0 position: 0
}) })
const [displayMode, setDisplayMode] = useState<PlayerDisplayMode>(() => { const [displayMode, setDisplayMode] = useState<PlayerDisplayMode>(() => {
@@ -566,11 +566,6 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
</DropdownMenu> </DropdownMenu>
)} )}
</div> </div>
{state.playlist && (
<p className="text-lg text-muted-foreground">
{state.playlist.name}
</p>
)}
</div> </div>
{/* Progress Bar */} {/* Progress Bar */}

View File

@@ -2,14 +2,11 @@ import { useEffect, useState, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router' import { useParams, useNavigate } from 'react-router'
import { import {
DndContext, DndContext,
DragOverlay,
DragStartEvent,
DragEndEvent, DragEndEvent,
closestCenter, closestCenter,
PointerSensor, PointerSensor,
useSensor, useSensor,
useSensors, useSensors,
useDroppable,
} from '@dnd-kit/core' } from '@dnd-kit/core'
import { import {
SortableContext, SortableContext,
@@ -19,7 +16,6 @@ import {
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { AppLayout } from '@/components/AppLayout' import { AppLayout } from '@/components/AppLayout'
import { playlistsService, type Playlist, type PlaylistSound } from '@/lib/api/services/playlists' import { playlistsService, type Playlist, type PlaylistSound } from '@/lib/api/services/playlists'
import { soundsService, type Sound } from '@/lib/api/services/sounds'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -28,53 +24,10 @@ import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { AlertCircle, Save, Music, Clock, ChevronUp, ChevronDown, Trash2, RefreshCw, Edit, X, ArrowLeft, Plus } from 'lucide-react' import { AlertCircle, Save, Music, Clock, ChevronUp, ChevronDown, Trash2, RefreshCw, Edit, X, ArrowLeft } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { formatDuration } from '@/utils/format-duration' import { formatDuration } from '@/utils/format-duration'
// Sortable playlist item component for add sounds mode
interface SortablePlaylistItemProps {
sound: PlaylistSound
index: number
}
function SortablePlaylistItem({ sound, index }: SortablePlaylistItemProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: `playlist-sound-${sound.id}` })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="flex items-center gap-3 p-3 border-b hover:bg-muted/50 cursor-grab active:cursor-grabbing"
>
<div className="text-sm text-muted-foreground font-mono w-8">
{index + 1}
</div>
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{sound.name}</div>
<div className="text-sm text-muted-foreground">
{formatDuration(sound.duration || 0)}
</div>
</div>
</div>
)
}
// Sortable table row component for normal table view // Sortable table row component for normal table view
interface SortableTableRowProps { interface SortableTableRowProps {
@@ -184,99 +137,6 @@ function SortableTableRow({
) )
} }
// Draggable available sound component
interface DraggableAvailableSoundProps {
sound: Sound
onAddSound: (soundId: number) => void
}
function DraggableAvailableSound({ sound, onAddSound }: DraggableAvailableSoundProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: sound.id.toString() })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="flex items-center gap-3 p-3 border-b hover:bg-muted/50 cursor-grab active:cursor-grabbing select-none"
onClick={() => onAddSound(sound.id)}
>
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{sound.name}</div>
<div className="text-sm text-muted-foreground">
{formatDuration(sound.duration || 0)} {sound.type}
</div>
</div>
<Plus className="h-4 w-4 text-primary" />
</div>
)
}
// Drop zone component
interface DropZoneProps {
position: number
isActive?: boolean
}
function DropZone({ position, isActive }: DropZoneProps) {
const {
setNodeRef,
isOver,
} = useDroppable({
id: `playlist-position-${position}`,
data: { type: 'position', position }
})
return (
<div
ref={setNodeRef}
className={`h-2 border-2 border-dashed transition-colors ${
isOver || isActive
? 'border-primary bg-primary/10'
: 'border-transparent hover:border-primary/50'
}`}
/>
)
}
// Empty playlist drop zone
function EmptyPlaylistDropZone() {
const {
setNodeRef,
isOver,
} = useDroppable({
id: 'playlist-position-0',
data: { type: 'position', position: 0 }
})
return (
<div
ref={setNodeRef}
className={`flex items-center justify-center h-full text-muted-foreground border-2 border-dashed transition-colors ${
isOver
? 'border-primary bg-primary/10'
: 'border-muted hover:border-primary/50'
}`}
>
<p>Drag sounds here to add to playlist</p>
</div>
)
}
export function PlaylistEditPage() { export function PlaylistEditPage() {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
@@ -290,12 +150,7 @@ export function PlaylistEditPage() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [isEditMode, setIsEditMode] = useState(false) const [isEditMode, setIsEditMode] = useState(false)
const [isAddSoundsMode, setIsAddSoundsMode] = useState(false)
const [availableSounds, setAvailableSounds] = useState<Sound[]>([])
const [loadingAvailableSounds, setLoadingAvailableSounds] = useState(false)
// dnd-kit state
const [draggedSound, setDraggedSound] = useState<Sound | PlaylistSound | null>(null)
// dnd-kit sensors // dnd-kit sensors
const sensors = useSensors( const sensors = useSensors(
@@ -481,135 +336,24 @@ export function PlaylistEditPage() {
} }
} }
const fetchAvailableSounds = useCallback(async () => {
try {
setLoadingAvailableSounds(true)
// Get all EXT sounds
const allExtSounds = await soundsService.getSoundsByType('EXT')
// Filter out sounds that are already in the current playlist
const currentSoundIds = sounds.map(sound => sound.id)
const available = allExtSounds.filter(sound => !currentSoundIds.includes(sound.id))
setAvailableSounds(available)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch available sounds'
toast.error(errorMessage)
} finally {
setLoadingAvailableSounds(false)
}
}, [sounds])
const handleOpenAddSounds = async () => {
setIsAddSoundsMode(true)
await fetchAvailableSounds()
}
const handleCloseAddSounds = () => {
setIsAddSoundsMode(false)
setAvailableSounds([])
}
const handleAddSoundToPlaylist = async (soundId: number, position?: number) => {
// Find the sound being added for potential rollback
const soundToAdd = availableSounds.find(sound => sound.id === soundId)
try {
// Optimistically remove the sound from available sounds for instant feedback
setAvailableSounds(prev => prev.filter(sound => sound.id !== soundId))
await playlistsService.addSoundToPlaylist(playlistId, soundId, position)
toast.success('Sound added to playlist')
// Refresh playlist sounds to show the new addition
await fetchSounds()
} catch (err) {
// Rollback: add the sound back to available sounds if the API call failed
if (soundToAdd) {
setAvailableSounds(prev => [...prev, soundToAdd])
}
const errorMessage = err instanceof Error ? err.message : 'Failed to add sound to playlist'
toast.error(errorMessage)
}
}
// dnd-kit drag handlers
const handleDragStart = (event: DragStartEvent) => {
const { active } = event
// Extract sound ID from different ID formats
const activeId = active.id as string
const getSoundId = (id: string) => {
if (id.startsWith('table-sound-')) {
return parseInt(id.replace('table-sound-', ''), 10)
} else if (id.startsWith('playlist-sound-')) {
return parseInt(id.replace('playlist-sound-', ''), 10)
} else {
return parseInt(id, 10)
}
}
const soundId = getSoundId(activeId)
const availableSound = availableSounds.find(s => s.id === soundId)
const playlistSound = sounds.find(s => s.id === soundId)
setDraggedSound(availableSound || playlistSound || null)
}
const handleDragOver = () => {
// Handle drag over logic here if needed for visual feedback
}
// dnd-kit drag handler
const handleDragEnd = async (event: DragEndEvent) => { const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event const { active, over } = event
setDraggedSound(null)
if (!over) return if (!over) return
const activeId = active.id as string const activeId = active.id as string
const overId = over.id as string const overId = over.id as string
// Extract sound ID from different ID formats // Only handle table-sound reordering
const getDraggedSoundId = (id: string) => { if (overId.startsWith('table-sound-')) {
if (id.startsWith('table-sound-')) { const draggedSoundId = parseInt(activeId.replace('table-sound-', ''), 10)
return parseInt(id.replace('table-sound-', ''), 10) const targetSoundId = parseInt(overId.replace('table-sound-', ''), 10)
} else if (id.startsWith('playlist-sound-')) {
return parseInt(id.replace('playlist-sound-', ''), 10)
} else {
return parseInt(id, 10)
}
}
const getTargetSoundId = (id: string) => {
if (id.startsWith('table-sound-')) {
return parseInt(id.replace('table-sound-', ''), 10)
} else if (id.startsWith('playlist-sound-')) {
return parseInt(id.replace('playlist-sound-', ''), 10)
} else {
return parseInt(id, 10)
}
}
const draggedSoundId = getDraggedSoundId(activeId)
// Check if dragging from available sounds (adding new sound)
const isFromAvailable = availableSounds.some(s => s.id === draggedSoundId)
// Handle dropping onto playlist positions (inserting new sound in add sounds mode)
if (overId.startsWith('playlist-position-') && isFromAvailable) {
const position = parseInt(overId.replace('playlist-position-', ''), 10)
await handleAddSoundToPlaylist(draggedSoundId, position)
return
}
// Handle reordering within playlist (both add sounds mode and table mode)
if (overId.startsWith('playlist-sound-') || overId.startsWith('table-sound-')) {
const targetSoundId = getTargetSoundId(overId)
const draggedIndex = sounds.findIndex(s => s.id === draggedSoundId) const draggedIndex = sounds.findIndex(s => s.id === draggedSoundId)
const targetIndex = sounds.findIndex(s => s.id === targetSoundId) const targetIndex = sounds.findIndex(s => s.id === targetSoundId)
// Only allow reordering if both sounds are in the playlist
if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) { if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) {
// Reorder sounds in playlist // Reorder sounds in playlist
const newSounds = [...sounds] const newSounds = [...sounds]
@@ -628,11 +372,6 @@ export function PlaylistEditPage() {
toast.error(errorMessage) toast.error(errorMessage)
} }
} }
// Handle dropping from available sounds onto existing playlist item (insert before)
else if (isFromAvailable && targetIndex !== -1) {
await handleAddSoundToPlaylist(draggedSoundId, targetIndex)
return
}
} }
} }
@@ -690,8 +429,6 @@ export function PlaylistEditPage() {
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<AppLayout <AppLayout
@@ -706,14 +443,6 @@ export function PlaylistEditPage() {
<div className="flex-1 rounded-xl bg-muted/50 p-4"> <div className="flex-1 rounded-xl bg-muted/50 p-4">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button
variant="outline"
size="sm"
onClick={() => navigate('/playlists')}
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
<div> <div>
<h1 className="text-2xl font-bold">{playlist.name}</h1> <h1 className="text-2xl font-bold">{playlist.name}</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
@@ -892,189 +621,79 @@ export function PlaylistEditPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Music className="h-5 w-5" /> <Music className="h-5 w-5" />
{isAddSoundsMode ? 'Add Sounds to Playlist' : `Playlist Sounds (${sounds.length})`} Playlist Sounds ({sounds.length})
</CardTitle> </CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isAddSoundsMode ? ( <Button
<Button variant="outline"
variant="outline" size="sm"
size="sm" onClick={fetchSounds}
onClick={handleCloseAddSounds} disabled={soundsLoading}
> >
<X className="h-4 w-4 mr-2" /> <RefreshCw className={`h-4 w-4 ${soundsLoading ? 'animate-spin' : ''}`} />
Cancel </Button>
</Button>
) : (
<>
<Button
variant="outline"
size="sm"
onClick={handleOpenAddSounds}
title="Add sounds to playlist"
>
<Plus className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={fetchSounds}
disabled={soundsLoading}
>
<RefreshCw className={`h-4 w-4 ${soundsLoading ? 'animate-spin' : ''}`} />
</Button>
</>
)}
</div> </div>
</div> </div>
{isAddSoundsMode && (
<p className="text-sm text-muted-foreground mt-2">
Drag sounds from the available list (right) to add them to your playlist (left) at specific positions, or click a sound to add it to the end.
</p>
)}
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isAddSoundsMode ? ( {soundsLoading ? (
/* Add Sounds Mode - Two Column Layout */ <div className="space-y-3">
<div className="grid grid-cols-2 gap-6 min-h-[600px]"> {Array.from({ length: 3 }).map((_, i) => (
{/* Current Playlist Sounds - Left Column */} <Skeleton key={i} className="h-12 w-full" />
<div className="flex flex-col"> ))}
<h3 className="font-semibold mb-4 flex items-center gap-2"> </div>
<Music className="h-4 w-4" /> ) : sounds.length === 0 ? (
Current Playlist ({sounds.length} sounds) <div className="text-center py-8 text-muted-foreground">
</h3> <Music className="h-8 w-8 mx-auto mb-2 opacity-50" />
<div className="flex-1 border rounded-lg overflow-hidden"> <p>No sounds in this playlist</p>
<SortableContext
items={sounds.map(sound => `playlist-sound-${sound.id}`)}
strategy={verticalListSortingStrategy}
>
{sounds.length === 0 ? (
<EmptyPlaylistDropZone />
) : (
<div className="h-full overflow-y-auto">
{/* Drop zone at the top */}
<DropZone position={0} />
{sounds.map((sound, index) => (
<div key={sound.id}>
<SortablePlaylistItem sound={sound} index={index} />
<DropZone position={index + 1} />
</div>
))}
</div>
)}
</SortableContext>
</div>
</div>
{/* Available Sounds - Right Column */}
<div className="flex flex-col">
<h3 className="font-semibold mb-4 flex items-center gap-2">
<Music className="h-4 w-4" />
Available EXT Sounds ({availableSounds.length} available)
</h3>
<div className="flex-1 border rounded-lg overflow-hidden">
{loadingAvailableSounds ? (
<div className="flex items-center justify-center h-full">
<RefreshCw className="h-6 w-6 animate-spin" />
</div>
) : availableSounds.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<p>No available EXT sounds</p>
</div>
) : (
<SortableContext
items={availableSounds.map(sound => sound.id.toString())}
strategy={verticalListSortingStrategy}
>
<div className="h-full overflow-y-auto">
{availableSounds.map((sound) => (
<DraggableAvailableSound
key={sound.id}
sound={sound}
onAddSound={handleAddSoundToPlaylist}
/>
))}
</div>
</SortableContext>
)}
</div>
</div>
</div> </div>
) : ( ) : (
/* Normal Mode - Table View */ <div className="rounded-md border">
soundsLoading ? ( <SortableContext
<div className="space-y-3"> items={sounds.map(sound => `table-sound-${sound.id}`)}
{Array.from({ length: 3 }).map((_, i) => ( strategy={verticalListSortingStrategy}
<Skeleton key={i} className="h-12 w-full" /> >
))} <Table>
</div> <TableHeader>
) : sounds.length === 0 ? ( <TableRow>
<div className="text-center py-8 text-muted-foreground"> <TableHead className="w-16 text-center">
<Music className="h-8 w-8 mx-auto mb-2 opacity-50" /> <div className="flex items-center justify-center gap-1">
<p>No sounds in this playlist</p> <div className="flex flex-col">
</div> <div className="w-1 h-1 bg-muted-foreground/40 rounded-full"></div>
) : ( <div className="w-1 h-1 bg-muted-foreground/40 rounded-full mt-0.5"></div>
<div className="rounded-md border"> <div className="w-1 h-1 bg-muted-foreground/40 rounded-full mt-0.5"></div>
<SortableContext
items={sounds.map(sound => `table-sound-${sound.id}`)}
strategy={verticalListSortingStrategy}
>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16 text-center">
<div className="flex items-center justify-center gap-1">
<div className="flex flex-col">
<div className="w-1 h-1 bg-muted-foreground/40 rounded-full"></div>
<div className="w-1 h-1 bg-muted-foreground/40 rounded-full mt-0.5"></div>
<div className="w-1 h-1 bg-muted-foreground/40 rounded-full mt-0.5"></div>
</div>
<span className="text-xs">#</span>
</div> </div>
</TableHead> <span className="text-xs">#</span>
<TableHead>Name</TableHead> </div>
<TableHead>Duration</TableHead> </TableHead>
<TableHead>Type</TableHead> <TableHead>Name</TableHead>
<TableHead>Plays</TableHead> <TableHead>Duration</TableHead>
<TableHead className="w-32">Actions</TableHead> <TableHead>Type</TableHead>
</TableRow> <TableHead>Plays</TableHead>
</TableHeader> <TableHead className="w-32">Actions</TableHead>
<TableBody> </TableRow>
{sounds.map((sound, index) => ( </TableHeader>
<SortableTableRow <TableBody>
key={sound.id} {sounds.map((sound, index) => (
sound={sound} <SortableTableRow
index={index} key={sound.id}
onMoveSoundUp={handleMoveSoundUp} sound={sound}
onMoveSoundDown={handleMoveSoundDown} index={index}
onRemoveSound={handleRemoveSound} onMoveSoundUp={handleMoveSoundUp}
totalSounds={sounds.length} onMoveSoundDown={handleMoveSoundDown}
/> onRemoveSound={handleRemoveSound}
))} totalSounds={sounds.length}
</TableBody> />
</Table> ))}
</SortableContext> </TableBody>
</div> </Table>
) </SortableContext>
</div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Drag Overlay */}
<DragOverlay>
{draggedSound && (
<div className="flex items-center gap-3 p-3 bg-background border rounded-lg shadow-lg">
<Music className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{draggedSound.name}</div>
<div className="text-sm text-muted-foreground">
{formatDuration(draggedSound.duration || 0)}
</div>
</div>
</div>
)}
</DragOverlay>
</AppLayout> </AppLayout>
</DndContext> </DndContext>
) )