feat(DirectorySlideshow): add navigation controls and pause functionality for image slideshow

This commit is contained in:
Jaime Idolpx 2026-06-11 12:47:12 -04:00
parent 290cdb8ae9
commit 096f13d926

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { ChevronLeft, ChevronRight, Pause, Play } from 'lucide-react';
import { listDirectory, getWebDAVBaseUrl, type EntryInfo } from '../webdav'; import { listDirectory, getWebDAVBaseUrl, type EntryInfo } from '../webdav';
import { IMAGE_EXTS } from './MediaEntry'; import { IMAGE_EXTS } from './MediaEntry';
@ -9,6 +10,9 @@ interface Props {
export default function DirectorySlideshow({ path }: Props) { export default function DirectorySlideshow({ path }: Props) {
const [images, setImages] = useState<EntryInfo[]>([]); const [images, setImages] = useState<EntryInfo[]>([]);
const [idx, setIdx] = useState(0); const [idx, setIdx] = useState(0);
const [paused, setPaused] = useState(false);
const [touched, setTouched] = useState(false);
const touchTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (!path) { setImages([]); return; } if (!path) { setImages([]); return; }
@ -26,25 +30,58 @@ export default function DirectorySlideshow({ path }: Props) {
}, [path]); }, [path]);
useEffect(() => { useEffect(() => {
if (images.length <= 1) return; if (images.length <= 1 || paused) return;
const t = setInterval(() => setIdx(i => (i + 1) % images.length), 4000); const t = setInterval(() => setIdx(i => (i + 1) % images.length), 4000);
return () => clearInterval(t); return () => clearInterval(t);
}, [images.length]); }, [images.length, paused]);
useEffect(() => () => clearTimeout(touchTimer.current), []);
if (images.length === 0) return null; if (images.length === 0) return null;
const current = images[idx]; const current = images[idx];
const prev = () => setIdx(i => (i - 1 + images.length) % images.length);
const next = () => setIdx(i => (i + 1) % images.length);
const handleTouch = () => {
setTouched(true);
clearTimeout(touchTimer.current);
touchTimer.current = setTimeout(() => setTouched(false), 3000);
};
const controlsVisible = touched ? 'opacity-100' : 'opacity-0 group-hover:opacity-100';
return ( return (
<div className="mb-3 rounded-lg overflow-hidden"> <div className="mb-3 rounded-lg overflow-hidden">
<div className="relative group h-48" onPointerDown={handleTouch}>
<img <img
key={current.path} key={current.path}
src={getWebDAVBaseUrl() + current.path} src={getWebDAVBaseUrl() + current.path}
alt={current.name} alt={current.name}
className="w-full h-48 object-contain" className="w-full h-full object-contain"
/> />
{images.length > 1 && ( {images.length > 1 && (
<div className="flex justify-center gap-1.5 py-1.5"> <>
{/* Prev / next */}
<div className={`absolute inset-0 flex items-center justify-between px-2 pointer-events-none transition-opacity duration-200 ${controlsVisible}`}>
<button onClick={prev} className="pointer-events-auto p-1 rounded-full bg-black/50 text-white hover:bg-black/70">
<ChevronLeft className="w-5 h-5" />
</button>
<button onClick={next} className="pointer-events-auto p-1 rounded-full bg-black/50 text-white hover:bg-black/70">
<ChevronRight className="w-5 h-5" />
</button>
</div>
{/* Dots + pause */}
<div className={`absolute bottom-0 left-0 right-0 flex items-center justify-center gap-2 py-1.5 transition-opacity duration-200 ${controlsVisible}`}>
<button
onClick={() => setPaused(p => !p)}
className="p-0.5 rounded-full bg-black/50 text-white hover:bg-black/70"
>
{paused ? <Play className="w-3 h-3" /> : <Pause className="w-3 h-3" />}
</button>
<div className="flex gap-1.5">
{images.map((_, i) => ( {images.map((_, i) => (
<button <button
key={i} key={i}
@ -53,7 +90,10 @@ export default function DirectorySlideshow({ path }: Props) {
/> />
))} ))}
</div> </div>
</div>
</>
)} )}
</div> </div>
</div>
); );
} }