rtube-online/app/demo/page.tsx

197 lines
7.3 KiB
TypeScript

'use client'
import { useState, useEffect, useRef } from 'react'
import Link from 'next/link'
import { AppSwitcher } from '@/components/AppSwitcher'
interface Video {
name: string
size: number
}
function formatSize(bytes: number): string {
if (!bytes) return ''
const units = ['B', 'KB', 'MB', 'GB']
let i = 0
let b = bytes
while (b >= 1024 && i < units.length - 1) {
b /= 1024
i++
}
return `${b.toFixed(1)} ${units[i]}`
}
function getIcon(filename: string): string {
const ext = filename.split('.').pop()?.toLowerCase() || ''
if (['mp4', 'webm', 'mov'].includes(ext)) return '\uD83C\uDFAC'
if (['mkv', 'avi', 'wmv', 'flv'].includes(ext)) return '\u26A0\uFE0F'
return '\uD83D\uDCC4'
}
function isPlayable(filename: string): boolean {
const ext = filename.split('.').pop()?.toLowerCase() || ''
return ['mp4', 'webm', 'mov', 'ogg', 'm4v'].includes(ext)
}
export default function DemoPage() {
const [videos, setVideos] = useState<Video[]>([])
const [filteredVideos, setFilteredVideos] = useState<Video[]>([])
const [currentVideo, setCurrentVideo] = useState<string | null>(null)
const [search, setSearch] = useState('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
fetch('/api/videos')
.then((res) => {
if (!res.ok) throw new Error('Failed to load videos')
return res.json()
})
.then((data) => {
setVideos(data)
setFilteredVideos(data)
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [])
useEffect(() => {
const q = search.toLowerCase()
setFilteredVideos(videos.filter((v) => v.name.toLowerCase().includes(q)))
}, [search, videos])
function playVideo(key: string) {
setCurrentVideo(key)
}
function copyLink() {
if (!currentVideo) return
navigator.clipboard.writeText(`${window.location.origin}/api/v/${encodeURIComponent(currentVideo)}`)
}
const ext = currentVideo?.split('.').pop()?.toLowerCase() || ''
const playable = currentVideo ? isPlayable(currentVideo) : false
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
{/* Nav */}
<nav className="border-b border-slate-700/50 backdrop-blur-sm">
<div className="max-w-[1400px] mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<AppSwitcher current="tube" />
<Link href="/" className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-red-500 to-pink-600 rounded-lg flex items-center justify-center font-bold text-white text-sm">
rT
</div>
<span className="font-semibold text-lg">rTube</span>
</Link>
<span className="text-slate-500 ml-2">/ Video Library</span>
</div>
<Link
href="/live"
className="text-sm px-4 py-2 bg-red-600 hover:bg-red-500 rounded-lg transition-colors font-medium"
>
Go Live
</Link>
</div>
</nav>
<div className="max-w-[1400px] mx-auto px-6 py-8">
<h1 className="text-3xl font-bold text-center mb-8 text-red-400">Video Library</h1>
<div className="grid grid-cols-1 lg:grid-cols-[300px_1fr] gap-8">
{/* Sidebar */}
<div className="bg-slate-800/50 rounded-2xl p-4 border border-slate-700/50 max-h-[80vh] overflow-y-auto">
<input
type="text"
placeholder="Search videos..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full px-4 py-3 bg-black/30 border border-slate-700 rounded-lg text-white placeholder-slate-500 mb-4 text-sm focus:outline-none focus:border-red-500"
/>
<h2 className="text-xs uppercase tracking-wider text-slate-500 mb-3">Library</h2>
{loading && <p className="text-slate-500 text-sm p-4">Loading videos...</p>}
{error && <p className="text-red-400 text-sm p-4 bg-red-500/10 rounded-lg">Error: {error}</p>}
<ul className="space-y-1">
{filteredVideos.map((v) => (
<li
key={v.name}
onClick={() => playVideo(v.name)}
className={`flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all text-sm
${currentVideo === v.name ? 'bg-red-500/20 border-l-2 border-red-500' : 'hover:bg-slate-700/50'}
${!isPlayable(v.name) ? 'opacity-60' : ''}`}
>
<span>{getIcon(v.name)}</span>
<span className="flex-1 truncate" title={v.name}>{v.name}</span>
<span className="text-xs text-slate-600 shrink-0">{formatSize(v.size)}</span>
</li>
))}
{!loading && filteredVideos.length === 0 && (
<li className="text-slate-500 text-sm p-4">No videos found</li>
)}
</ul>
</div>
{/* Player */}
<div>
<div className="bg-black rounded-2xl overflow-hidden aspect-video flex items-center justify-center">
{!currentVideo && (
<p className="text-slate-600 text-lg">Select a video to play</p>
)}
{currentVideo && !playable && (
<div className="text-center p-8">
<p className="text-4xl mb-4">{'\u26A0\uFE0F'}</p>
<p><strong>{ext.toUpperCase()}</strong> files cannot play in browsers</p>
<p className="text-sm text-slate-500 mt-2">Download to play locally, or re-record in MP4 format</p>
</div>
)}
{currentVideo && playable && (
<video
ref={videoRef}
key={currentVideo}
controls
autoPlay
preload="auto"
className="w-full h-full"
>
<source
src={`/api/v/${encodeURIComponent(currentVideo)}`}
type={ext === 'webm' ? 'video/webm' : 'video/mp4'}
/>
</video>
)}
</div>
{currentVideo && (
<div className="mt-4 bg-slate-800/50 rounded-xl p-4 border border-slate-700/50 flex items-center justify-between flex-wrap gap-4">
<p className="font-medium">{currentVideo}</p>
<div className="flex gap-2">
<a
href={`/api/v/${encodeURIComponent(currentVideo)}`}
download
className="px-4 py-2 bg-slate-700/50 hover:bg-slate-600/50 rounded-lg text-sm transition-colors"
>
Download
</a>
<button
onClick={copyLink}
className="px-4 py-2 bg-slate-700/50 hover:bg-slate-600/50 rounded-lg text-sm transition-colors"
>
Copy Link
</button>
</div>
</div>
)}
</div>
</div>
</div>
</div>
)
}