summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-15 13:55:32 +0300
committerPaul Buetow <paul@buetow.org>2026-06-15 23:50:40 +0300
commitbe6ae2c9541a01927616b46cf6a683d7ce4519ff (patch)
tree53161337bae46495b8e88b96f1b28b1bd6f9c2ca
parent6e47246dbbc97821b97aa9c45352abc9c2a9aa4a (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-xbin/shuriken205
-rw-r--r--share/templates/default/camera.tmpl15
-rw-r--r--share/templates/default/cameraview.tmpl10
-rw-r--r--src/lib/stats.source.sh204
-rw-r--r--src/lib/template.source.sh1
-rwxr-xr-xtests/cli.sh63
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">&lArr;</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">&rArr;</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} &mdash; ${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">&lArr;</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">&rArr;</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 &amp; Co &lt;Z6&gt;' "$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")