Compare commits

..

No commits in common. "d6b50164e45c3b6286ab61f1b6cca90e7acc9e8c" and "096f13d9267b0944677db57389fa2b3fe765843a" have entirely different histories.

4 changed files with 31 additions and 52 deletions

View File

@ -4,7 +4,7 @@ import { motion, AnimatePresence } from 'motion/react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { fileExists, getFileContents, joinPath } from '../webdav'; import { fileExists, getFileContents, joinPath } from '../webdav';
import MediaBrowser from './MediaBrowser'; import MediaBrowser from './MediaBrowser';
import MediaSet, { type MediaSetEntry } from './MediaSet'; import MediaSet from './MediaSet';
interface Device { interface Device {
id: string; id: string;
@ -122,11 +122,11 @@ export default function DeviceDetailOverlay({
}; };
// Prefer an explicit .lst-derived mediaSet stored in config; fall back to pattern detection. // Prefer an explicit .lst-derived mediaSet stored in config; fall back to pattern detection.
const [mediaSetFiles, setMediaSetFiles] = useState<MediaSetEntry[] | null>(null); const [mediaSetFiles, setMediaSetFiles] = useState<string[] | null>(null);
useEffect(() => { useEffect(() => {
if (Array.isArray(deviceData.media_set) && deviceData.media_set.length > 0) { if (Array.isArray(deviceData.media_set) && deviceData.media_set.length > 0) {
setMediaSetFiles(deviceData.media_set as MediaSetEntry[]); setMediaSetFiles(deviceData.media_set as string[]);
return; return;
} }
if (!deviceData.url) { setMediaSetFiles(null); return; } if (!deviceData.url) { setMediaSetFiles(null); return; }

View File

@ -10,7 +10,7 @@ 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(() => localStorage.getItem('slideshow.paused') === '1'); const [paused, setPaused] = useState(false);
const [touched, setTouched] = useState(false); const [touched, setTouched] = useState(false);
const touchTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const touchTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
@ -24,8 +24,7 @@ export default function DirectorySlideshow({ path }: Props) {
return IMAGE_EXTS.has(ext); return IMAGE_EXTS.has(ext);
}); });
setImages(imgs); setImages(imgs);
const saved = parseInt(localStorage.getItem(`slideshow.idx:${path}`) ?? '0', 10); setIdx(0);
setIdx(imgs.length > 0 ? Math.min(saved, imgs.length - 1) : 0);
}) })
.catch(() => setImages([])); .catch(() => setImages([]));
}, [path]); }, [path]);
@ -36,11 +35,6 @@ export default function DirectorySlideshow({ path }: Props) {
return () => clearInterval(t); return () => clearInterval(t);
}, [images.length, paused]); }, [images.length, paused]);
useEffect(() => {
if (images.length === 0) return;
localStorage.setItem(`slideshow.idx:${path}`, String(idx));
}, [idx, path, images.length]);
useEffect(() => () => clearTimeout(touchTimer.current), []); useEffect(() => () => clearTimeout(touchTimer.current), []);
if (images.length === 0) return null; if (images.length === 0) return null;
@ -79,18 +73,15 @@ export default function DirectorySlideshow({ path }: Props) {
</button> </button>
</div> </div>
{/* Pause / play — centered */} {/* Dots + pause */}
<div className={`absolute inset-0 flex items-center justify-center pointer-events-none transition-opacity duration-200 ${controlsVisible}`}> <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 <button
onClick={() => setPaused(p => { const next = !p; localStorage.setItem('slideshow.paused', next ? '1' : '0'); return next; })} onClick={() => setPaused(p => !p)}
className="pointer-events-auto p-3 rounded-full bg-black/50 text-white hover:bg-black/70" className="p-0.5 rounded-full bg-black/50 text-white hover:bg-black/70"
> >
{paused ? <Play className="w-7 h-7" /> : <Pause className="w-7 h-7" />} {paused ? <Play className="w-3 h-3" /> : <Pause className="w-3 h-3" />}
</button> </button>
</div> <div className="flex gap-1.5">
{/* Dots */}
<div className={`absolute bottom-0 left-0 right-0 flex justify-center gap-1.5 py-1.5 transition-opacity duration-200 ${controlsVisible}`}>
{images.map((_, i) => ( {images.map((_, i) => (
<button <button
key={i} key={i}
@ -99,6 +90,7 @@ export default function DirectorySlideshow({ path }: Props) {
/> />
))} ))}
</div> </div>
</div>
</> </>
)} )}
</div> </div>

View File

@ -1,20 +1,7 @@
export type MediaSetEntry = string | { url: string; name?: string };
export function mediaSetEntryUrl(e: MediaSetEntry): string {
return typeof e === 'string' ? e : e.url;
}
function entryLabel(e: MediaSetEntry, index: number): string {
if (typeof e === 'object' && e.name) return `${index + 1}: ${e.name}`;
const file = mediaSetEntryUrl(e);
const fileName = file.split('/').pop() || file;
return `${index + 1}: ${fileName.replace(/\.[^.]+$/, '')}`;
}
interface MediaSetProps { interface MediaSetProps {
files: MediaSetEntry[]; files: string[];
activeUrl: string; activeUrl: string;
onSwitch: (url: string) => void; onSwitch: (file: string) => void;
} }
export default function MediaSet({ files, activeUrl, onSwitch }: MediaSetProps) { export default function MediaSet({ files, activeUrl, onSwitch }: MediaSetProps) {
@ -22,21 +9,21 @@ export default function MediaSet({ files, activeUrl, onSwitch }: MediaSetProps)
<div> <div>
<div className="text-sm text-neutral-500 mb-2">Media Set</div> <div className="text-sm text-neutral-500 mb-2">Media Set</div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{files.map((entry, index) => { {files.map((file, index) => {
const url = mediaSetEntryUrl(entry); const fileName = file.split('/').pop() || file;
const label = entryLabel(entry, index); const title = fileName.replace(/\.[^.]+$/, '');
const active = activeUrl === url; const active = activeUrl === file;
return ( return (
<button <button
key={url} key={file}
onClick={() => onSwitch(url)} onClick={() => onSwitch(file)}
className={`px-3 py-1.5 rounded-lg text-sm ${ className={`px-3 py-1.5 rounded-lg text-sm ${
active active
? 'bg-blue-600 text-white' ? 'bg-blue-600 text-white'
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200' : 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
}`} }`}
> >
{label} {`${index + 1}: ${title}`}
</button> </button>
); );
})} })}

View File

@ -6,7 +6,7 @@ import { MediaEntry } from './MediaEntry';
import DirectorySlideshow from './DirectorySlideshow'; import DirectorySlideshow from './DirectorySlideshow';
import { useWs } from '../ws'; import { useWs } from '../ws';
import DeviceDetailOverlay from './DeviceDetailOverlay'; import DeviceDetailOverlay from './DeviceDetailOverlay';
import MediaSet, { type MediaSetEntry } from './MediaSet'; import MediaSet from './MediaSet';
import { ImageWithFallback } from './figma/ImageWithFallback'; import { ImageWithFallback } from './figma/ImageWithFallback';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog'; import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog';
@ -45,10 +45,10 @@ export default function StatusPage({ config, setConfig, onOpenFileManager }: Sta
? (activeDevice.base_url || splitPath(activeDevice.url || '/').parent) ? (activeDevice.base_url || splitPath(activeDevice.url || '/').parent)
: null; : null;
const mediaSetFiles: MediaSetEntry[] | null = (() => { const mediaSetFiles: string[] | null = (() => {
if (!activeDevice?.url) return null; if (!activeDevice?.url) return null;
if (Array.isArray(activeDevice.media_set) && activeDevice.media_set.length > 0) if (Array.isArray(activeDevice.media_set) && activeDevice.media_set.length > 0)
return activeDevice.media_set as MediaSetEntry[]; return activeDevice.media_set as string[];
const match = (activeDevice.url as string).match(/^(.+?)(\d+)(\.[^.]+)$/); const match = (activeDevice.url as string).match(/^(.+?)(\d+)(\.[^.]+)$/);
if (!match) return null; if (!match) return null;
const [, prefix, , ext] = match; const [, prefix, , ext] = match;