feat: integrate volume control into PlayerControls and remove PlayerVolume component
This commit is contained in:
@@ -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,22 +346,17 @@ 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)}
|
||||||
|
onVolumeChange={handleVolumeChange}
|
||||||
|
onMute={handleMute}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<PlayerVolume
|
|
||||||
volume={state.volume}
|
|
||||||
onVolumeChange={handleVolumeChange}
|
|
||||||
onMute={handleMute}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Playlist */}
|
{/* Playlist */}
|
||||||
{showPlaylist && state.playlist && (
|
{showPlaylist && state.playlist && (
|
||||||
<div className={`mt-4 pt-4 border-t ${playlistFlashClass}`}>
|
<div className={`mt-4 pt-4 border-t ${playlistFlashClass}`}>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user