feat(lightbox): custom immersive video player

Replace the native <video controls> element with a custom player.
Videos now fill the lightbox media area edge-to-edge on black instead
of sitting in a padded, rounded box, with an auto-hiding control bar
over a bottom gradient (2.5s idle, always visible while paused):
play/pause, drag scrubber with buffered ranges, time readout, volume
slider with session-persisted volume/mute, playback speed (0.25-2x),
loop, and fullscreen.

Click toggles playback, double-click toggles fullscreen. Keyboard:
Space play/pause, M mute, F fullscreen, L loop, Shift+Left/Right seek
5s, Up/Down volume (unmutes) — plain arrows still navigate between
media, and all player keys are ignored while typing in the info-panel
inputs. Videos autoplay on open, falling back to paused-with-controls
if the webview blocks it.
This commit is contained in:
2026-06-12 12:39:04 +01:00
parent b02bf1da2b
commit 86ce7bc8e2
2 changed files with 458 additions and 9 deletions
+10 -9
View File
@@ -2,6 +2,7 @@ import { useEffect, useCallback, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { convertFileSrc } from "@tauri-apps/api/core";
import { useGalleryStore, ImageTag, AiRating } from "../store";
import { VideoPlayer } from "./VideoPlayer";
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
@@ -223,8 +224,9 @@ export function Lightbox() {
}
}
if (regionSelectMode) return; // block nav keys during selection
if (event.key === "ArrowLeft") goPrev();
if (event.key === "ArrowRight") goNext();
// Shift+arrows are reserved for video seeking (handled by VideoPlayer)
if (event.key === "ArrowLeft" && !event.shiftKey) goPrev();
if (event.key === "ArrowRight" && !event.shiftKey) goNext();
if (event.key === "+" || event.key === "=") setZoom((value) => Math.min(3, value + 0.25));
if (event.key === "-") setZoom((value) => Math.max(0.75, value - 0.25));
};
@@ -365,19 +367,18 @@ export function Lightbox() {
<AnimatePresence mode="wait">
<motion.div
key={selectedImage.id}
className="flex items-center justify-center"
className={
selectedImage.media_kind === "video"
? "absolute inset-0"
: "flex items-center justify-center"
}
initial={{ opacity: 0.3, scale: 0.985 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0.3, scale: 0.985 }}
transition={{ duration: 0.12 }}
>
{selectedImage.media_kind === "video" ? (
<video
src={convertFileSrc(selectedImage.path)}
controls
className="max-h-full max-w-full rounded-2xl shadow-2xl"
style={{ maxHeight: "calc(100vh - 10rem)" }}
/>
<VideoPlayer src={convertFileSrc(selectedImage.path)} />
) : (
<>
<img