diff --git a/photomap/frontend/static/css/control-and-search-panels.css b/photomap/frontend/static/css/control-and-search-panels.css index 5b27c1c5..b3e745a0 100644 --- a/photomap/frontend/static/css/control-and-search-panels.css +++ b/photomap/frontend/static/css/control-and-search-panels.css @@ -56,7 +56,9 @@ } #controlPanel button.back-nav-disabled, -#controlPanel button.back-nav-disabled:hover { +#controlPanel button.back-nav-disabled:hover, +#controlPanel button.slideshow-disabled, +#controlPanel button.slideshow-disabled:hover { opacity: 0.25; cursor: default; } diff --git a/photomap/frontend/static/javascript/events.js b/photomap/frontend/static/javascript/events.js index 9f7497da..f9bf2b57 100644 --- a/photomap/frontend/static/javascript/events.js +++ b/photomap/frontend/static/javascript/events.js @@ -205,7 +205,9 @@ export function showHidePanelText(hide) { // Listen for slide changes to update UI window.addEventListener("slideChanged", () => { - // nothing to do here yet, but could be used to update UI elements + // Refresh the play/pause button so it grays out when we land on the last + // slide in sequential mode (and re-activates when we navigate away). + updateSlideshowButtonIcon(); }); // Toggle grid/swiper views diff --git a/photomap/frontend/static/javascript/slideshow.js b/photomap/frontend/static/javascript/slideshow.js index 11ba50a4..0d81b15c 100644 --- a/photomap/frontend/static/javascript/slideshow.js +++ b/photomap/frontend/static/javascript/slideshow.js @@ -1,4 +1,5 @@ import { saveSettingsToLocalStorage, state } from "./state.js"; +import { slideState } from "./slide-state.js"; import { isUmapFullscreen, toggleUmapWindow } from "./umap.js"; // SVG icons used for the button/menu @@ -15,6 +16,14 @@ export function slideShowRunning() { return !!state.single_swiper?.swiper?.autoplay?.running; } +// In sequential mode there is a genuine end of the list, so a stopped slideshow +// resting on the final slide has nowhere to advance to. resolveOffset(+1) +// reports null only at that end (wrap mode always resolves to a real index, and +// random mode has no end), so it cleanly captures the "last slide" case. +function atSequentialEnd() { + return state.mode !== "random" && slideState.resolveOffset(+1).globalIndex === null; +} + // public: update the icon displayed on the start/stop button according to state export function updateSlideshowButtonIcon() { const container = document.getElementById("slideshowIcon"); @@ -42,6 +51,13 @@ export function updateSlideshowButtonIcon() { btn.title = `Start Slideshow (${modeLabel})`; } } + + // Gray out and disable Play when stopped on the final slide in sequential + // mode — there is nothing left to play. Never disable the Pause button. + if (btn) { + const disabled = !isRunning && atSequentialEnd(); + btn.classList.toggle("slideshow-disabled", disabled); + } } // small fullscreen/play indicator (moved from events.js) @@ -92,6 +108,12 @@ export async function toggleSlideshowWithIndicator(e) { e.stopPropagation(); } + // Ignore clicks while parked on the last sequential slide (button is grayed + // out): there is nothing to start. + if (!slideShowRunning() && atSequentialEnd()) { + return; + } + if (slideShowRunning()) { // pause try {