feat: integrate volume control into PlayerControls and remove PlayerVolume component

This commit is contained in:
JSC
2025-09-22 20:10:37 +02:00
parent 9784d259ba
commit 9366dbca14
5 changed files with 71 additions and 95 deletions

View File

@@ -21,7 +21,6 @@ import { Playlist } from './Playlist'
import { PlayerControls } from './PlayerControls' import { PlayerControls } from './PlayerControls'
import { PlayerProgress } from './PlayerProgress' import { PlayerProgress } from './PlayerProgress'
import { PlayerTrackInfo } from './PlayerTrackInfo' import { PlayerTrackInfo } from './PlayerTrackInfo'
import { PlayerVolume } from './PlayerVolume'
import { useRenderFlash } from '@/hooks/useRenderFlash' import { useRenderFlash } from '@/hooks/useRenderFlash'
export type PlayerDisplayMode = 'normal' | 'minimized' | 'maximized' | 'sidebar' export type PlayerDisplayMode = 'normal' | 'minimized' | 'maximized' | 'sidebar'
@@ -347,21 +346,16 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
mode={state.mode} mode={state.mode}
isLoading={isLoading} isLoading={isLoading}
showPlaylistButton={true} showPlaylistButton={true}
volume={state.volume}
onPlayPause={handlePlayPause} onPlayPause={handlePlayPause}
onStop={handleStop} onStop={handleStop}
onPrevious={handlePrevious} onPrevious={handlePrevious}
onNext={handleNext} onNext={handleNext}
onModeChange={handleModeChange} onModeChange={handleModeChange}
onTogglePlaylist={() => setShowPlaylist(!showPlaylist)} onTogglePlaylist={() => setShowPlaylist(!showPlaylist)}
/>
<div className="flex items-center justify-end">
<PlayerVolume
volume={state.volume}
onVolumeChange={handleVolumeChange} onVolumeChange={handleVolumeChange}
onMute={handleMute} onMute={handleMute}
/> />
</div>
{/* Playlist */} {/* Playlist */}
{showPlaylist && state.playlist && ( {showPlaylist && state.playlist && (
@@ -429,16 +423,12 @@ export function Player({ className, onPlayerModeChange }: PlayerProps) {
status={state.status} status={state.status}
mode={state.mode} mode={state.mode}
isLoading={isLoading} isLoading={isLoading}
volume={state.volume}
onPlayPause={handlePlayPause} onPlayPause={handlePlayPause}
onStop={handleStop} onStop={handleStop}
onPrevious={handlePrevious} onPrevious={handlePrevious}
onNext={handleNext} onNext={handleNext}
onModeChange={handleModeChange} onModeChange={handleModeChange}
variant="maximized"
/>
<PlayerVolume
volume={state.volume}
onVolumeChange={handleVolumeChange} onVolumeChange={handleVolumeChange}
onMute={handleMute} onMute={handleMute}
variant="maximized" variant="maximized"

View File

@@ -1,5 +1,6 @@
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import type { PlayerMode, PlayerState } from '@/lib/api/services/player' import type { PlayerMode, PlayerState } from '@/lib/api/services/player'
import { import {
ArrowRight, ArrowRight,
@@ -13,6 +14,8 @@ import {
SkipBack, SkipBack,
SkipForward, SkipForward,
Square, Square,
Volume2,
VolumeX,
} from 'lucide-react' } from 'lucide-react'
import { memo, useMemo } from 'react' import { memo, useMemo } from 'react'
import { useRenderFlash } from '@/hooks/useRenderFlash' import { useRenderFlash } from '@/hooks/useRenderFlash'
@@ -22,12 +25,15 @@ interface PlayerControlsProps {
mode: PlayerMode mode: PlayerMode
isLoading: boolean isLoading: boolean
showPlaylistButton?: boolean showPlaylistButton?: boolean
volume?: number
onPlayPause: () => void onPlayPause: () => void
onStop: () => void onStop: () => void
onPrevious: () => void onPrevious: () => void
onNext: () => void onNext: () => void
onModeChange: () => void onModeChange: () => void
onTogglePlaylist?: () => void onTogglePlaylist?: () => void
onVolumeChange?: (volume: number[]) => void
onMute?: () => void
variant?: 'normal' | 'maximized' | 'minimized' variant?: 'normal' | 'maximized' | 'minimized'
} }
@@ -36,17 +42,20 @@ export const PlayerControls = memo(function PlayerControls({
mode, mode,
isLoading, isLoading,
showPlaylistButton = false, showPlaylistButton = false,
volume,
onPlayPause, onPlayPause,
onStop, onStop,
onPrevious, onPrevious,
onNext, onNext,
onModeChange, onModeChange,
onTogglePlaylist, onTogglePlaylist,
onVolumeChange,
onMute,
variant = 'normal', variant = 'normal',
}: PlayerControlsProps) { }: PlayerControlsProps) {
const isMinimized = variant === 'minimized' const isMinimized = variant === 'minimized'
const isMaximized = variant === 'maximized' const isMaximized = variant === 'maximized'
const flashClass = useRenderFlash([status, mode], 'green') const flashClass = useRenderFlash([status, mode, volume], 'green')
const modeIcon = useMemo(() => { const modeIcon = useMemo(() => {
switch (mode) { switch (mode) {
@@ -168,6 +177,30 @@ export const PlayerControls = memo(function PlayerControls({
</Button> </Button>
<Badge variant="secondary">{modeLabel}</Badge> <Badge variant="secondary">{modeLabel}</Badge>
</div> </div>
{volume !== undefined && onVolumeChange && onMute && (
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={onMute}>
{volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-24">
<Slider
value={[volume]}
max={100}
step={1}
onValueChange={onVolumeChange}
className="w-full"
/>
</div>
<span className="text-sm text-muted-foreground w-8">
{Math.round(volume)}%
</span>
</div>
)}
</div> </div>
</div> </div>
) )
@@ -242,6 +275,32 @@ export const PlayerControls = memo(function PlayerControls({
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{modeLabel} {modeLabel}
</Badge> </Badge>
{volume !== undefined && onVolumeChange && onMute && (
<div className="flex items-center gap-2">
<Button
size="sm"
variant="ghost"
onClick={onMute}
className="h-8 w-8 p-0"
>
{volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-16">
<Slider
value={[volume]}
max={100}
step={1}
onValueChange={onVolumeChange}
className="w-full"
/>
</div>
</div>
)}
</div> </div>
</div> </div>
) )

View File

@@ -18,7 +18,11 @@ export const PlayerProgress = memo(function PlayerProgress({
variant = 'normal', variant = 'normal',
}: PlayerProgressProps) { }: PlayerProgressProps) {
const isMaximized = variant === 'maximized' const isMaximized = variant === 'maximized'
const flashClass = useRenderFlash([position, duration], 'blue')
// Only flash when seconds actually change to avoid NumberFlow timing issues
const positionSeconds = Math.floor(position / 1000)
const durationSeconds = Math.floor(duration / 1000)
const flashClass = useRenderFlash([positionSeconds, durationSeconds], 'blue')
const progressPercentage = useMemo(() => const progressPercentage = useMemo(() =>
(position / (duration || 1)) * 100, (position / (duration || 1)) * 100,

View File

@@ -1,77 +0,0 @@
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { Volume2, VolumeX } from 'lucide-react'
import { memo } from 'react'
import { useRenderFlash } from '@/hooks/useRenderFlash'
interface PlayerVolumeProps {
volume: number
onVolumeChange: (volume: number[]) => void
onMute: () => void
variant?: 'normal' | 'maximized'
}
export const PlayerVolume = memo(function PlayerVolume({
volume,
onVolumeChange,
onMute,
variant = 'normal',
}: PlayerVolumeProps) {
const isMaximized = variant === 'maximized'
const flashClass = useRenderFlash([volume], 'yellow')
if (isMaximized) {
return (
<div className={`${flashClass} flex items-center gap-2`}>
{/* DEBUG: PlayerVolume Maximized - YELLOW FLASH */}
<Button size="sm" variant="ghost" onClick={onMute}>
{volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-24">
<Slider
value={[volume]}
max={100}
step={1}
onValueChange={onVolumeChange}
className="w-full"
/>
</div>
<span className="text-sm text-muted-foreground w-8">
{Math.round(volume)}%
</span>
</div>
)
}
// Normal variant
return (
<div className={`${flashClass} flex items-center gap-2`}>
{/* DEBUG: PlayerVolume Normal - YELLOW FLASH */}
<Button
size="sm"
variant="ghost"
onClick={onMute}
className="h-8 w-8 p-0"
>
{volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-16">
<Slider
value={[volume]}
max={100}
step={1}
onValueChange={onVolumeChange}
className="w-full"
/>
</div>
</div>
)
})

View File

@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
export function useRenderFlash(deps: any[], color: string = 'red', duration: number = 300) { export function useRenderFlash(deps: any[], color: string = 'red', duration: number = 300) {
const [isFlashing, setIsFlashing] = useState(false) const [isFlashing, setIsFlashing] = useState(false)
const prevDepsRef = useRef<any[]>() const prevDepsRef = useRef<any[] | undefined>(undefined)
useEffect(() => { useEffect(() => {
// Check if this is the first render // Check if this is the first render
@@ -25,7 +25,7 @@ export function useRenderFlash(deps: any[], color: string = 'red', duration: num
} }
prevDepsRef.current = deps prevDepsRef.current = deps
}, deps) }, [...deps])
const flashClass = isFlashing const flashClass = isFlashing
? `border-2 border-${color}-500 border-dashed animate-pulse` ? `border-2 border-${color}-500 border-dashed animate-pulse`