177 lines
6.5 KiB
TypeScript
177 lines
6.5 KiB
TypeScript
import React, { HTMLAttributes, useState, useRef } from 'react';
|
|
import classNames from 'classnames';
|
|
import { FaPlay, FaPause, FaStepForward, FaStepBackward, FaVolumeUp, FaDesktop, FaStop } from 'react-icons/fa';
|
|
import { Features } from '../api/player';
|
|
|
|
interface NowPlayingProps extends HTMLAttributes<HTMLDivElement> {
|
|
songName: string;
|
|
fileName: string;
|
|
isPlaying: boolean;
|
|
isIdle: boolean;
|
|
volume: number;
|
|
timePosition?: number;
|
|
duration?: number;
|
|
seekable?: boolean;
|
|
onPlayPause: () => void;
|
|
onStop: () => void;
|
|
onSkip: () => void;
|
|
onPrevious: () => void;
|
|
onSeek: (time: number) => void;
|
|
|
|
onScreenShare: () => void;
|
|
isScreenSharingSupported: boolean;
|
|
isScreenSharing: boolean;
|
|
|
|
// Sent when the volume setting actually changes value
|
|
onVolumeSettingChange: (volume: number) => void;
|
|
|
|
// Sent when the volume is about to start changing
|
|
onVolumeWillChange: (volume: number) => void;
|
|
|
|
// Sent when the volume has changed
|
|
onVolumeDidChange: (volume: number) => void;
|
|
|
|
features: Features | null;
|
|
|
|
audioEnabled: boolean;
|
|
onAudioEnabledChange: (enabled: boolean) => void;
|
|
}
|
|
|
|
const NowPlaying: React.FC<NowPlayingProps> = (props) => {
|
|
const [isSeeking, setIsSeeking] = useState(false);
|
|
const progressBarRef = useRef<HTMLDivElement>(null);
|
|
|
|
const formatTime = (time: number) => {
|
|
const minutes = Math.floor(time / 60);
|
|
const seconds = Math.floor(time % 60);
|
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
const handleSeek = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (progressBarRef.current) {
|
|
const rect = progressBarRef.current.getBoundingClientRect();
|
|
const newSeekPosition = (e.clientX - rect.left) / rect.width;
|
|
if (props.duration) {
|
|
props.onSeek(newSeekPosition * props.duration);
|
|
}
|
|
}
|
|
};
|
|
|
|
const titleArea = props.isScreenSharing ? (
|
|
<div className="flex flex-row items-center gap-2 text-white text-center justify-center">
|
|
<FaDesktop size={24} />
|
|
<div className="text-lg font-bold truncate">Screen Sharing</div>
|
|
</div>
|
|
) : (
|
|
<div className={classNames(props.isIdle ? 'opacity-50' : 'opacity-100', "flex flex-row items-center justify-between gap-2 w-full")}>
|
|
<div className="truncate">
|
|
<div className="text-lg font-bold truncate">{props.songName}</div>
|
|
<div className="text-sm truncate">{props.fileName}</div>
|
|
</div>
|
|
<div className="text-sm opacity-50 shrink-0">
|
|
{props.timePosition && props.duration ?
|
|
(props.seekable ? `${formatTime(props.timePosition)} / ${formatTime(props.duration)}`
|
|
: `${formatTime(props.timePosition)}` )
|
|
: ''}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className={classNames(props.className, 'bg-black/50 h-fit p-5')}>
|
|
<div className="flex flex-col w-full gap-2">
|
|
<div className="flex flex-col w-full h-full bg-black/50 rounded-lg gap-4 overflow-hidden">
|
|
<div className="p-5">
|
|
<div className="flex-grow min-w-0 w-full text-white text-left">
|
|
{titleArea}
|
|
</div>
|
|
<div className="flex flex-row items-center gap-4 w-full pt-4">
|
|
<div className="flex items-center gap-2 text-white w-full max-w-[250px]">
|
|
<FaVolumeUp size={20} />
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
value={props.volume}
|
|
onMouseDown={() => props.onVolumeWillChange(props.volume)}
|
|
onMouseUp={() => props.onVolumeDidChange(props.volume)}
|
|
onChange={(e) => props.onVolumeSettingChange(Number(e.target.value))}
|
|
className="fancy-slider h-2 w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex-grow"></div>
|
|
|
|
<button className="text-white hover:text-violet-300 transition-colors" onClick={props.onPrevious}>
|
|
<FaStepBackward size={24} />
|
|
</button>
|
|
|
|
<button className="text-white hover:text-violet-300 transition-colors" onClick={props.onPlayPause}>
|
|
{(props.isPlaying && !props.isIdle) ? <FaPause size={24} /> : <FaPlay size={24} />}
|
|
</button>
|
|
|
|
<button className="text-white hover:text-violet-300 transition-colors" onClick={props.onStop}>
|
|
<FaStop size={24} className={props.isIdle ? 'opacity-25' : 'opacity-100'} />
|
|
</button>
|
|
|
|
<button className="text-white hover:text-violet-300 transition-colors" onClick={props.onSkip}>
|
|
<FaStepForward size={24} />
|
|
</button>
|
|
|
|
{(props.isScreenSharingSupported && props.features?.screenshare) && (
|
|
<button
|
|
className={classNames("text-white hover:text-violet-300 transition-colors rounded-full p-2", props.isScreenSharing ? ' bg-violet-800' : '')}
|
|
onClick={props.onScreenShare}
|
|
title="Share your screen"
|
|
>
|
|
<FaDesktop size={24} />
|
|
</button>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{props.seekable !== false && (
|
|
<div
|
|
ref={progressBarRef}
|
|
className="w-full h-2 bg-gray-600 cursor-pointer -mt-3"
|
|
onMouseDown={(e) => {
|
|
setIsSeeking(true);
|
|
handleSeek(e);
|
|
}}
|
|
onMouseMove={(e) => {
|
|
if (isSeeking) {
|
|
handleSeek(e);
|
|
}
|
|
}}
|
|
onMouseUp={() => setIsSeeking(false)}
|
|
onMouseLeave={() => setIsSeeking(false)}
|
|
>
|
|
<div
|
|
className="h-full bg-violet-500"
|
|
style={{ width: `${(props.timePosition && props.duration ? (props.timePosition / props.duration) * 100 : 0)}%` }}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{props.features?.browserPlayback && (
|
|
<div>
|
|
<label className="flex items-center gap-2 text-white text-sm cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={props.audioEnabled}
|
|
onChange={(e) => props.onAudioEnabledChange(e.target.checked)}
|
|
className="w-4 h-4 text-violet-600 bg-gray-100 border-gray-300 rounded focus:ring-violet-500 focus:ring-2"
|
|
/>
|
|
Enable audio playback in browser
|
|
</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default NowPlaying;
|