Compare commits
2 Commits
096f13d926
...
d6b50164e4
| Author | SHA1 | Date | |
|---|---|---|---|
| d6b50164e4 | |||
| b8d3041035 |
|
|
@ -4,7 +4,7 @@ import { motion, AnimatePresence } from 'motion/react';
|
|||
import { toast } from 'sonner';
|
||||
import { fileExists, getFileContents, joinPath } from '../webdav';
|
||||
import MediaBrowser from './MediaBrowser';
|
||||
import MediaSet from './MediaSet';
|
||||
import MediaSet, { type MediaSetEntry } from './MediaSet';
|
||||
|
||||
interface Device {
|
||||
id: string;
|
||||
|
|
@ -122,11 +122,11 @@ export default function DeviceDetailOverlay({
|
|||
};
|
||||
|
||||
// Prefer an explicit .lst-derived mediaSet stored in config; fall back to pattern detection.
|
||||
const [mediaSetFiles, setMediaSetFiles] = useState<string[] | null>(null);
|
||||
const [mediaSetFiles, setMediaSetFiles] = useState<MediaSetEntry[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(deviceData.media_set) && deviceData.media_set.length > 0) {
|
||||
setMediaSetFiles(deviceData.media_set as string[]);
|
||||
setMediaSetFiles(deviceData.media_set as MediaSetEntry[]);
|
||||
return;
|
||||
}
|
||||
if (!deviceData.url) { setMediaSetFiles(null); return; }
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface Props {
|
|||
export default function DirectorySlideshow({ path }: Props) {
|
||||
const [images, setImages] = useState<EntryInfo[]>([]);
|
||||
const [idx, setIdx] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [paused, setPaused] = useState(() => localStorage.getItem('slideshow.paused') === '1');
|
||||
const [touched, setTouched] = useState(false);
|
||||
const touchTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
|
||||
|
|
@ -24,7 +24,8 @@ export default function DirectorySlideshow({ path }: Props) {
|
|||
return IMAGE_EXTS.has(ext);
|
||||
});
|
||||
setImages(imgs);
|
||||
setIdx(0);
|
||||
const saved = parseInt(localStorage.getItem(`slideshow.idx:${path}`) ?? '0', 10);
|
||||
setIdx(imgs.length > 0 ? Math.min(saved, imgs.length - 1) : 0);
|
||||
})
|
||||
.catch(() => setImages([]));
|
||||
}, [path]);
|
||||
|
|
@ -35,6 +36,11 @@ export default function DirectorySlideshow({ path }: Props) {
|
|||
return () => clearInterval(t);
|
||||
}, [images.length, paused]);
|
||||
|
||||
useEffect(() => {
|
||||
if (images.length === 0) return;
|
||||
localStorage.setItem(`slideshow.idx:${path}`, String(idx));
|
||||
}, [idx, path, images.length]);
|
||||
|
||||
useEffect(() => () => clearTimeout(touchTimer.current), []);
|
||||
|
||||
if (images.length === 0) return null;
|
||||
|
|
@ -73,23 +79,25 @@ export default function DirectorySlideshow({ path }: Props) {
|
|||
</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}`}>
|
||||
{/* Pause / play — centered */}
|
||||
<div className={`absolute inset-0 flex items-center justify-center pointer-events-none 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"
|
||||
onClick={() => setPaused(p => { const next = !p; localStorage.setItem('slideshow.paused', next ? '1' : '0'); return next; })}
|
||||
className="pointer-events-auto p-3 rounded-full bg-black/50 text-white hover:bg-black/70"
|
||||
>
|
||||
{paused ? <Play className="w-3 h-3" /> : <Pause className="w-3 h-3" />}
|
||||
{paused ? <Play className="w-7 h-7" /> : <Pause className="w-7 h-7" />}
|
||||
</button>
|
||||
<div className="flex gap-1.5">
|
||||
{images.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setIdx(i)}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-colors ${i === idx ? 'bg-white' : 'bg-white/40'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setIdx(i)}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-colors ${i === idx ? 'bg-white' : 'bg-white/40'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
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 {
|
||||
files: string[];
|
||||
files: MediaSetEntry[];
|
||||
activeUrl: string;
|
||||
onSwitch: (file: string) => void;
|
||||
onSwitch: (url: string) => void;
|
||||
}
|
||||
|
||||
export default function MediaSet({ files, activeUrl, onSwitch }: MediaSetProps) {
|
||||
|
|
@ -9,21 +22,21 @@ export default function MediaSet({ files, activeUrl, onSwitch }: MediaSetProps)
|
|||
<div>
|
||||
<div className="text-sm text-neutral-500 mb-2">Media Set</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{files.map((file, index) => {
|
||||
const fileName = file.split('/').pop() || file;
|
||||
const title = fileName.replace(/\.[^.]+$/, '');
|
||||
const active = activeUrl === file;
|
||||
{files.map((entry, index) => {
|
||||
const url = mediaSetEntryUrl(entry);
|
||||
const label = entryLabel(entry, index);
|
||||
const active = activeUrl === url;
|
||||
return (
|
||||
<button
|
||||
key={file}
|
||||
onClick={() => onSwitch(file)}
|
||||
key={url}
|
||||
onClick={() => onSwitch(url)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm ${
|
||||
active
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
|
||||
}`}
|
||||
>
|
||||
{`${index + 1}: ${title}`}
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { MediaEntry } from './MediaEntry';
|
|||
import DirectorySlideshow from './DirectorySlideshow';
|
||||
import { useWs } from '../ws';
|
||||
import DeviceDetailOverlay from './DeviceDetailOverlay';
|
||||
import MediaSet from './MediaSet';
|
||||
import MediaSet, { type MediaSetEntry } from './MediaSet';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/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)
|
||||
: null;
|
||||
|
||||
const mediaSetFiles: string[] | null = (() => {
|
||||
const mediaSetFiles: MediaSetEntry[] | null = (() => {
|
||||
if (!activeDevice?.url) return null;
|
||||
if (Array.isArray(activeDevice.media_set) && activeDevice.media_set.length > 0)
|
||||
return activeDevice.media_set as string[];
|
||||
return activeDevice.media_set as MediaSetEntry[];
|
||||
const match = (activeDevice.url as string).match(/^(.+?)(\d+)(\.[^.]+)$/);
|
||||
if (!match) return null;
|
||||
const [, prefix, , ext] = match;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user