
All checks were successful
GBCI / build (push) Successful in 14m45s
- Added a new navigation style with responsive design in client-nav.css. - Created a comprehensive editor style in editor/style.css for better user experience. - Introduced paper style for ProseMirror editor with enhanced text formatting options. - Developed a media player component with waveform visualization and media library in player/page.tsx. - Styled media player controls and sliders for improved usability in player/style.css. - Implemented media type detection for audio, video, and slides. - Added keyboard shortcuts for media control and navigation.
470 lines
16 KiB
TypeScript
470 lines
16 KiB
TypeScript
"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;
|