gbclient/app/player/page.tsx

471 lines
16 KiB
TypeScript
Raw Permalink Normal View History

"use client";
import React, { useState, useRef, useEffect } from 'react';
import { Play, Pause, Volume2, VolumeX, SkipBack, SkipForward, Maximize, Settings, Music, Video, FileText, Search } from 'lucide-react';
import Footer from '../footer';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
// Media type detection
const getMediaType = (url) => {
const extension = url.split('.').pop()?.toLowerCase();
if (['mp4', 'webm', 'ogg', 'mov', 'avi'].includes(extension || '')) return 'video';
if (['mp3', 'wav', 'flac', 'aac', 'm4a'].includes(extension || '')) return 'audio';
if (['pdf', 'ppt', 'pptx'].includes(extension || '')) return 'slides';
return 'video'; // default
};
import './style.css'; // Ensure you have a styles.css file for custom styles
// Waveform Component (Winamp inspired)
const WaveformVisualizer = ({ isPlaying, currentTime = 0 }) => {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Generate mock waveform data
const bars = 100;
const barWidth = width / bars;
const animate = () => {
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < bars; i++) {
const barHeight = isPlaying
? Math.random() * height * 0.7 + height * 0.1
: height * 0.2;
const x = i * barWidth;
const progress = currentTime * bars;
// Gradient effect
const gradient = ctx.createLinearGradient(0, 0, 0, height);
if (i < progress) {
gradient.addColorStop(0, 'hsl(207, 90%, 54%)'); // accent color
gradient.addColorStop(1, 'hsl(207, 90%, 64%)');
} else {
gradient.addColorStop(0, 'hsl(0, 0%, 85%)'); // muted color
gradient.addColorStop(1, 'hsl(0, 0%, 75%)');
}
ctx.fillStyle = gradient;
ctx.fillRect(x, height - barHeight, barWidth - 1, barHeight);
}
if (isPlaying) {
requestAnimationFrame(animate);
}
};
animate();
}, [isPlaying, currentTime]);
return (
<div className="bg-card border border-border rounded-lg p-4">
<canvas
ref={canvasRef}
width={800}
height={120}
className="w-full h-20 rounded"
/>
</div>
);
};
// Media Player Component
const MediaPlayer = ({ media }) => {
const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(0.7);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [isMuted, setIsMuted] = useState(false);
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const videoRef = useRef(null);
const playerContainerRef = useRef(null);
const mediaType = getMediaType(media.url);
const formatTime = (time) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const handlePlayPause = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
const handleVolumeChange = (e) => {
const newVolume = parseFloat(e.target.value);
setVolume(newVolume);
if (videoRef.current) {
videoRef.current.volume = newVolume;
}
};
const handleProgressChange = (e) => {
const newTime = (parseFloat(e.target.value) / 100) * duration;
setCurrentTime(newTime);
if (videoRef.current) {
videoRef.current.currentTime = newTime;
}
};
const toggleMute = () => {
setIsMuted(!isMuted);
if (videoRef.current) {
videoRef.current.muted = !isMuted;
}
};
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
playerContainerRef.current?.requestFullscreen();
} else {
document.exitFullscreen();
}
};
const skipTime = (seconds) => {
if (videoRef.current) {
videoRef.current.currentTime += seconds;
}
};
return (
<div ref={playerContainerRef} className="bg-card border border-border rounded-lg overflow-hidden shadow-lg">
{/* Media Display Area */}
<div className="relative bg-black aspect-video">
{mediaType === 'video' && (
<video
ref={videoRef}
className="w-full h-full object-contain"
onTimeUpdate={(e) => setCurrentTime((e.target as HTMLVideoElement).currentTime)}
onLoadedMetadata={(e) => setDuration((e.target as HTMLVideoElement).duration)}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
<source src={media.url} type="video/mp4" />
</video>
)}
{mediaType === 'audio' && (
<div className="flex items-center justify-center h-full bg-gradient-to-br from-primary/10 to-accent/10">
<div className="text-center">
<Music className="w-24 h-24 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold text-foreground">{media.title}</h3>
<p className="text-muted-foreground">{media.artist || 'Unknown Artist'}</p>
</div>
<audio
ref={videoRef}
onTimeUpdate={(e) => setCurrentTime((e.target as HTMLAudioElement).currentTime)}
onLoadedMetadata={(e) => setDuration((e.target as HTMLAudioElement).duration)}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
<source src={media.url} type="audio/mp3" />
</audio>
</div>
)}
{mediaType === 'slides' && (
<div className="flex items-center justify-center h-full bg-gradient-to-br from-secondary/50 to-muted/30">
<div className="text-center">
<FileText className="w-24 h-24 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold text-foreground">{media.title}</h3>
<p className="text-muted-foreground">Presentation Slides</p>
</div>
</div>
)}
{/* Overlay Controls */}
<div className="absolute inset-0 bg-black/20 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center">
<button
onClick={handlePlayPause}
className="bg-primary hover:bg-primary/90 text-primary-foreground rounded-full p-4 transition-all hover:scale-110"
>
{isPlaying ? <Pause className="w-8 h-8" /> : <Play className="w-8 h-8 ml-1" />}
</button>
</div>
</div>
{/* Waveform for Audio */}
{mediaType === 'audio' && (
<div className="p-4">
<WaveformVisualizer isPlaying={isPlaying} currentTime={currentTime / duration} />
</div>
)}
{/* Controls */}
<div className="p-4 bg-card border-t border-border">
{/* Progress Bar */}
<div className="mb-4">
<input
type="range"
min="0"
max="100"
value={(currentTime / duration) * 100 || 0}
onChange={handleProgressChange}
className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer slider"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
{/* Control Buttons */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<button
onClick={() => skipTime(-10)}
className="p-2 hover:bg-secondary rounded-lg transition-colors"
>
<SkipBack className="w-5 h-5" />
</button>
<button
onClick={handlePlayPause}
className="bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg p-3 transition-all hover:scale-105"
>
{isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
</button>
<button
onClick={() => skipTime(10)}
className="p-2 hover:bg-secondary rounded-lg transition-colors"
>
<SkipForward className="w-5 h-5" />
</button>
<div className="flex items-center space-x-2 ml-4">
<button
onClick={toggleMute}
className="p-2 hover:bg-secondary rounded-lg transition-colors"
>
{isMuted || volume === 0 ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
</button>
<input
type="range"
min="0"
max="1"
step="0.1"
value={isMuted ? 0 : volume}
onChange={handleVolumeChange}
className="w-20 h-2 bg-muted rounded-lg appearance-none cursor-pointer slider"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<select
value={playbackSpeed}
onChange={(e) => setPlaybackSpeed(parseFloat(e.target.value))}
className="bg-secondary text-secondary-foreground px-2 py-1 rounded text-sm border border-border"
>
<option value={0.5}>0.5x</option>
<option value={0.75}>0.75x</option>
<option value={1}>Normal</option>
<option value={1.25}>1.25x</option>
<option value={1.5}>1.5x</option>
<option value={2}>2x</option>
</select>
<button
onClick={toggleFullscreen}
className="p-2 hover:bg-secondary rounded-lg transition-colors"
>
<Maximize className="w-5 h-5" />
</button>
<button className="p-2 hover:bg-secondary rounded-lg transition-colors">
<Settings className="w-5 h-5" />
</button>
</div>
</div>
</div>
</div>
);
};
// Media Library Component
const MediaLibrary = ({ mediaList, onSelectMedia, selectedMedia }) => {
const [searchQuery, setSearchQuery] = useState('');
const filteredMedia = mediaList.filter(media =>
media.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
(media.artist && media.artist.toLowerCase().includes(searchQuery.toLowerCase()))
);
const getMediaIcon = (url) => {
const type = getMediaType(url);
switch (type) {
case 'video': return <Video className="w-5 h-5" />;
case 'audio': return <Music className="w-5 h-5" />;
case 'slides': return <FileText className="w-5 h-5" />;
default: return <Video className="w-5 h-5" />;
}
};
return (
<div className="bg-card border border-border rounded-lg p-4 h-full">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-foreground">Media Library</h2>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="w-4 h-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 pr-2 py-1 bg-input border border-border rounded text-sm focus:outline-none focus:ring-2 focus:ring-ring w-full"
/>
</div>
</div>
</div>
{/* Media List */}
<div className="space-y-2 overflow-y-auto max-h-[calc(100vh-200px)]">
{filteredMedia.map((media, index) => (
<div
key={index}
onClick={() => onSelectMedia(media)}
className={`cursor-pointer transition-all hover:bg-muted rounded p-2 flex items-center space-x-2 ${
selectedMedia?.url === media.url ? 'bg-primary/10 border-l-2 border-primary' : ''
}`}
>
<div className="text-accent">
{getMediaIcon(media.url)}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-foreground truncate">{media.title}</h3>
{media.artist && (
<p className="text-xs text-muted-foreground truncate">{media.artist}</p>
)}
</div>
</div>
))}
</div>
</div>
);
};
// Main App Component
const CorporateMediaPlayer = () => {
const [selectedMedia, setSelectedMedia] = useState(null);
// Sample media library
const mediaLibrary = [
{
title: "Corporate Overview Video",
url: "https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4",
type: "video"
},
{
title: "Quarterly Results Presentation",
url: "https://example.com/presentation.pdf",
type: "slides"
},
{
title: "Background Music",
artist: "Corporate Audio",
url: "https://www2.cs.uic.edu/~i101/SoundFiles/BabyElephantWalk60.wav",
type: "audio"
},
{
title: "Product Demo",
url: "https://sample-videos.com/zip/10/mp4/SampleVideo_640x360_1mb.mp4",
type: "video"
},
{
title: "Conference Call Audio",
artist: "Meeting Recording",
url: "https://www2.cs.uic.edu/~i101/SoundFiles/StarWars60.wav",
type: "audio"
}
];
useEffect(() => {
// Auto-select first media item
if (mediaLibrary.length > 0 && !selectedMedia) {
setSelectedMedia(mediaLibrary[0]);
}
}, []);
const shortcuts = [
// Media control row
[
{ key: 'Space', label: 'Play/Pause', action: () => console.log('Play/Pause') },
{ key: '←', label: 'Skip Back', action: () => console.log('Skip Back') },
{ key: '→', label: 'Skip Forward', action: () => console.log('Skip Forward') },
{ key: '↑', label: 'Volume Up', action: () => console.log('Volume Up') },
{ key: '↓', label: 'Volume Down', action: () => console.log('Volume Down') },
],
// Navigation row
[
{ key: 'F', label: 'Fullscreen', action: () => console.log('Fullscreen') },
{ key: 'M', label: 'Mute', action: () => console.log('Mute') },
{ key: 'L', label: 'Playlist', action: () => console.log('Playlist') },
{ key: 'S', label: 'Settings', action: () => console.log('Settings') },
{ key: 'H', label: 'Help', action: () => console.log('Help') },
]
];
return (
<div className="min-h-screen bg-background p-4 flex flex-col">
<div className="text-center mb-6">
<h1 className="text-3xl font-bold text-foreground mb-2">How to use LLMs</h1>
<p className="text-muted-foreground">The guide to LLMs in tutorials</p>
</div>
<ResizablePanelGroup direction="horizontal" className="flex-grow">
<ResizablePanel defaultSize={70}>
<div className="flex-grow max-w-full mx-auto">
{/* Media Player */}
{selectedMedia && (
<div className="mb-6">
<MediaPlayer media={selectedMedia} />
</div>
)}
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={30}>
<div className="ml-4 h-full">
{/* Media Library */}
<MediaLibrary
mediaList={mediaLibrary}
onSelectMedia={setSelectedMedia}
selectedMedia={selectedMedia}
/>
</div>
</ResizablePanel>
</ResizablePanelGroup>
{/* Footer */}
<Footer shortcuts={shortcuts} />
</div>
);
};
export default CorporateMediaPlayer;