1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
|
# Per-filter mini-album page rendering. Split out of stats.source.sh (task cn0)
# so this concern is separate from the EXIF aggregation (stats-aggregate.source.sh)
# and the stats overview page (stats-render.source.sh). Every tallied bucket
# becomes a clickable mini album under dist/stats/<pagebase>/. This module reads
# the aggregated filter data filled by collect_photo_exif_stats -- through the
# stats-aggregate.source.sh accessors (stats_filter_count, stats_filter_pagebases,
# stats_filter_photos, stats_filter_title), never by indexing the aggregator's
# private STATS_FILTER_* maps directly -- and resolves each photo's album view
# page through the album_view_page_for_photo accessor (task pn0) instead of
# indexing the album's private ALBUM_VIEW_PAGE_BY_PHOTO global, so stats stays
# decoupled from both the aggregator's key conventions and album-internal page
# naming/caching. All libs are sourced before run so cross-module references
# resolve.
# ----------------------------------------------------------------------------
# Filter mini-album pages
# ----------------------------------------------------------------------------
# Every tallied bucket (camera, lens, year, month, aperture, ISO, ...) becomes a
# clickable, self-contained mini album. render_filter_pages turns each pagebase
# in STATS_FILTER_PHOTOS into a gallery (<pagebase>.html, a thumbnail grid) plus
# one view page per photo (<pagebase>--<index>.html) whose prev/next cycle only
# within that filter. The "--<index>" suffix cannot collide with another gallery
# name because a pagebase never contains "--". All pages reuse the album's shared
# photos/thumbs/blurs assets (only the HTML differs); view pages link "Details"
# to the album's own details page via the album_view_page_for_photo accessor,
# but only when DETAILS_PAGE=yes actually rendered that page (see
# _stats_build_filterview_body below).
# Pages render in
# parallel through the shared job pool, throttled to IMAGE_JOBS. The galleries
# reuse camera.tmpl and the view pages reuse cameraview.tmpl.
# Dist-relative subdirectories holding the shared full-size images, thumbnails
# and blurred backgrounds, matching the literals generate() passes to
# render_album_pages so filter pages reuse the same asset files.
declare -gr STATS_PHOTOS_DIR='photos'
declare -gr STATS_THUMBS_DIR='thumbs'
declare -gr STATS_BLURS_DIR='blurs'
# Everything stats-related lives under this dist subdirectory so the album root
# only holds the main album: the stats overview is stats/index.html and each
# filter mini-album is stats/<pagebase>/ (gallery index.html + view pages
# <index>.html). This keeps the file count in any single directory bounded.
declare -gr STATS_DIR='stats'
# Relative path from a filter mini-album page (dist/stats/<pagebase>/...) back to
# the album root (dist/), used for shared assets and album links.
declare -gr STATS_FILTER_BACKHREF='../..'
# Build the full thumbnail grid for one filter from its newline-separated photo
# list, preserving aggregation (encounter) order for deterministic output.
#
# This reuses the album's shared grid builder (append_preview_grid), so a filter
# mini-album gallery gets EXACTLY the same tiles as the main overview: 2x2
# feature tiles, subdivided tiles with sub-thumbnails, the seeded entry
# animations and the dramatic hover. The only mini-album-specific detail is the
# link target -- its view pages are bare "<index>.html" in the same directory --
# so the href prefix is empty (the main album passes "<page_num>-"). The
# thumbnail images come from the shared thumbs/ dir, reached via the gallery's
# backhref (../..). The result is wrapped by camera.tmpl in the same
# <div class="thumbs-grid"> container the main pages use.
_stats_build_filter_thumbs() {
local -r backhref_html="$1"; shift
# photos is a newline-joined string of photo paths, not an array. Other
# modules reuse the name "photos" as an array, so --check-sourced nameref
# aliasing misreports SC2178/SC2128; both are false positives here.
# shellcheck disable=SC2178
local -r photos="$1"; shift
local -a photo_list=()
local photo
local thumbs=''
# shellcheck disable=SC2128
while IFS= read -r photo; do
[ -n "$photo" ] && photo_list+=("$photo")
done <<< "$photos"
if (( ${#photo_list[@]} == 0 )); then
return
fi
# 'no' fill_last: stats mini-albums never widen a leftover tile to a full row
# (that orphan-final-page treatment is for the main album only).
append_preview_grid thumbs \
"$STATS_THUMBS_DIR" "$backhref_html" '' no "${photo_list[@]}"
printf '%s\n' "$thumbs"
}
# Render a filter gallery page (<pagebase>.html): header + camera.tmpl (heading +
# pre-built thumbnail grid) + footer. camera.tmpl is reused for every filter; the
# heading is the bucket's title (camera name, "ISO 400", "Year 2023", ...).
# Each filter mini-album lives in its own directory stats/<pagebase>/, so the
# gallery is index.html and the view pages are <index>.html. backhref is fixed
# (../.. back to the album root). camera.tmpl/cameraview.tmpl are reused.
_stats_render_filter_gallery() {
local -r pagebase="$1"; shift
local thumbs
local background_image
local -r html_dir="$STATS_DIR/$pagebase"
local -r backhref="$STATS_FILTER_BACKHREF"
local -r backhref_html="$STATS_FILTER_BACKHREF"
local title
title=$(stats_filter_title "$pagebase")
thumbs=$(_stats_build_filter_thumbs \
"$backhref_html" "$(stats_filter_photos "$pagebase")")
# Background fits the category: a random photo from this filter's own set,
# mirroring how the album preview pages pick a random album photo.
background_image=$(_stats_pick_background \
"$pagebase" "$(stats_filter_photos "$pagebase")")
template header index.html \
html_dir "$html_dir" backhref "$backhref" \
blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \
show_header_bar 'yes'
template camera index.html \
html_dir "$html_dir" backhref "$backhref" \
camera_name "$title" camera_thumbs "$thumbs"
template footer index.html \
html_dir "$html_dir" backhref "$backhref" tarball_name ''
}
# Render one filter view page (<pagebase>--<index>.html): header + cameraview
# body + footer, with prev/next cycling within the filter.
_stats_render_filter_view_page() {
local -r pagebase="$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 html_dir="$STATS_DIR/$pagebase"
local -r backhref="$STATS_FILTER_BACKHREF"
local -r page="$index.html"
body=$(_stats_build_filterview_body \
"$STATS_FILTER_BACKHREF" "$photo" "$prev" "$next")
# The view page's blurred background is the photo it shows, exactly like the
# album view pages.
background_image="$photo"
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 a filter view page body: the photo (linked to the filter's next photo)
# plus a navigator whose prev/next cycle within the filter, a link back to the
# gallery, an optional Details link to the album's details page, and a direct
# image link.
_stats_build_filterview_body() {
local -r backhref_html="$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_for_photo "$photo")
# Only link to the album's details page when it was actually rendered
# (DETAILS_PAGE=yes); otherwise album_view_page_for_photo resolving a page
# would still point at a details file that render_photo_view_and_details
# never wrote, leaving a dangling link.
if [ -n "$view_page" ] && [ "$DETAILS_PAGE" = yes ]; then
details_link=$(printf \
' <a href="%s/%s-details.html">Details</a> <span class="nav-sep">|</span>' \
"$backhref_html" "$view_page")
fi
_stats_print_filterview_body "$backhref_html" "$photo_html" \
"$animation_class" "$tooltip_attr" "$details_link" "$prev" "$next"
}
# Emit the filter view page markup. Split out so _stats_build_filterview_body
# stays focused on assembling the pieces.
# The prev/next/gallery links are same-directory (this view page lives in the
# filter's own stats/<pagebase>/ dir); the image, details, and direct links go
# back to the album root via backhref.
_stats_print_filterview_body() {
local -r backhref_html="$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="$next.html">
<img class='view $animation_class' alt='$photo_html' src='$backhref_html/$STATS_PHOTOS_DIR/$photo_html'$tooltip_attr />
</a>
<div class="navigator">
<a href="$prev.html" class="arrow">⇐</a>
<a href="index.html">Gallery</a> <span class="nav-sep">|</span>$details_link
<a href="$backhref_html/$STATS_PHOTOS_DIR/$photo_html">Direct link</a>
<a href="$next.html" class="arrow">⇒</a>
</div>
</div>
END
}
# Enqueue one filter's mini-album (gallery + a view page per photo) onto the
# shared render job pool, waiting for a free slot (<= IMAGE_JOBS) before each
# background render so parallelism follows the configured IMAGE_JOBS.
_stats_enqueue_filter_album() {
local -r pagebase="$1"; shift
local -r pool="$1"; shift
local photo
local -a photo_list=()
local -i i n
while IFS= read -r photo; do
[ -n "$photo" ] && photo_list+=("$photo")
done <<< "$(stats_filter_photos "$pagebase")"
n=${#photo_list[@]}
job_pool_submit "$pool" "filter gallery $pagebase" \
_stats_render_filter_gallery "$pagebase"
for (( i = 1; i <= n; i++ )); do
job_pool_submit "$pool" "filter view $pagebase/$i" \
_stats_render_filter_view_page \
"$pagebase" "${photo_list[i - 1]}" "$i" \
"$(( i == 1 ? n : i - 1 ))" "$(( i == n ? 1 : i + 1 ))"
done
}
# Public entry point: render every filter mini-album in parallel. Call
# collect_photo_exif_stats first to fill STATS_FILTER_PHOTOS. html_dir is the
# dist-relative output dir ("." for a top-level album) and backhref the relative
# path back to the album root ("."), the same values render_stats_page uses.
# Pagebases are walked in LC_ALL=C order for reproducible enqueue order.
# Render every filter mini-album under dist/stats/<pagebase>/ in parallel. Each
# mini-album's location and backhref are fixed by the layout (see STATS_DIR /
# STATS_FILTER_BACKHREF), so no path arguments are needed. Call
# collect_photo_exif_stats first to fill STATS_FILTER_PHOTOS.
render_filter_pages() {
local pagebase
if (( $(stats_filter_count) == 0 )); then
return
fi
# Render job pool (max IMAGE_JOBS concurrent), addressed by the single handle
# "render_jobs". job_pool_wait returns 1 if any render job failed.
job_pool_init render_jobs
# stats_filter_pagebases yields the pagebases in LC_ALL=C-sorted order, so the
# enqueue order is reproducible.
while IFS= read -r pagebase; do
_stats_enqueue_filter_album "$pagebase" render_jobs
done < <(stats_filter_pagebases)
job_pool_wait render_jobs
}
|