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
8 changes: 5 additions & 3 deletions docs/user-guide/semantic-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@ With "Show hover thumbnails" turned on and "Show landmarks" turned off:

## Showing images, videos, or both

If the album contains videos as well as photos, the **Show** radio buttons choose
which of them to show: **Both**, **Images only**, or **Videos only**.
If the album contains videos as well as photos, the **Show** icon in the Search
panel chooses which of them to show: **Images**, **Videos**, or **Both**. The icon
changes to a camera, a clapper, or the two together to indicate the current choice.
It is greyed out for an album that holds only one kind.
Videos are placed by the same semantic embedding as photos — computed from a
still captured shortly after the video starts — so a clip lands next to the
pictures it looks like.

The choice applies everywhere, not just to the map. With **Videos only**
The choice applies everywhere, not just to the map. With **Videos**
selected, the slideshow and the grid view step through the album's videos
alone, shuffle mode draws only from them, the seek slider covers just them,
and text and image searches return videos only (up to the album's maximum
Expand Down
78 changes: 72 additions & 6 deletions photomap/frontend/static/css/control-and-search-panels.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,48 @@
border-color: #fff;
}

/* ===== "Show" menu (images / videos / both) ===== */

#showMediaMenu {
position: fixed;
display: flex;
flex-direction: column;
gap: 2px;
background: rgba(30, 30, 30, 0.95);
border: 1px solid #444;
padding: 4px;
border-radius: 6px;
z-index: 10000;
}

.show-media-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px 4px 6px;
background: transparent;
border: none;
border-radius: 4px;
color: #fff;
font-size: 0.95em;
cursor: pointer;
}

.show-media-item svg {
width: 24px;
height: 24px;
display: block;
}

.show-media-item:hover {
background: rgba(255, 255, 255, 0.12);
}

.show-media-item[aria-current="true"] {
background: rgba(255, 255, 255, 0.22);
font-weight: bold;
}

/* Drag-over highlight for search panel */
#searchPanel.drag-over {
border: 2px dashed #00ff99;
Expand Down Expand Up @@ -221,6 +263,9 @@
#controlPanel {
position: static !important;
width: 100vw;
/* Without this the side padding is added to 100vw, pushing the centred
icon row off the right-hand edge. */
box-sizing: border-box;
margin: 0;
border-radius: 0;
box-shadow: none;
Expand Down Expand Up @@ -266,12 +311,23 @@
}
}

/* The Search panel carries six icons since Clear became always-visible and
About moved across. At the phone layout's 48px that needs 328px of row,
which a 320px viewport cannot give — and html/body are overflow:hidden, so
the excess is clipped off both ends rather than scrolled. Step the buttons
back down on the narrowest screens: 6x40 + 5x4 = 260px fits with room over. */
@media (max-width: 380px) {
/* The two panels share the bottom edge until they stack at 600px, and with
seven icons in the Search panel they would run into each other on a tablet
in portrait (768-820px). The room comes out of the gaps between icons. Below
about 745px they still overlap, by less than they would otherwise. */
@media (min-width: 601px) and (max-width: 860px) {
.control-icons-row,
.search-icons-row {
gap: 1px;
}
}

/* Labels make some icons wider than their buttons. At the phone layout's 48px
the Search panel's seven icons need 399px of row and the Control panel 426px,
plus padding, which most phones cannot give — and html/body are
overflow:hidden, so the excess is clipped off both ends rather than scrolled.
Step the buttons back down on the narrower screens. */
@media (max-width: 429px) {
#controlPanel button,
#searchPanel button {
width: 40px;
Expand All @@ -291,3 +347,13 @@
gap: 0.25em;
}
}

/* Under about 360px even the 40px rows need more than the width on offer. This
fits the Search row down to 320px; at 320px the Control row's outer labels
still lose about 6px a side. */
@media (max-width: 357px) {
.control-icons-row,
.search-icons-row {
gap: 0;
}
}
7 changes: 5 additions & 2 deletions photomap/frontend/static/css/search.css
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,15 @@
nothing to clear. updateSearchCheckmarks() owns the disabled attribute.
Both rules outrank the #searchPanel button / :hover opacities. */
#clearSearchBtn:disabled,
#clearSearchBtn:disabled:hover {
#clearSearchBtn:disabled:hover,
#showMediaBtn:disabled,
#showMediaBtn:disabled:hover {
opacity: 0.25;
cursor: default;
}

#clearSearchBtn:disabled + .button-label {
#clearSearchBtn:disabled + .button-label,
#showMediaBtn:disabled + .button-label {
opacity: 0.25;
}

Expand Down
6 changes: 2 additions & 4 deletions photomap/frontend/static/css/umap-floating-window.css
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,11 @@
outline: none;
}

#umapClickBehaviorContainer,
#umapMediaFilterContainer {
#umapClickBehaviorContainer {
font-size: 0.85em;
}

#umapClickBehaviorContainer label,
#umapMediaFilterContainer label {
#umapClickBehaviorContainer label {
cursor: pointer;
user-select: none;
}
Expand Down
31 changes: 27 additions & 4 deletions photomap/frontend/static/javascript/media-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
// 1. which album indices are videos, fetched from /media_indices on every
// album change (a deletion is renumbered locally instead — slideState
// does the same to the live list, and a refetch would force a rebuild of
// the very slides the deletion path is careful to keep in place);
// the very slides the deletion path is careful to keep in place). Each
// update fires `mediaIndicesUpdated`, whether or not the filter changed;
// 2. the derived views the rest of the app needs — a predicate over global
// indices, a filter for result lists, and the "browse list": every index
// the filter keeps, which search.js hands to the slide machinery as the
Expand All @@ -28,6 +29,8 @@ import { fetchJson } from "./utils.js";
let videoIndices = new Set();
let totalIndices = 0;
let loadedAlbum = null;
// The last /media_indices fetch failed, so the album's contents are unknown.
let loadFailed = false;
let loadSequence = 0;
// The album whose /media_indices fetch is in flight, or null. A filter change
// during the fetch must not announce from the previous album's set; the load
Expand All @@ -44,18 +47,32 @@ let announcedFilter = "both";
// nothing. Either way there is nothing to filter, and a browse list would be
// a slower spelling of "the whole album" — or a blank screen. So the stored
// preference only takes effect on an album that has both kinds, which is also
// what the map's radio buttons require before they enable themselves.
// when the Show menu enables itself.
export function effectiveMediaFilter() {
const filter = state.mediaFilter;
if (filter !== "images" && filter !== "videos") {
return "both";
}
if (videoIndices.size === 0 || videoIndices.size >= totalIndices) {
if (albumMediaMix() !== "mixed") {
return "both";
}
return filter;
}

// What the loaded album holds: "mixed" is the only case with anything to filter
// here. "unknown" means the fetch failed; browsing fails open then, but the map
// classifies its own points and may still be filtered. An album not loaded yet
// counts as "images".
export function albumMediaMix() {
if (loadFailed) {
return "unknown";
}
if (videoIndices.size === 0) {
return "images";
}
return videoIndices.size >= totalIndices ? "videos" : "mixed";
}

// Whether the active filter shows the album entry at `index`.
export function isIndexVisible(index) {
const filter = effectiveMediaFilter();
Expand Down Expand Up @@ -119,9 +136,11 @@ export async function loadMediaIndices(album, reason = "album") {
const sequence = ++loadSequence;
loadingAlbum = album;
let data = { total: 0, video_indices: [] };
let failed = false;
try {
data = await fetchJson(`media_indices/${encodeURIComponent(album)}`);
} catch (err) {
failed = true;
// No index yet, or the server is away: fail open. An album with no known
// videos is one the filter leaves alone.
console.warn("Failed to load media indices; media filter is inactive:", err);
Expand All @@ -130,10 +149,12 @@ export async function loadMediaIndices(album, reason = "album") {
return;
}
loadingAlbum = null;
loadFailed = failed;
videoIndices = new Set(data?.video_indices || []);
totalIndices = data?.total || 0;
loadedAlbum = album;
announce(reason);
window.dispatchEvent(new CustomEvent("mediaIndicesUpdated"));
}

// Renumber the video set after `deletedIndices` were removed from the album,
Expand Down Expand Up @@ -161,6 +182,7 @@ function applyDeletion(deletedIndices, totalImages) {
export function _setMediaIndicesForTest({ album = null, total = 0, videoIndices: videos = [] } = {}) {
loadedAlbum = album;
loadingAlbum = null;
loadFailed = false;
totalIndices = total;
videoIndices = new Set(videos);
announcedFilter = "both";
Expand All @@ -173,6 +195,7 @@ window.addEventListener("albumChanged", (e) => {
// The live list has already been renumbered in place by slideState;
// keep our set in step and stay quiet so nothing is rebuilt.
applyDeletion(detail.deletedIndices, detail.totalImages);
window.dispatchEvent(new CustomEvent("mediaIndicesUpdated"));
return;
}
if (detail.changeType !== "refresh") {
Expand All @@ -186,7 +209,7 @@ window.addEventListener("albumChanged", (e) => {
});

// Fired by the mediaFilter setting's onSet hook in state.js — which runs for
// the user's radio click, and also when a server-side preference record is
// the user's choice in the Show menu, and also when a server-side preference record is
// applied at boot, where no settingsUpdated event is dispatched.
window.addEventListener("mediaFilterSettingChanged", () => {
if (loadingAlbum !== null) {
Expand Down
Loading
Loading