# 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//. 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 (.html, a thumbnail grid) plus # one view page per photo (--.html) whose prev/next cycle only # within that filter. The "--" 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// (gallery index.html + view pages # .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//...) 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 ".html" in the same directory -- # so the href prefix is empty (the main album passes "-"). 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 #
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 (.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//, so the # gallery is index.html and the view pages are .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 (--.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 \ ' Details |' \ "$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// 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 < $photo_html
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// 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 }