diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 856cb17..e28db86 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -68,11 +68,25 @@ function App() { } + element={ + `/clips/${id}/edit`} + /> + } /> } + element={ + `/vods/${id}/edit`} + /> + } /> { />
- +
diff --git a/frontend/src/pages/MediaPlayer.tsx b/frontend/src/pages/MediaPlayer.tsx index 64b02c2..c1b4a15 100644 --- a/frontend/src/pages/MediaPlayer.tsx +++ b/frontend/src/pages/MediaPlayer.tsx @@ -1,22 +1,26 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { Link, useNavigate, useParams } from "react-router-dom"; import { - ArrowLeft, Lock, Maximize, Minimize, Pause, Play, - RotateCcw, RotateCw, Volume1, Volume2, VolumeX, + ArrowLeft, CalendarDays, Check, Clock, Download, Gauge, Keyboard, Link2, + Lock, Maximize, Minimize, MonitorPlay, Pause, Pencil, PictureInPicture2, + Play, RotateCcw, RotateCw, Volume1, Volume2, VolumeX, } from "lucide-react"; import clsx from "clsx"; import { AuthError } from "../utils/api/client.ts"; import Card from "../components/ui/Card.tsx"; import Button from "../components/ui/Button.tsx"; +import Badge from "../components/ui/Badge.tsx"; import EmptyState from "../components/ui/EmptyState.tsx"; import Spinner from "../components/ui/Spinner.tsx"; +import Menu, { MenuItem } from "../components/ui/Menu.tsx"; import { dateToTimeAgo, formatTime, stringToDate } from "../utils/utils.ts"; // Both VoDs and clips share the same on-screen shape. type MediaItem = { title: string, description?: string, + duration?: number, createdAt: string, }; @@ -28,20 +32,61 @@ type MediaPlayerProps = { getMediaUrl: (id: string) => string, /** Fetches the media metadata for the given id. */ fetchDetails: (id: string) => Promise, + /** Where the "Edit details" action goes. */ + editPath: (id: string) => string, }; -const ControlButton = ({ label, onClick, children }: { label: string; onClick: () => void; children: React.ReactNode }) => ( +const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2]; + +const SHORTCUTS: [string, string][] = [ + ["Space / K", "Play or pause"], + ["← / →", "Skip 5 seconds"], + ["J / L", "Skip 10 seconds"], + ["↑ / ↓", "Volume"], + ["0 – 9", "Jump to 0–90%"], + ["M", "Mute"], + ["F", "Fullscreen"], + ["P", "Picture in picture"], +]; + +/** Turns a frame height into the label people actually use for it. */ +const qualityLabel = (height: number) => { + if (height >= 2160) return "4K"; + if (height >= 1440) return "1440p"; + if (height >= 1080) return "1080p"; + if (height >= 720) return "720p"; + if (height >= 480) return "480p"; + return `${height}p`; +}; + +const Stat = ({ icon: Icon, label, value }: { icon: typeof Clock; label: string; value: string }) => ( +
+ + {label} + {value} +
+); + +const ControlButton = ({ label, onClick, active = false, children }: { + label: string; + onClick: () => void; + active?: boolean; + children: React.ReactNode; +}) => ( ); -const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => { +const MediaPlayer = ({ noun, getMediaUrl, fetchDetails, editPath }: MediaPlayerProps) => { const { id } = useParams(); const navigate = useNavigate(); @@ -64,6 +109,8 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => { const [duration, setDuration] = useState(0); const [bufferedPct, setBufferedPct] = useState(0); const [isWaiting, setIsWaiting] = useState(false); + const [speed, setSpeed] = useState(1); + const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null); // Volume const [volume, setVolume] = useState(1); @@ -71,8 +118,13 @@ const MediaPlayer = ({ noun, getMediaUrl, fetchDetails }: MediaPlayerProps) => { // UI const [isFullscreen, setIsFullscreen] = useState(false); + const [isPip, setIsPip] = useState(false); const [controlsVisible, setControlsVisible] = useState(true); const [isDragging, setIsDragging] = useState(false); + const [hoverTime, setHoverTime] = useState(null); + const [showRemaining, setShowRemaining] = useState(false); + const [copied, setCopied] = useState(false); + const [shortcutsOpen, setShortcutsOpen] = useState(false); // ── Data loading ──────────────────────────────────────────────────────── // The video stream itself is loaded by the