diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-15 13:26:28 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-15 23:50:40 +0300 |
| commit | 6e47246dbbc97821b97aa9c45352abc9c2a9aa4a (patch) | |
| tree | aa893f4ef633f76faab5731313de2d694a1de94e | |
| parent | 3adeaa68af3bac73450a4156edb51fd1ec88347a (diff) | |
Stats/camera pages: blurred background, album-like camera thumbs, spacing
Three refinements to the stats site:
- Stats and per-camera pages now get a random blurred background image (same
seeded pick the album preview pages use) instead of a plain black page. Adds
_stats_random_background + STATS_BLURS_DIR; falls back to black when no photos
exist (e.g. isolated unit tests).
- Per-camera thumbnails now behave like the main album: they link to each
photo's album view page (navigation, details, EXIF tooltip) and carry the same
seeded animation class, instead of linking to the raw image. render_album_pages
records a photo -> "<page>-<preview>" map (ALBUM_VIEW_PAGE_BY_PHOTO) that the
camera pages read; photos with no recorded view page fall back to the image.
- Spacing: the camera leaderboard rows get padding + a separator line so the long
wrapping camera names are distinguishable, and the stats sections, headings,
bar rows, back-link, and camera grid get moderate extra spacing so nothing
looks cramped.
Tests updated for the new camera-thumbnail markup and extended to assert the
stats background and the album view-page links; camera.tmpl comment corrected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rwxr-xr-x | bin/shuriken | 78 | ||||
| -rw-r--r-- | share/templates/default/camera.tmpl | 9 | ||||
| -rw-r--r-- | share/templates/default/header.tmpl | 36 | ||||
| -rw-r--r-- | src/lib/album.source.sh | 15 | ||||
| -rw-r--r-- | src/lib/stats.source.sh | 63 | ||||
| -rwxr-xr-x | tests/cli.sh | 27 |
6 files changed, 194 insertions, 34 deletions
diff --git a/bin/shuriken b/bin/shuriken index 8fafb3b..b91a36d 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -1512,6 +1512,13 @@ maybe_shuffle() { } # Inlined from src/lib/album.source.sh +# Maps each album photo filename to its view-page basename ("<page>-<preview>") +# as assigned during render_album_pages. The stats camera pages read this so +# their thumbnails link into the album view (with navigation, details, and the +# EXIF tooltip) exactly like the main album, instead of the raw image. Declared +# globally so it always exists for callers even when no album was rendered. +declare -gA ALBUM_VIEW_PAGE_BY_PHOTO=() + album_photo_files() { local -r photos_dir="$1"; shift @@ -2255,6 +2262,10 @@ render_album_pages() { # shellcheck disable=SC2034 local -a rendered_view_pages=() + # Rebuild the photo -> view-page map for this album from scratch so a + # re-generate (or a smaller incoming set) does not keep stale entries. + ALBUM_VIEW_PAGE_BY_PHOTO=() + name=$(album_page_name "$num") start_preview_page \ @@ -2299,6 +2310,10 @@ render_album_pages() { render_failed record_rendered_view_page rendered_view_pages rendered_last_views \ "$num" "$i" + # Read later by the stats camera pages (render_camera_pages); shellcheck + # cannot see that cross-function use. + # shellcheck disable=SC2034 + ALBUM_VIEW_PAGE_BY_PHOTO["$photo"]="$num-$i" done < <(album_photo_files "$photos_dir") finish_preview_page "$name" "$html_dir" "$backhref" "$tarball_name" @@ -3391,15 +3406,22 @@ _stats_bar_row() { } # Open a <section> with an escaped heading and the <ul> bar container. Split from -# the row emitters so every section shares identical chrome. +# the row emitters so every section shares identical chrome. An optional second +# argument adds an extra CSS class to the <ul> (e.g. the leaderboard uses it to +# space out and separate its rows of long, wrapping camera names). _stats_section_open() { local -r heading="$1"; shift + local -r list_class="${1:-}" local heading_html + local ul_class='stats-bars' + if [ -n "$list_class" ]; then + ul_class+=" $list_class" + fi heading_html=$(_html_escape "$heading") printf '<section class="stats-section">\n' printf '<h2>%s</h2>\n' "$heading_html" - printf '<ul class="stats-bars">\n' + printf '<ul class="%s">\n' "$ul_class" } _stats_section_close() { @@ -3422,7 +3444,7 @@ _stats_render_camera_section() { return fi max=$(_stats_max_count STATS_CAMERAS) - _stats_section_open 'Camera leaderboard' + _stats_section_open 'Camera leaderboard' 'stats-leaderboard' while IFS= read -r label; do label_html=$(_html_escape "$label") slug="${STATS_CAMERA_SLUGS[$label]}" @@ -3601,18 +3623,31 @@ _stats_build_body() { # html_dir is the dist-relative output directory (top-level album: "."), # backhref is the relative path back to the album root ("." for a top-level # stats.html), and page_name defaults to "stats" -> stats.html. +# Pick a seeded-random photo for a stats/camera page's blurred background, the +# same way the album preview pages do. The context seeds the choice so each page +# gets a stable (per RANDOM_SEED) but varied background. Degrades to an empty +# string (plain black background) when no photos exist, e.g. unit tests that +# render the page without a populated photos directory. +_stats_random_background() { + local -r context="$1"; shift + + randomphoto "$STATS_CAMERA_PHOTOS_DIR" "$context" 2>/dev/null || true +} + render_stats_page() { local -r html_dir="$1"; shift local -r backhref="$1"; shift local -r page_name="${1:-stats}" local stats_body + local background_image stats_body=$(_stats_build_body) + background_image=$(_stats_random_background "$page_name.html") template header "$page_name.html" \ html_dir "$html_dir" \ backhref "$backhref" \ - blurs_dir '' \ - background_image '' \ + blurs_dir "$STATS_BLURS_DIR" \ + background_image "$background_image" \ show_header_bar 'yes' template stats "$page_name.html" \ html_dir "$html_dir" \ @@ -3651,6 +3686,9 @@ render_stats_page() { # pages link to the same assets the album pages do. declare -gr STATS_CAMERA_PHOTOS_DIR='photos' declare -gr STATS_CAMERA_THUMBS_DIR='thumbs' +# Dist-relative directory of blurred images used for the page background, the +# same one render_album_pages passes to the album preview pages. +declare -gr STATS_BLURS_DIR='blurs' # Emit one thumbnail anchor for the camera grid: a thumb image (from thumbs/) # wrapped in a link to the full-size image (from photos/), both resolved through @@ -3661,12 +3699,28 @@ _stats_camera_thumbnail() { local -r backhref_html="$1"; shift local -r photo="$1"; shift local photo_html + local animation_class + local view_page photo_html=$(_html_escape "$photo") - printf ' <a href="%s/%s/%s">' \ - "$backhref_html" "$STATS_CAMERA_PHOTOS_DIR" "$photo_html" - printf '<img class="thumb" src="%s/%s/%s" /></a>\n' \ - "$backhref_html" "$STATS_CAMERA_THUMBS_DIR" "$photo_html" + # Same seeded animation the album thumbnail uses for this photo, so camera + # pages animate identically. + animation_class=$(random_animation_css_class slow "$photo") + # Link to the photo's album view page (navigation, details, EXIF tooltip) + # so a camera page behaves exactly like the main album. Fall back to the + # full-size image when the album mapping is unknown (e.g. unit tests that + # render camera pages without a rendered album). + view_page="${ALBUM_VIEW_PAGE_BY_PHOTO[$photo]:-}" + if [ -n "$view_page" ]; then + printf ' <a name="%s" href="%s/%s.html">' \ + "$photo_html" "$backhref_html" "$view_page" + else + printf ' <a href="%s/%s/%s">' \ + "$backhref_html" "$STATS_CAMERA_PHOTOS_DIR" "$photo_html" + fi + printf '<img class="thumb %s" src="%s/%s/%s" /></a>\n' \ + "$animation_class" "$backhref_html" "$STATS_CAMERA_THUMBS_DIR" \ + "$photo_html" } # Build the full thumbnail grid for one camera from its newline-separated photo @@ -3696,15 +3750,17 @@ _stats_render_camera_page() { local -r photos="$1"; shift local backhref_html local camera_thumbs + local background_image local -r page="camera-$slug.html" backhref_html=$(_html_escape "$backhref") camera_thumbs=$(_stats_build_camera_thumbs "$backhref_html" "$photos") + background_image=$(_stats_random_background "$page") template header "$page" \ html_dir "$html_dir" \ backhref "$backhref" \ - blurs_dir '' \ - background_image '' \ + blurs_dir "$STATS_BLURS_DIR" \ + background_image "$background_image" \ show_header_bar 'yes' template camera "$page" \ html_dir "$html_dir" \ diff --git a/share/templates/default/camera.tmpl b/share/templates/default/camera.tmpl index 39ca273..2e08461 100644 --- a/share/templates/default/camera.tmpl +++ b/share/templates/default/camera.tmpl @@ -7,10 +7,11 @@ # <html>/<head>/<body> and footer come from the header/footer templates, which # render_camera_pages emits around this one (same pattern as stats/view/details). # -# Thumbnails link to the full-size image under photos/ rather than to a per-photo -# view page: a camera page is built outside the album's pagination loop and so -# cannot know which <page>-<preview>.html a given photo landed on. Linking to the -# original image is the sensible derivable target (see render_camera_pages). +# Thumbnails link to each photo's album view page (navigation, details, EXIF +# tooltip) so a camera page behaves exactly like the main album. +# render_camera_pages resolves the <page>-<preview>.html target from the map the +# album build records; a photo with no recorded view page falls back to the +# full-size image under photos/ (see render_camera_pages). cat <<END <div class="view stats camera"> <h1 class="stats-title">${render_title_html} — ${render_camera_name_html}</h1> diff --git a/share/templates/default/header.tmpl b/share/templates/default/header.tmpl index 6a163f9..411d106 100644 --- a/share/templates/default/header.tmpl +++ b/share/templates/default/header.tmpl @@ -98,23 +98,31 @@ cat <<END div.stats { margin: 0 auto; max-width: 900px; + padding: 0 12px; text-align: left; } h1.stats-title { + margin-bottom: 12px; text-align: center; } + /* Give the "Back to album/stats" link room before the first section. */ + div.stats-back { + margin-bottom: 16px; + } + section.stats-section { background-color: #000000; border: 3px solid #ffffff; - margin: 8px auto; - padding: 10px; + margin: 16px auto; + padding: 14px 16px; } section.stats-section h2 { font-size: 1.1em; margin-top: 0; + margin-bottom: 12px; } /* Each bar row is a label, a proportional bar, and the count/percent. @@ -129,8 +137,20 @@ cat <<END ul.stats-bars li { align-items: center; display: flex; - gap: 8px; - margin: 2px 0; + gap: 10px; + margin: 4px 0; + } + + /* The camera leaderboard labels are long, EXIF-derived names that wrap, + so give each row extra vertical padding and a separator line to make + the boundary between cameras clear. */ + ul.stats-leaderboard li { + border-bottom: 1px solid #444444; + padding: 8px 0; + } + + ul.stats-leaderboard li:last-child { + border-bottom: none; } span.stats-label { @@ -160,12 +180,18 @@ cat <<END padding: 5px; } - img.thumb { + img.thumb { height: ${render_thumbheight_html}px; width: ${render_thumbheight_html}px; object-fit: cover; } + /* Camera filter page grid: separate from the back link and let the + thumbnails (which already carry their own padding) breathe a little. */ + div.camera-thumbs { + margin-top: 12px; + } + img.thumb:hover { outline: 4px solid #ffffff; -webkit-transform: scaleX(-1); diff --git a/src/lib/album.source.sh b/src/lib/album.source.sh index 4914642..65282f1 100644 --- a/src/lib/album.source.sh +++ b/src/lib/album.source.sh @@ -1,3 +1,10 @@ +# Maps each album photo filename to its view-page basename ("<page>-<preview>") +# as assigned during render_album_pages. The stats camera pages read this so +# their thumbnails link into the album view (with navigation, details, and the +# EXIF tooltip) exactly like the main album, instead of the raw image. Declared +# globally so it always exists for callers even when no album was rendered. +declare -gA ALBUM_VIEW_PAGE_BY_PHOTO=() + album_photo_files() { local -r photos_dir="$1"; shift @@ -741,6 +748,10 @@ render_album_pages() { # shellcheck disable=SC2034 local -a rendered_view_pages=() + # Rebuild the photo -> view-page map for this album from scratch so a + # re-generate (or a smaller incoming set) does not keep stale entries. + ALBUM_VIEW_PAGE_BY_PHOTO=() + name=$(album_page_name "$num") start_preview_page \ @@ -785,6 +796,10 @@ render_album_pages() { render_failed record_rendered_view_page rendered_view_pages rendered_last_views \ "$num" "$i" + # Read later by the stats camera pages (render_camera_pages); shellcheck + # cannot see that cross-function use. + # shellcheck disable=SC2034 + ALBUM_VIEW_PAGE_BY_PHOTO["$photo"]="$num-$i" done < <(album_photo_files "$photos_dir") finish_preview_page "$name" "$html_dir" "$backhref" "$tarball_name" diff --git a/src/lib/stats.source.sh b/src/lib/stats.source.sh index b0a1646..527ea1f 100644 --- a/src/lib/stats.source.sh +++ b/src/lib/stats.source.sh @@ -614,15 +614,22 @@ _stats_bar_row() { } # Open a <section> with an escaped heading and the <ul> bar container. Split from -# the row emitters so every section shares identical chrome. +# the row emitters so every section shares identical chrome. An optional second +# argument adds an extra CSS class to the <ul> (e.g. the leaderboard uses it to +# space out and separate its rows of long, wrapping camera names). _stats_section_open() { local -r heading="$1"; shift + local -r list_class="${1:-}" local heading_html + local ul_class='stats-bars' + if [ -n "$list_class" ]; then + ul_class+=" $list_class" + fi heading_html=$(_html_escape "$heading") printf '<section class="stats-section">\n' printf '<h2>%s</h2>\n' "$heading_html" - printf '<ul class="stats-bars">\n' + printf '<ul class="%s">\n' "$ul_class" } _stats_section_close() { @@ -645,7 +652,7 @@ _stats_render_camera_section() { return fi max=$(_stats_max_count STATS_CAMERAS) - _stats_section_open 'Camera leaderboard' + _stats_section_open 'Camera leaderboard' 'stats-leaderboard' while IFS= read -r label; do label_html=$(_html_escape "$label") slug="${STATS_CAMERA_SLUGS[$label]}" @@ -824,18 +831,31 @@ _stats_build_body() { # html_dir is the dist-relative output directory (top-level album: "."), # backhref is the relative path back to the album root ("." for a top-level # stats.html), and page_name defaults to "stats" -> stats.html. +# Pick a seeded-random photo for a stats/camera page's blurred background, the +# same way the album preview pages do. The context seeds the choice so each page +# gets a stable (per RANDOM_SEED) but varied background. Degrades to an empty +# string (plain black background) when no photos exist, e.g. unit tests that +# render the page without a populated photos directory. +_stats_random_background() { + local -r context="$1"; shift + + randomphoto "$STATS_CAMERA_PHOTOS_DIR" "$context" 2>/dev/null || true +} + render_stats_page() { local -r html_dir="$1"; shift local -r backhref="$1"; shift local -r page_name="${1:-stats}" local stats_body + local background_image stats_body=$(_stats_build_body) + background_image=$(_stats_random_background "$page_name.html") template header "$page_name.html" \ html_dir "$html_dir" \ backhref "$backhref" \ - blurs_dir '' \ - background_image '' \ + blurs_dir "$STATS_BLURS_DIR" \ + background_image "$background_image" \ show_header_bar 'yes' template stats "$page_name.html" \ html_dir "$html_dir" \ @@ -874,6 +894,9 @@ render_stats_page() { # pages link to the same assets the album pages do. declare -gr STATS_CAMERA_PHOTOS_DIR='photos' declare -gr STATS_CAMERA_THUMBS_DIR='thumbs' +# Dist-relative directory of blurred images used for the page background, the +# same one render_album_pages passes to the album preview pages. +declare -gr STATS_BLURS_DIR='blurs' # Emit one thumbnail anchor for the camera grid: a thumb image (from thumbs/) # wrapped in a link to the full-size image (from photos/), both resolved through @@ -884,12 +907,28 @@ _stats_camera_thumbnail() { local -r backhref_html="$1"; shift local -r photo="$1"; shift local photo_html + local animation_class + local view_page photo_html=$(_html_escape "$photo") - printf ' <a href="%s/%s/%s">' \ - "$backhref_html" "$STATS_CAMERA_PHOTOS_DIR" "$photo_html" - printf '<img class="thumb" src="%s/%s/%s" /></a>\n' \ - "$backhref_html" "$STATS_CAMERA_THUMBS_DIR" "$photo_html" + # Same seeded animation the album thumbnail uses for this photo, so camera + # pages animate identically. + animation_class=$(random_animation_css_class slow "$photo") + # Link to the photo's album view page (navigation, details, EXIF tooltip) + # so a camera page behaves exactly like the main album. Fall back to the + # full-size image when the album mapping is unknown (e.g. unit tests that + # render camera pages without a rendered album). + view_page="${ALBUM_VIEW_PAGE_BY_PHOTO[$photo]:-}" + if [ -n "$view_page" ]; then + printf ' <a name="%s" href="%s/%s.html">' \ + "$photo_html" "$backhref_html" "$view_page" + else + printf ' <a href="%s/%s/%s">' \ + "$backhref_html" "$STATS_CAMERA_PHOTOS_DIR" "$photo_html" + fi + printf '<img class="thumb %s" src="%s/%s/%s" /></a>\n' \ + "$animation_class" "$backhref_html" "$STATS_CAMERA_THUMBS_DIR" \ + "$photo_html" } # Build the full thumbnail grid for one camera from its newline-separated photo @@ -919,15 +958,17 @@ _stats_render_camera_page() { local -r photos="$1"; shift local backhref_html local camera_thumbs + local background_image local -r page="camera-$slug.html" backhref_html=$(_html_escape "$backhref") camera_thumbs=$(_stats_build_camera_thumbs "$backhref_html" "$photos") + background_image=$(_stats_random_background "$page") template header "$page" \ html_dir "$html_dir" \ backhref "$backhref" \ - blurs_dir '' \ - background_image '' \ + blurs_dir "$STATS_BLURS_DIR" \ + background_image "$background_image" \ show_header_bar 'yes' template camera "$page" \ html_dir "$html_dir" \ diff --git a/tests/cli.sh b/tests/cli.sh index f01c2e0..14439bf 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -3060,6 +3060,7 @@ test::stats_identify_output() { } test_generate_stats_pages_created_and_nav_linked() { + local camera_html local config_file local fake_bin local -i nav_links @@ -3088,6 +3089,21 @@ test_generate_stats_pages_created_and_nav_linked() { test::assert_contains 'Canon EOS R5' "$(<"$TEST_TMPDIR/dist/stats.html")" test::assert_not_contains '<script' "$(<"$TEST_TMPDIR/dist/stats.html")" + # The stats page gets a random blurred background like the album pages. + test::assert_contains 'background-image: url("./blurs/' \ + "$(<"$TEST_TMPDIR/dist/stats.html")" + + # Camera-page thumbnails link into the album view pages (behave like the main + # album), not the raw image, and point at the thumbs. + camera_html=$(<"$TEST_TMPDIR/dist/camera-canon-eos-r5.html") + test::assert_contains 'src="./thumbs/' "$camera_html" + test::assert_not_contains 'href="./photos/' "$camera_html" + if ! grep -Eq 'href="\./[0-9]+-[0-9]+\.html"' <<< "$camera_html"; then + printf 'FAIL: camera page does not link thumbnails to album view pages\n' \ + >&2 + exit 1 + fi + # The header bar links to the stats page on at least one generated page. nav_links=$(grep -lF 'stats.html">Stats' "$TEST_TMPDIR"/dist/*.html | wc -l) test "$nav_links" -gt 0 @@ -4001,6 +4017,8 @@ MAXPREVIEWS=40 ORIGINAL_BASEPATH='' TARBALL_INCLUDE=no SHURIKEN_OUTPUT_MODE=quiet +# Seed so the per-thumbnail animation class is reproducible across runs. +RANDOM_SEED=camera-test apply_config_defaults feed() { @@ -4040,11 +4058,14 @@ BASH test::assert_file_exists "$dist_dir/run1/camera-canon-eos-5d-2.html" test::assert_file_exists "$dist_dir/run1/camera-nikon-co-z6.html" - # The Canon EOS 5D page lists exactly its two photos as thumbnails linking to - # the full images under photos/, and not the other camera's photo. + # The Canon EOS 5D page lists exactly its two photos as thumbnails, and not + # the other camera's photo. With no album rendered here, the thumbnail link + # falls back to the full image under photos/; the img carries a thumb class + # (plus a seeded animation class) pointing at thumbs/. html=$(cat "$dist_dir/run1/camera-canon-eos-5d.html") test::assert_contains '<a href="../photos/a.jpg">' "$html" - test::assert_contains '<img class="thumb" src="../thumbs/a.jpg" />' "$html" + test::assert_contains 'class="thumb ' "$html" + test::assert_contains 'src="../thumbs/a.jpg" />' "$html" test::assert_contains '<a href="../photos/b.jpg">' "$html" test::assert_not_contains 'photos/c.png' "$html" test::assert_not_contains 'photos/d.jpg' "$html" |
