Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions photomap/frontend/static/css/video-player.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* video-player.css */
/* Modal video player, opened from the play badge on a video still. */

/* The base .modal-content caps at 600px, which letterboxes a 1080p clip into
a stamp. The player gets a much wider box and a darker backdrop. */
#videoPlayerModal {
background: rgba(0, 0, 0, 0.82);
}

.video-player-content {
width: min(92vw, 1400px);
max-width: min(92vw, 1400px);
padding: 2.2em 1.2em 1.2em;
display: flex;
flex-direction: column;
gap: 0.75em;
}

.video-player-title {
color: #ddd;
font-size: 0.95rem;
text-align: center;
overflow-wrap: anywhere;
}

.video-player-title:empty {
display: none;
}

.video-player-element {
width: 100%;
/* Leave room for the title and close button so the controls are never
pushed off the bottom of a short viewport. */
max-height: 78vh;
background: #000;
border-radius: 6px;
display: block;
}

.video-player-element[hidden] {
display: none;
}

/* Shown when the container is one browsers cannot play, or when the <video>
element reports an error. */
.video-player-fallback {
padding: 2em 1em;
text-align: center;
color: #ddd;
}

.video-player-fallback[hidden] {
display: none;
}

.video-player-fallback p {
margin: 0 0 1em;
}

.video-player-download {
display: inline-block;
padding: 0.55em 1.2em;
background: #3a3a3a;
color: #fff;
border-radius: 6px;
text-decoration: none;
}

.video-player-download:hover,
.video-player-download:focus-visible {
background: #4a4a4a;
}

.video-player-download[hidden] {
display: none;
}
16 changes: 16 additions & 0 deletions photomap/frontend/static/javascript/control-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ function toggleFullscreen() {
}
}

// Panel visibility follows document.fullscreenElement and nothing else.
//
// A <video controls> element has its own fullscreen button, which makes this
// fire with the video as the fullscreen element and again with none on the
// way out. Suppressing the handler while the video player is open looks like
// the fix and is worse than the problem: if the app leaves fullscreen while
// the modal is open — which is what pressing Escape in fullscreen does, since
// browsers consume that keydown to exit rather than delivering it to the page
// — the panels keep .hidden-fullscreen (opacity:0 + visibility:hidden, both
// !important) after the modal closes, and nothing restores them until the
// user happens to toggle fullscreen twice.
//
// Letting every transition through is self-correcting instead. The video's
// own fullscreen is entered and left in pairs, so the class ends up where it
// started, and while the modal is open its backdrop sits at z-index 99999 —
// so no intermediate state is ever visible to the user anyway.
function handleFullscreenChange() {
const isFullscreen = !!document.fullscreenElement;

Expand Down
42 changes: 40 additions & 2 deletions photomap/frontend/static/javascript/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { initializeSingleSwiper } from "./swiper.js";
import {} from "./touch.js"; // Import touch event handlers
import { isUmapFullscreen, toggleUmapWindow } from "./umap.js";
import { setCheckmarkOnIcon } from "./utils.js";
import { closeVideoPlayer, initializeVideoPlayer, isVideoPlayerOpen } from "./video-player.js";

// MAIN INITIALIZATION FUNCTIONS
// Initialize event listeners after the DOM is fully loaded
Expand Down Expand Up @@ -75,7 +76,15 @@ const KEYBOARD_SHORTCUTS = {
ArrowLeft: () => pauseSlideshow(),
ArrowRight: () => pauseSlideshow(),
i: () => toggleMetadataOverlay(),
Escape: () => hideMetadataOverlay(),
Escape: () => {
// The video player is the topmost layer, so Escape dismisses it first
// and leaves the metadata drawer alone.
if (isVideoPlayerOpen()) {
closeVideoPlayer();
return;
}
hideMetadataOverlay();
},
f: () => toggleFullscreen(),
g: () => toggleGridSwiperView(),
m: () => toggleUmapWindow(),
Expand All @@ -85,13 +94,24 @@ const KEYBOARD_SHORTCUTS = {
Backspace: (e) => handleBackKey(e),
};

// Keys that must still reach KEYBOARD_SHORTCUTS while the video player is
// open, because their handlers are how it gets dismissed.
const PLAYER_DISMISS_KEYS = new Set(["Escape", "Backspace"]);

function pauseSlideshow() {
state.single_swiper?.pauseSlideshow();
}

function handleBackKey(e) {
e.preventDefault();
e.stopPropagation();
// The player is a transient overlay, not a navigation state — it pushes no
// history entry, so Back dismisses it rather than moving the slideshow
// underneath it.
if (isVideoPlayerOpen()) {
closeVideoPlayer();
return;
}
backStack.popOne();
}

Expand All @@ -116,13 +136,31 @@ function handleKeydown(e) {
}

function shouldIgnoreKeyEvent(e) {
return e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.isContentEditable;
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.isContentEditable) {
return true;
}
// While the video player is open the native <video> controls own the
// keyboard. Space would otherwise be swallowed by handleSpacebarToggle
// (which preventDefaults it) and toggle the slideshow instead of pausing
// the video, and the arrows would change slides behind the modal.
//
// Escape and Backspace are exempt: both are ways to dismiss the player, and
// their handlers check for it. Leaving Backspace out made the guard inside
// handleBackKey unreachable — this returns before KEYBOARD_SHORTCUTS is
// consulted, so the shortcut never ran and the key silently did nothing.
if (isVideoPlayerOpen() && !PLAYER_DISMISS_KEYS.has(e.key)) {
return true;
}
return false;
}

function setupGlobalEventListeners() {
// Keyboard navigation
window.addEventListener("keydown", handleKeydown);

// Modal video player, opened by the play badge on a video still.
initializeVideoPlayer();

// Window resize event
window.addEventListener("resize", positionMetadataDrawer);

Expand Down
7 changes: 6 additions & 1 deletion photomap/frontend/static/javascript/touch.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,12 @@ function handleTouchEnd(e) {
document.msFullscreenElement
);

if (isTap && isFullscreen) {
// A tap on a control — the play badge, the player's own buttons — is not a
// tap on the slide. Without this, tapping the play badge in fullscreen both
// opens the player and toggles the slideshow behind it.
const tappedAControl = e.target?.closest?.("button, .video-badge, #videoPlayerModal");

if (isTap && isFullscreen && !tappedAControl) {
toggleSlideshowWithIndicator();
} else {
// Only detect horizontal swipe (left/right) for pausing slideshow
Expand Down
Loading