diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-15 13:55:32 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-15 23:50:40 +0300 |
| commit | be6ae2c9541a01927616b46cf6a683d7ce4519ff (patch) | |
| tree | 53161337bae46495b8e88b96f1b28b1bd6f9c2ca | |
| parent | 6e47246dbbc97821b97aa9c45352abc9c2a9aa4a (diff) | |
Per-camera mini-albums: full-width grid + camera-scoped view navigation
Two fixes to the per-camera pages:
- The camera thumbnail grid is now full-width and reflows with the window like
the main album. Previously it sat inside the 900px-wide stats column, so wide
thumbnails only fit ~2 per row. camera.tmpl now puts the grid in a full-width
sibling block (centered by the body, like the album's direct thumbnail
children); the heading/back-link stay in the narrow stats column.
- Each camera is now a self-contained mini album. Thumbnails link to per-camera
view pages (camera-<slug>--<index>.html) whose prev/next cycle only through
that camera's photos, instead of linking into the shared album view pages
(where next/prev walked the whole album and left the camera). The view pages
reuse the album's look (blurred background, EXIF tooltip, animation) and offer
Gallery / Details (to the album details page) / Direct-link nav. The "--"
index separator can never collide with a gallery name since slugs never
contain "--".
Adds cameraview.tmpl + its render field, _stats_render_camera_{gallery,views}
and the cameraview body builders. Tests updated for the new thumbnail links and
extended to assert the per-camera view pages and their camera-scoped navigation;
required-context-vars test now also covers the camera and cameraview templates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rwxr-xr-x | bin/shuriken | 205 | ||||
| -rw-r--r-- | share/templates/default/camera.tmpl | 15 | ||||
| -rw-r--r-- | share/templates/default/cameraview.tmpl | 10 | ||||
| -rw-r--r-- | src/lib/stats.source.sh | 204 | ||||
| -rw-r--r-- | src/lib/template.source.sh | 1 | ||||
| -rwxr-xr-x | tests/cli.sh | 63 |
6 files changed, 384 insertions, 114 deletions
diff --git a/bin/shuriken b/bin/shuriken index b91a36d..ce95020 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -615,6 +615,7 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_background_image_css|context_css|background_image|background_image|header splash' 'render_blurs_dir_css|context_css|blurs_dir|blurs_dir|header splash' 'render_camera_name_html|context_html|camera_name|camera_name|camera' + 'render_cameraview_body_html|context_raw|cameraview_body|cameraview_body|cameraview' 'render_camera_thumbs_html|context_raw|camera_thumbs|camera_thumbs|camera' 'render_current_date_text|current_date_html|||' 'render_enter_page_html|context_html|enter_page|enter_page|splash' @@ -3671,15 +3672,14 @@ render_stats_page() { # field; camera.tmpl supplies only the page chrome (heading + back-to-stats # link), wrapped by the shared header/footer the same way render_stats_page does. # -# The album's incoming photo and thumbnail directories are the conventional -# 'photos'/'thumbs' that generate() passes to render_album_pages -# (render_album_pages 'photos' '.' 'thumbs' ...), so the grid reuses preview.tmpl's -# thumbnail markup style with those fixed dir names. Each thumbnail links to the -# full-size image under photos/ (backhref/photos/<file>) rather than a per-photo -# <page>-<preview>.html view page: a camera page is rendered outside the album -# pagination loop and cannot know which view page a given photo landed on, so the -# original image under photos/ is the only target derivable in this context (this -# choice is also documented in camera.tmpl). +# Each camera is a self-contained mini album: the gallery (camera-<slug>.html) +# shows a thumbnail grid, and every thumbnail links to a per-camera view page +# (camera-<slug>--<index>.html) whose prev/next navigation cycles only through +# that camera's photos. The "--<index>" suffix never collides with a gallery +# name because slugs never contain "--". The album's photo/thumb dirs are the +# conventional 'photos'/'thumbs' generate() passes to render_album_pages, and the +# view pages link "Details" to the album's own details page for the photo via the +# ALBUM_VIEW_PAGE_BY_PHOTO map. # Dist-relative subdirectories that hold the full-size images and thumbnails. # These mirror the literals generate() passes to render_album_pages so camera @@ -3691,33 +3691,23 @@ declare -gr STATS_CAMERA_THUMBS_DIR='thumbs' 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 -# the page's backhref. The photo filename is HTML-escaped because it ends up in -# both href and src attributes. Mirrors preview.tmpl's markup but targets the -# full image instead of a view page (see render_camera_pages header comment). +# wrapped in a link to this camera's view page for that photo +# (camera-<slug>--<index>.html), both resolved through the page's backhref. The +# photo filename is HTML-escaped because it ends up in the src attribute; the +# slug and index are filename-safe by construction. _stats_camera_thumbnail() { local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -ri index="$1"; shift local -r photo="$1"; shift local photo_html local animation_class - local view_page photo_html=$(_html_escape "$photo") - # Same seeded animation the album thumbnail uses for this photo, so camera - # pages animate identically. + # Same seeded animation the album thumbnail uses for this photo. 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 ' <a name="%s" href="%s/camera-%s--%d.html">' \ + "$photo_html" "$backhref_html" "$slug" "$index" printf '<img class="thumb %s" src="%s/%s/%s" /></a>\n' \ "$animation_class" "$backhref_html" "$STATS_CAMERA_THUMBS_DIR" \ "$photo_html" @@ -3728,20 +3718,20 @@ _stats_camera_thumbnail() { # grid is deterministic for a given input. Returns the grid HTML on stdout. _stats_build_camera_thumbs() { local -r backhref_html="$1"; shift + local -r slug="$1"; shift local -r photos="$1"; shift local photo + local -i index=0 while IFS= read -r photo; do if [ -n "$photo" ]; then - _stats_camera_thumbnail "$backhref_html" "$photo" + (( ++index )) + _stats_camera_thumbnail "$backhref_html" "$slug" "$index" "$photo" fi done <<< "$photos" } -# Render a single camera-<slug>.html: header + camera.tmpl (escaped heading and -# pre-built thumbnail grid) + footer. The label is HTML-escaped via camera.tmpl's -# context_html field spec; backhref_html is the already-escaped path used inside -# the grid anchors so thumbnail links resolve from the page's location. +# Render one camera's mini album: the gallery page plus a view page per photo. _stats_render_camera_page() { local -r html_dir="$1"; shift local -r backhref="$1"; shift @@ -3749,28 +3739,151 @@ _stats_render_camera_page() { local -r slug="$1"; shift local -r photos="$1"; shift local backhref_html + + backhref_html=$(_html_escape "$backhref") + _stats_render_camera_gallery \ + "$html_dir" "$backhref" "$backhref_html" "$label" "$slug" "$photos" + _stats_render_camera_views \ + "$html_dir" "$backhref" "$backhref_html" "$slug" "$photos" +} + +# Render camera-<slug>.html: header + camera.tmpl (escaped heading and pre-built +# thumbnail grid) + footer. The label is HTML-escaped via camera.tmpl's +# context_html field spec; backhref_html is the already-escaped path used inside +# the grid anchors so thumbnail links resolve from the page's location. +_stats_render_camera_gallery() { + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r backhref_html="$1"; shift + local -r label="$1"; shift + local -r slug="$1"; shift + local -r photos="$1"; shift 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") + camera_thumbs=$(_stats_build_camera_thumbs "$backhref_html" "$slug" "$photos") background_image=$(_stats_random_background "$page") template header "$page" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - blurs_dir "$STATS_BLURS_DIR" \ - background_image "$background_image" \ + html_dir "$html_dir" backhref "$backhref" \ + blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \ show_header_bar 'yes' template camera "$page" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - camera_name "$label" \ - camera_thumbs "$camera_thumbs" + html_dir "$html_dir" backhref "$backhref" \ + camera_name "$label" camera_thumbs "$camera_thumbs" template footer "$page" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - tarball_name '' + html_dir "$html_dir" backhref "$backhref" tarball_name '' +} + +# Render one camera-<slug>--<index>.html view page per photo, with prev/next +# wrapping around the camera's own photo list so navigation stays in the camera. +_stats_render_camera_views() { + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -r photos="$1"; shift + local photo + local -a photo_list=() + local -i i + + while IFS= read -r photo; do + [ -n "$photo" ] && photo_list+=("$photo") + done <<< "$photos" + + local -ri n=${#photo_list[@]} + for (( i = 1; i <= n; i++ )); do + _stats_render_camera_view_page \ + "$html_dir" "$backhref" "$backhref_html" "$slug" \ + "${photo_list[i - 1]}" "$i" \ + "$(( i == 1 ? n : i - 1 ))" "$(( i == n ? 1 : i + 1 ))" + done +} + +# Render a single per-camera view page (header + cameraview body + footer). +_stats_render_camera_view_page() { + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -r photo="$1"; shift + local -ri index="$1"; shift + local -ri prev="$1"; shift + local -ri next="$1"; shift + local body + local background_image + local -r page="camera-$slug--$index.html" + + body=$(_stats_build_cameraview_body \ + "$backhref_html" "$slug" "$photo" "$prev" "$next") + background_image=$(_stats_random_background "$page") + template header "$page" \ + html_dir "$html_dir" backhref "$backhref" \ + blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \ + show_header_bar 'no' + template cameraview "$page" \ + html_dir "$html_dir" backhref "$backhref" cameraview_body "$body" + template footer "$page" \ + html_dir "$html_dir" backhref "$backhref" tarball_name '' +} + +# Build the body of a per-camera view page: the photo (linked to this camera's +# next photo) plus a navigator whose prev/next cycle within the camera, a link +# back to the gallery, an optional Details link to the album's details page for +# the photo, and a direct image link. +_stats_build_cameraview_body() { + local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -r photo="$1"; shift + local -ri prev="$1"; shift + local -ri next="$1"; shift + local photo_html + local animation_class + local tooltip + local tooltip_attr='' + local view_page + local details_link='' + + photo_html=$(_html_escape "$photo") + animation_class=$(random_animation_css_class fast "$photo") + tooltip=$(photo_exif_tooltip_text "$photo" "$INCOMING_DIR/$photo") + if [ -n "$tooltip" ]; then + tooltip_attr=" title=\"$(_html_escape "$tooltip")\"" + fi + view_page="${ALBUM_VIEW_PAGE_BY_PHOTO[$photo]:-}" + if [ -n "$view_page" ]; then + details_link=$(printf ' <a href="%s/%s-details.html">Details</a> |' \ + "$backhref_html" "$view_page") + fi + _stats_print_cameraview_body "$backhref_html" "$slug" "$photo_html" \ + "$animation_class" "$tooltip_attr" "$details_link" "$prev" "$next" +} + +# Emit the per-camera view page markup. Split out so _stats_build_cameraview_body +# stays focused on assembling the pieces. +_stats_print_cameraview_body() { + local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -r photo_html="$1"; shift + local -r animation_class="$1"; shift + local -r tooltip_attr="$1"; shift + local -r details_link="$1"; shift + local -ri prev="$1"; shift + local -ri next="$1"; shift + + cat <<END +<div class='view'> + <a href="$backhref_html/camera-$slug--$next.html"> + <img class='view $animation_class' border='0' src='$backhref_html/$STATS_CAMERA_PHOTOS_DIR/$photo_html'$tooltip_attr /> + </a> + <div class="navigator"> + <a href="$backhref_html/camera-$slug--$prev.html" class="arrow">⇐</a> + <a href="$backhref_html/camera-$slug.html">Gallery</a> |$details_link + <a href="$backhref_html/$STATS_CAMERA_PHOTOS_DIR/$photo_html">Direct link</a> + <a href="$backhref_html/camera-$slug--$next.html" class="arrow">⇒</a> + </div> +</div> +END } # Public render entry point (handoff for task rm0). Renders one diff --git a/share/templates/default/camera.tmpl b/share/templates/default/camera.tmpl index 2e08461..982e32f 100644 --- a/share/templates/default/camera.tmpl +++ b/share/templates/default/camera.tmpl @@ -7,19 +7,20 @@ # <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 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). +# This is a self-contained mini album: each thumbnail links to a per-camera view +# page (camera-<slug>--<index>.html) whose prev/next navigation cycles only +# through this camera's photos (see render_camera_pages). The heading and back +# link sit in the narrow centered stats column, but the thumbnail grid is a +# full-width sibling so it flows and wraps with the window like the main album +# (the album thumbnails are likewise direct, full-width body children). cat <<END <div class="view stats camera"> <h1 class="stats-title">${render_title_html} — ${render_camera_name_html}</h1> <div class="stats-back navigator"> <a href="${render_backhref_html}/stats.html">Back to stats</a> </div> - <div class="camera-thumbs"> +</div> +<div class="camera-thumbs"> ${render_camera_thumbs_html} - </div> </div> END diff --git a/share/templates/default/cameraview.tmpl b/share/templates/default/cameraview.tmpl new file mode 100644 index 0000000..47b0e9b --- /dev/null +++ b/share/templates/default/cameraview.tmpl @@ -0,0 +1,10 @@ +# Per-camera view page body: a self-contained mini-album view. The whole body -- +# the photo and its camera-scoped navigator (prev/next cycle within the camera, +# plus links back to the gallery, the album details page, and the direct image) +# -- is pre-rendered by render_camera_pages in src/lib/stats.source.sh and handed +# in raw through render_cameraview_body_html. The surrounding chrome (header with +# the blurred background, and footer) comes from the header/footer templates, +# rendered around this one exactly like the album view pages. +cat <<END +${render_cameraview_body_html} +END diff --git a/src/lib/stats.source.sh b/src/lib/stats.source.sh index 527ea1f..2488ea7 100644 --- a/src/lib/stats.source.sh +++ b/src/lib/stats.source.sh @@ -879,15 +879,14 @@ render_stats_page() { # field; camera.tmpl supplies only the page chrome (heading + back-to-stats # link), wrapped by the shared header/footer the same way render_stats_page does. # -# The album's incoming photo and thumbnail directories are the conventional -# 'photos'/'thumbs' that generate() passes to render_album_pages -# (render_album_pages 'photos' '.' 'thumbs' ...), so the grid reuses preview.tmpl's -# thumbnail markup style with those fixed dir names. Each thumbnail links to the -# full-size image under photos/ (backhref/photos/<file>) rather than a per-photo -# <page>-<preview>.html view page: a camera page is rendered outside the album -# pagination loop and cannot know which view page a given photo landed on, so the -# original image under photos/ is the only target derivable in this context (this -# choice is also documented in camera.tmpl). +# Each camera is a self-contained mini album: the gallery (camera-<slug>.html) +# shows a thumbnail grid, and every thumbnail links to a per-camera view page +# (camera-<slug>--<index>.html) whose prev/next navigation cycles only through +# that camera's photos. The "--<index>" suffix never collides with a gallery +# name because slugs never contain "--". The album's photo/thumb dirs are the +# conventional 'photos'/'thumbs' generate() passes to render_album_pages, and the +# view pages link "Details" to the album's own details page for the photo via the +# ALBUM_VIEW_PAGE_BY_PHOTO map. # Dist-relative subdirectories that hold the full-size images and thumbnails. # These mirror the literals generate() passes to render_album_pages so camera @@ -899,33 +898,23 @@ declare -gr STATS_CAMERA_THUMBS_DIR='thumbs' 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 -# the page's backhref. The photo filename is HTML-escaped because it ends up in -# both href and src attributes. Mirrors preview.tmpl's markup but targets the -# full image instead of a view page (see render_camera_pages header comment). +# wrapped in a link to this camera's view page for that photo +# (camera-<slug>--<index>.html), both resolved through the page's backhref. The +# photo filename is HTML-escaped because it ends up in the src attribute; the +# slug and index are filename-safe by construction. _stats_camera_thumbnail() { local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -ri index="$1"; shift local -r photo="$1"; shift local photo_html local animation_class - local view_page photo_html=$(_html_escape "$photo") - # Same seeded animation the album thumbnail uses for this photo, so camera - # pages animate identically. + # Same seeded animation the album thumbnail uses for this photo. 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 ' <a name="%s" href="%s/camera-%s--%d.html">' \ + "$photo_html" "$backhref_html" "$slug" "$index" printf '<img class="thumb %s" src="%s/%s/%s" /></a>\n' \ "$animation_class" "$backhref_html" "$STATS_CAMERA_THUMBS_DIR" \ "$photo_html" @@ -936,20 +925,20 @@ _stats_camera_thumbnail() { # grid is deterministic for a given input. Returns the grid HTML on stdout. _stats_build_camera_thumbs() { local -r backhref_html="$1"; shift + local -r slug="$1"; shift local -r photos="$1"; shift local photo + local -i index=0 while IFS= read -r photo; do if [ -n "$photo" ]; then - _stats_camera_thumbnail "$backhref_html" "$photo" + (( ++index )) + _stats_camera_thumbnail "$backhref_html" "$slug" "$index" "$photo" fi done <<< "$photos" } -# Render a single camera-<slug>.html: header + camera.tmpl (escaped heading and -# pre-built thumbnail grid) + footer. The label is HTML-escaped via camera.tmpl's -# context_html field spec; backhref_html is the already-escaped path used inside -# the grid anchors so thumbnail links resolve from the page's location. +# Render one camera's mini album: the gallery page plus a view page per photo. _stats_render_camera_page() { local -r html_dir="$1"; shift local -r backhref="$1"; shift @@ -957,28 +946,151 @@ _stats_render_camera_page() { local -r slug="$1"; shift local -r photos="$1"; shift local backhref_html + + backhref_html=$(_html_escape "$backhref") + _stats_render_camera_gallery \ + "$html_dir" "$backhref" "$backhref_html" "$label" "$slug" "$photos" + _stats_render_camera_views \ + "$html_dir" "$backhref" "$backhref_html" "$slug" "$photos" +} + +# Render camera-<slug>.html: header + camera.tmpl (escaped heading and pre-built +# thumbnail grid) + footer. The label is HTML-escaped via camera.tmpl's +# context_html field spec; backhref_html is the already-escaped path used inside +# the grid anchors so thumbnail links resolve from the page's location. +_stats_render_camera_gallery() { + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r backhref_html="$1"; shift + local -r label="$1"; shift + local -r slug="$1"; shift + local -r photos="$1"; shift 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") + camera_thumbs=$(_stats_build_camera_thumbs "$backhref_html" "$slug" "$photos") background_image=$(_stats_random_background "$page") template header "$page" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - blurs_dir "$STATS_BLURS_DIR" \ - background_image "$background_image" \ + html_dir "$html_dir" backhref "$backhref" \ + blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \ show_header_bar 'yes' template camera "$page" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - camera_name "$label" \ - camera_thumbs "$camera_thumbs" + html_dir "$html_dir" backhref "$backhref" \ + camera_name "$label" camera_thumbs "$camera_thumbs" template footer "$page" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - tarball_name '' + html_dir "$html_dir" backhref "$backhref" tarball_name '' +} + +# Render one camera-<slug>--<index>.html view page per photo, with prev/next +# wrapping around the camera's own photo list so navigation stays in the camera. +_stats_render_camera_views() { + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -r photos="$1"; shift + local photo + local -a photo_list=() + local -i i + + while IFS= read -r photo; do + [ -n "$photo" ] && photo_list+=("$photo") + done <<< "$photos" + + local -ri n=${#photo_list[@]} + for (( i = 1; i <= n; i++ )); do + _stats_render_camera_view_page \ + "$html_dir" "$backhref" "$backhref_html" "$slug" \ + "${photo_list[i - 1]}" "$i" \ + "$(( i == 1 ? n : i - 1 ))" "$(( i == n ? 1 : i + 1 ))" + done +} + +# Render a single per-camera view page (header + cameraview body + footer). +_stats_render_camera_view_page() { + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -r photo="$1"; shift + local -ri index="$1"; shift + local -ri prev="$1"; shift + local -ri next="$1"; shift + local body + local background_image + local -r page="camera-$slug--$index.html" + + body=$(_stats_build_cameraview_body \ + "$backhref_html" "$slug" "$photo" "$prev" "$next") + background_image=$(_stats_random_background "$page") + template header "$page" \ + html_dir "$html_dir" backhref "$backhref" \ + blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \ + show_header_bar 'no' + template cameraview "$page" \ + html_dir "$html_dir" backhref "$backhref" cameraview_body "$body" + template footer "$page" \ + html_dir "$html_dir" backhref "$backhref" tarball_name '' +} + +# Build the body of a per-camera view page: the photo (linked to this camera's +# next photo) plus a navigator whose prev/next cycle within the camera, a link +# back to the gallery, an optional Details link to the album's details page for +# the photo, and a direct image link. +_stats_build_cameraview_body() { + local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -r photo="$1"; shift + local -ri prev="$1"; shift + local -ri next="$1"; shift + local photo_html + local animation_class + local tooltip + local tooltip_attr='' + local view_page + local details_link='' + + photo_html=$(_html_escape "$photo") + animation_class=$(random_animation_css_class fast "$photo") + tooltip=$(photo_exif_tooltip_text "$photo" "$INCOMING_DIR/$photo") + if [ -n "$tooltip" ]; then + tooltip_attr=" title=\"$(_html_escape "$tooltip")\"" + fi + view_page="${ALBUM_VIEW_PAGE_BY_PHOTO[$photo]:-}" + if [ -n "$view_page" ]; then + details_link=$(printf ' <a href="%s/%s-details.html">Details</a> |' \ + "$backhref_html" "$view_page") + fi + _stats_print_cameraview_body "$backhref_html" "$slug" "$photo_html" \ + "$animation_class" "$tooltip_attr" "$details_link" "$prev" "$next" +} + +# Emit the per-camera view page markup. Split out so _stats_build_cameraview_body +# stays focused on assembling the pieces. +_stats_print_cameraview_body() { + local -r backhref_html="$1"; shift + local -r slug="$1"; shift + local -r photo_html="$1"; shift + local -r animation_class="$1"; shift + local -r tooltip_attr="$1"; shift + local -r details_link="$1"; shift + local -ri prev="$1"; shift + local -ri next="$1"; shift + + cat <<END +<div class='view'> + <a href="$backhref_html/camera-$slug--$next.html"> + <img class='view $animation_class' border='0' src='$backhref_html/$STATS_CAMERA_PHOTOS_DIR/$photo_html'$tooltip_attr /> + </a> + <div class="navigator"> + <a href="$backhref_html/camera-$slug--$prev.html" class="arrow">⇐</a> + <a href="$backhref_html/camera-$slug.html">Gallery</a> |$details_link + <a href="$backhref_html/$STATS_CAMERA_PHOTOS_DIR/$photo_html">Direct link</a> + <a href="$backhref_html/camera-$slug--$next.html" class="arrow">⇒</a> + </div> +</div> +END } # Public render entry point (handoff for task rm0). Renders one diff --git a/src/lib/template.source.sh b/src/lib/template.source.sh index 026e3ce..1fc8894 100644 --- a/src/lib/template.source.sh +++ b/src/lib/template.source.sh @@ -148,6 +148,7 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_background_image_css|context_css|background_image|background_image|header splash' 'render_blurs_dir_css|context_css|blurs_dir|blurs_dir|header splash' 'render_camera_name_html|context_html|camera_name|camera_name|camera' + 'render_cameraview_body_html|context_raw|cameraview_body|cameraview_body|cameraview' 'render_camera_thumbs_html|context_raw|camera_thumbs|camera_thumbs|camera' 'render_current_date_text|current_date_html|||' 'render_enter_page_html|context_html|enter_page|enter_page|splash' diff --git a/tests/cli.sh b/tests/cli.sh index 14439bf..a05485c 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -3061,6 +3061,7 @@ test::stats_identify_output() { test_generate_stats_pages_created_and_nav_linked() { local camera_html + local camera_view_html local config_file local fake_bin local -i nav_links @@ -3093,16 +3094,22 @@ test_generate_stats_pages_created_and_nav_linked() { 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. + # The camera gallery is a mini album: thumbnails link to per-camera view + # pages (camera-<slug>--<n>.html), not the album view pages or raw images. camera_html=$(<"$TEST_TMPDIR/dist/camera-canon-eos-r5.html") test::assert_contains 'src="./thumbs/' "$camera_html" + test::assert_contains 'href="./camera-canon-eos-r5--1.html"' "$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 + + # A per-camera view page exists and its navigation stays within the camera: + # prev/next point at this camera's view pages, plus links back to the gallery + # and to the album details page for the photo. + test::assert_file_exists "$TEST_TMPDIR/dist/camera-canon-eos-r5--1.html" + camera_view_html=$(<"$TEST_TMPDIR/dist/camera-canon-eos-r5--1.html") + test::assert_contains 'href="./camera-canon-eos-r5--2.html"' "$camera_view_html" + test::assert_contains 'href="./camera-canon-eos-r5.html">Gallery</a>' \ + "$camera_view_html" + test::assert_contains '-details.html">Details</a>' "$camera_view_html" # 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) @@ -3853,6 +3860,8 @@ test_template_required_context_vars_come_from_render_specs() { local output expected=$(cat <<'END' +camera:backhref camera_name camera_thumbs html_dir +cameraview:cameraview_body html_dir details:animation_class backhref exif_details exif_tooltip html_dir page_num photo photos_dir preview_num footer:backhref html_dir tarball_name header:backhref background_image blurs_dir html_dir show_header_bar @@ -3870,6 +3879,8 @@ END repo_root="$1"; shift template_name='' declare -a template_names=( + camera + cameraview details footer header @@ -4021,6 +4032,15 @@ SHURIKEN_OUTPUT_MODE=quiet RANDOM_SEED=camera-test apply_config_defaults +# The per-camera view pages read each photo's EXIF tooltip from INCOMING_DIR, so +# provide empty stand-in files (no ImageMagick here, so the tooltip is empty but +# the view pages still render with full camera navigation). +INCOMING_DIR="$dist_dir/incoming" +mkdir -p "$INCOMING_DIR" +for stub in a.jpg b.jpg c.png d.jpg; do + : > "$INCOMING_DIR/$stub" +done + feed() { reset_photo_exif_stats accumulate_photo_stats 'a.jpg' <<'EXIF' @@ -4058,26 +4078,39 @@ 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, 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/. + # The Canon EOS 5D gallery lists exactly its two photos as thumbnails linking + # to this camera's own view pages (camera-<slug>--<n>.html), not the other + # camera's photo. 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 'href="../camera-canon-eos-5d--1.html"' "$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_contains 'href="../camera-canon-eos-5d--2.html"' "$html" test::assert_not_contains 'photos/c.png' "$html" test::assert_not_contains 'photos/d.jpg' "$html" + test::assert_not_contains 'href="../photos/' "$html" # Heading shows the (trusted) camera label and a back-to-stats link. test::assert_contains 'Canon EOS 5D' "$html" test::assert_contains '<a href="../stats.html">Back to stats</a>' "$html" - # The EXIF-derived label with & and < is HTML-escaped in the heading. + # Each per-camera view page exists and its prev/next stay within the camera + # (two photos, so view 1's prev and next both point at view 2), with a link + # back to the gallery. + test::assert_file_exists "$dist_dir/run1/camera-canon-eos-5d--1.html" + test::assert_file_exists "$dist_dir/run1/camera-canon-eos-5d--2.html" + html=$(cat "$dist_dir/run1/camera-canon-eos-5d--1.html") + test::assert_contains 'href="../camera-canon-eos-5d--2.html" class="arrow"' \ + "$html" + test::assert_contains 'href="../camera-canon-eos-5d.html">Gallery</a>' "$html" + test::assert_contains 'src=' "$html" + + # The EXIF-derived label with & and < is HTML-escaped in the heading, and its + # single photo links to that camera's own view page. html=$(cat "$dist_dir/run1/camera-nikon-co-z6.html") test::assert_contains 'Nikon & Co <Z6>' "$html" test::assert_not_contains 'Nikon & Co <Z6>' "$html" - test::assert_contains '<a href="../photos/c.png">' "$html" + test::assert_contains 'href="../camera-nikon-co-z6--1.html"' "$html" # Output is deterministic: the second run is byte-identical to the first. html=$(cat "$dist_dir/run1/camera-canon-eos-5d.html") |
