diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-25 12:16:17 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-25 12:16:17 +0300 |
| commit | 964901d672d8e12baf4ac0eb29821d670ae15eb8 (patch) | |
| tree | dea3e16972b0d6caa9bfbf2aa063d8c3fdb88cab | |
| parent | 3377635c88a6a2f6631a4c416c32f0d57ec05ccd (diff) | |
Flush preview-grid rows: fixed breakpoint columns + multiple-of-12 pages
The overview grid used auto-fill columns (an unpredictable count at view
time) while tiles per page were fixed at generation, so the last row was
ragged -- an empty, cut-off bottom-right corner, made worse by a 2x2
feature tile.
CSS (header.tmpl): replace auto-fill with a FIXED column count per width
breakpoint -- 2 (phone) / 3 / 4 / 6 -- all divisors of 12. THUMBHEIGHT no
longer drives the grid (it only sizes the thumbnail files), so its
obsolete render-var is dropped (template.source.sh).
Generator (album-tile-layout, album-thumbnail-html): append_preview_grid
now decides a page's tiles, then snaps the grid-cell total onto a multiple
of 12 before emitting, via two photo-preserving levers --
_grid_split_subdivides_to_add (round up: split subdivided tiles into
singles; preferred, abundant) and _grid_merge_singles_to_remove (round
down: merge adjacent singles). Because 2/3/4/6 all divide 12, a
multiple-of-12 page tiles into a COMPLETE rectangle at every breakpoint:
a flush last row at any window width, with no image distortion
(object-fit: cover). Per-photo preview numbers and all navigation
redirects are unchanged. Tiny pages (a short final page or small stats
mini-album) are left as-is.
Decrements use assignment, not bare "(( --k ))": under set -euo pipefail
an arithmetic command evaluating to 0 returns status 1 and would abort
generate. Helper namerefs are uniquely named and arrays are forwarded by
name to avoid bash circular-nameref errors.
Tests: add test_album_grid_cells_align_to_multiple_of_12 (both levers);
update the two tests that pinned the old auto-fill CSS / render-var list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rwxr-xr-x | bin/shuriken | 202 | ||||
| -rw-r--r-- | share/templates/default/header.tmpl | 44 | ||||
| -rw-r--r-- | src/lib/album-thumbnail-html.source.sh | 42 | ||||
| -rw-r--r-- | src/lib/album-tile-layout.source.sh | 159 | ||||
| -rw-r--r-- | src/lib/template.source.sh | 1 | ||||
| -rwxr-xr-x | tests/cli.sh | 104 |
6 files changed, 517 insertions, 35 deletions
diff --git a/bin/shuriken b/bin/shuriken index da1da0b..e70b770 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -801,7 +801,6 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_stats_page_html|config_html|STATS_PAGE||header' 'render_tarball_include|tarball_include|||footer' 'render_tarball_name_html|context_html|tarball_name|tarball_name|footer' - 'render_thumbheight_html|config_html|THUMBHEIGHT||header' 'render_thumbs_dir_html|context_html|thumbs_dir|thumbs_dir|preview' 'render_title_html|config_html|TITLE||camera header splash stats' 'render_view_next_html|preview_num_next_html|preview_num||details view' @@ -2703,6 +2702,165 @@ tile_layout_for() { printf '%s %s\n' "${names[choice]}" "${counts[choice]}" } +# The valid subdivided-tile layout name for a given photo count (2..4); used when +# a split leaves a smaller-but-still-subdivided remainder. A count of 1 is a +# plain single and never asks here. +_grid_subdivide_layout_for() { + case "$1" in + 2) printf 'two_wide\n' ;; + 3) printf 'squares_wide_top\n' ;; + *) printf 'quad\n' ;; + esac +} + +# Snap a page's tiles onto a grid-cell total that is a multiple of 12 so the +# fixed 2/3/4/6-column overview grid (all divisors of 12) forms a COMPLETE +# rectangle at every breakpoint -- no ragged, cut-off last row at any window +# width. The cell footprint is 4 for a 2x2 "feature" tile and 1 for every other +# tile, so a page's natural total is rarely a multiple of 12. +# +# Two levers reach the nearest reachable multiple of 12, both keeping the SAME +# photos in the SAME order (so per-photo preview numbers / view-page links never +# change), only their visual grouping shifts: +# - PREFERRED, round up: split subdivided tiles into singles (+1 cell per photo +# peeled). Abundant on a normal album (many subdivided tiles) and needs no +# adjacency, so this is the reliable lever. +# - FALLBACK, round down: merge adjacent single tiles into two-up tiles (-1 cell +# per merge). Used when there are too few subdivided cells to round up (e.g. a +# single-heavy page, or THUMB_SUBDIVIDE_PERCENT=0). +# Left untouched when the page has fewer than 12 cells (a tiny stats mini-album or +# the album's short final page) or neither lever can reach a multiple of 12. +# Operates in place on the parallel layout/start/count arrays passed by name. +# The three array arguments are the NAMES of the caller's parallel arrays (passed +# as strings and forwarded by name to the chosen lever helper). We bind only +# read-only local namerefs here (uniquely named so they never alias a caller's +# nameref of the same name -- bash would treat that as a circular reference). +_align_page_tiles_to_grid() { + local -r layouts_name="$1"; shift + local -r starts_name="$1"; shift + local -r counts_name="$1"; shift + local -ri total_cells="$1"; shift + + local -ri remainder=$(( total_cells % 12 )) + if (( total_cells < 12 || remainder == 0 )); then + return + fi + + # How many extra cells splitting every subdivided tile fully could yield. + # shellcheck disable=SC2178 + local -n align_layouts="$layouts_name" + # shellcheck disable=SC2178 + local -n align_counts="$counts_name" + local -i capacity_up=0 i + for (( i = 0; i < ${#align_layouts[@]}; i++ )); do + if [ "${align_layouts[i]}" != single ] \ + && [ "${align_layouts[i]}" != feature ] \ + && (( align_counts[i] >= 2 )); then + capacity_up=$(( capacity_up + align_counts[i] - 1 )) + fi + done + + if (( capacity_up >= 12 - remainder )); then + _grid_split_subdivides_to_add \ + "$layouts_name" "$starts_name" "$counts_name" "$(( 12 - remainder ))" + else + _grid_merge_singles_to_remove \ + "$layouts_name" "$starts_name" "$counts_name" "$remainder" + fi +} + +# Round a page UP to the next multiple of 12 by peeling `to_add` photos off +# subdivided tiles into trailing singles (each peel: +1 cell, photos preserved in +# order). Peeling the tail of a subdivide keeps both pieces contiguous, so the +# kept remainder (a smaller subdivide, or a single when only one photo is left) +# and the peeled singles stay in photo order. Rebuilds the arrays in place. +_grid_split_subdivides_to_add() { + # shellcheck disable=SC2178 + local -n split_layouts="$1"; shift + # shellcheck disable=SC2178 + local -n split_starts="$1"; shift + # shellcheck disable=SC2178 + local -n split_counts="$1"; shift + local -i to_add="$1"; shift + + local -a new_layouts=() new_starts=() new_counts=() + local -i i p peel keep start cnt + for (( i = 0; i < ${#split_layouts[@]}; i++ )); do + start=${split_starts[i]} + cnt=${split_counts[i]} + if (( to_add > 0 )) && [ "${split_layouts[i]}" != single ] \ + && [ "${split_layouts[i]}" != feature ] && (( cnt >= 2 )); then + peel=$(( to_add < cnt - 1 ? to_add : cnt - 1 )) + keep=$(( cnt - peel )) + if (( keep == 1 )); then + new_layouts+=(single) + else + new_layouts+=("$(_grid_subdivide_layout_for "$keep")") + fi + new_starts+=("$start") + new_counts+=("$keep") + for (( p = 0; p < peel; p++ )); do + new_layouts+=(single) + new_starts+=("$(( start + keep + p ))") + new_counts+=(1) + done + to_add=$(( to_add - peel )) + else + new_layouts+=("${split_layouts[i]}") + new_starts+=("$start") + new_counts+=("$cnt") + fi + done + + split_layouts=("${new_layouts[@]}") + split_starts=("${new_starts[@]}") + split_counts=("${new_counts[@]}") +} + +# Round a page DOWN to the previous multiple of 12 by merging `to_remove` pairs of +# adjacent single tiles into two-up "two_wide" tiles (each merge: -1 cell). Walks +# right-to-left building a reversed result, then un-reverses it. Decrements use +# assignment ("k=$(( k - 1 ))"), never a bare "(( --k ))": under set -euo pipefail +# an arithmetic command whose result is 0 (e.g. --k reaching 0) returns status 1 +# and would abort the whole generate; an assignment always returns 0. +_grid_merge_singles_to_remove() { + # shellcheck disable=SC2178 + local -n merge_layouts="$1"; shift + # shellcheck disable=SC2178 + local -n merge_starts="$1"; shift + # shellcheck disable=SC2178 + local -n merge_counts="$1"; shift + local -i merges_left="$1"; shift + + local -a new_layouts=() new_starts=() new_counts=() + local -i k=${#merge_layouts[@]} j + while (( k > 0 )); do + k=$(( k - 1 )) + if (( merges_left > 0 && k > 0 )) \ + && [ "${merge_layouts[k]}" = single ] \ + && [ "${merge_layouts[k - 1]}" = single ]; then + new_layouts+=(two_wide) + new_starts+=("${merge_starts[k - 1]}") + new_counts+=(2) + merges_left=$(( merges_left - 1 )) + k=$(( k - 1 )) + else + new_layouts+=("${merge_layouts[k]}") + new_starts+=("${merge_starts[k]}") + new_counts+=("${merge_counts[k]}") + fi + done + + merge_layouts=() merge_starts=() merge_counts=() + j=${#new_layouts[@]} + while (( j > 0 )); do + j=$(( j - 1 )) + merge_layouts+=("${new_layouts[j]}") + merge_starts+=("${new_starts[j]}") + merge_counts+=("${new_counts[j]}") + done +} + # Render one tile's markup (no trailing newline). A "single" tile is the plain # square thumbnail, byte-identical to the previous per-thumbnail output. A # "feature" tile is the same single thumbnail but with the 'feature' anchor class @@ -2801,6 +2959,13 @@ build_subdivided_tile() { # the single shared grid builder for both the main preview pages and the stats # mini-album galleries. Tile blocks are separated by a single newline; the # template adds the trailing newline. +# +# Three passes: (1) decide the whole page's tiles, recording each tile's layout, +# first-photo index and photo count and accumulating the grid-cell footprint; +# (2) snap that cell total onto a multiple of 12 (_align_page_tiles_to_grid) so +# the fixed 2/3/4/6-column grid is a complete rectangle -- a flush last row -- at +# every width; (3) emit the (possibly merged) tiles. Splitting decide-from-emit +# is what lets pass 2 adjust the layout before any HTML is built. append_preview_grid() { local -n buffer_ref="$1"; shift local -r thumbs_dir="$1"; shift @@ -2809,6 +2974,7 @@ append_preview_grid() { local -a photos=("$@") local -i i=0 local -i count + local -i total_cells=0 local layout local block # Cap the big 2x2 feature tiles at this many per page; once reached, later @@ -2816,27 +2982,49 @@ append_preview_grid() { # subdivided/single), so a page never gets crowded with hero tiles. local -ri max_features=2 local -i features_used=0 + # Parallel records of the page's tiles: layout, the first photo's 0-based + # index, and how many photos the tile spans. + local -a tile_layouts=() + local -a tile_starts=() + local -a tile_counts=() + local -i t + # Pass 1: decide every tile (deterministic, seeded off each tile's first photo + # name). A 2x2 feature occupies 4 grid cells; every other tile occupies 1. while (( i < ${#photos[@]} )); do - # Decide this tile's layout from the photos still available; the first - # photo's name is the seeded-random context so the choice is stable. - # Features are only offered until the per-page cap is reached. read -r layout count < <( tile_layout_for "$(( ${#photos[@]} - i ))" "${photos[i]}" \ "$(( features_used < max_features ? 1 : 0 ))" ) if [ "$layout" = feature ]; then (( ++features_used )) + (( total_cells += 4 )) + else + (( ++total_cells )) fi + tile_layouts+=("$layout") + tile_starts+=("$i") + tile_counts+=("$count") + (( i += count )) + done + + # Pass 2: force the cell total onto a multiple of 12 so the grid is flush at + # every column breakpoint (no-op for tiny pages / too-few-singles -- see the + # helper). Per-photo preview numbers are preserved. + _align_page_tiles_to_grid \ + tile_layouts tile_starts tile_counts "$total_cells" + + # Pass 3: emit the tile blocks in order. + for (( t = 0; t < ${#tile_layouts[@]}; t++ )); do block=$(build_tile_block \ - "$thumbs_dir" "$backhref" "$href_prefix" "$layout" "$(( i + 1 ))" \ - "${photos[@]:i:count}") + "$thumbs_dir" "$backhref" "$href_prefix" "${tile_layouts[t]}" \ + "$(( tile_starts[t] + 1 ))" \ + "${photos[@]:${tile_starts[t]}:${tile_counts[t]}}") if [ -z "$buffer_ref" ]; then buffer_ref="$block" else buffer_ref+=$'\n'"$block" fi - (( i += count )) done } diff --git a/share/templates/default/header.tmpl b/share/templates/default/header.tmpl index d5d1648..c0a0fac 100644 --- a/share/templates/default/header.tmpl +++ b/share/templates/default/header.tmpl @@ -197,27 +197,37 @@ cat <<END vertical-align: top; } - /* The album overview is a CSS grid. THUMBHEIGHT is the MINIMUM cell - width: columns grow (1fr) to fill the row, so the thumbnails use the - full window width instead of leaving big margins left and right, and - there is no centred leftover. Each 1x1 grid item is kept square with - aspect-ratio, so the auto-sized rows are square too; a "feature" tile - then spans a 2x2 block (a large hero image) and a subdivided tile packs - several smaller thumbnails into one cell. grid-auto-flow: dense lets - the smaller tiles backfill the holes a 2x2 feature would otherwise - leave. Spacing comes from the grid gap, so images carry no padding. */ + /* The album overview is a CSS grid with a FIXED column count chosen by + width breakpoints (not auto-fill). The counts are 2 / 3 / 4 / 6 -- all + divisors of 12 -- so a page whose tile-cell total is a multiple of 12 + (the generator guarantees this, see append_preview_grid) forms a + COMPLETE rectangle at every breakpoint: no ragged, cut-off last row at + any window width. Columns are 1fr, so the thumbnails grow to fill the + full window width instead of leaving margins. Each 1x1 grid item is + kept square with aspect-ratio; a "feature" tile spans a 2x2 block (a + large hero image) and a subdivided tile packs several smaller + thumbnails into one cell. grid-auto-flow: dense lets the smaller tiles + backfill the holes a 2x2 feature would otherwise leave. Spacing comes + from the grid gap, so images carry no padding. Mobile-first: the base + rule is the 2-column phone layout; min-width breakpoints widen it. */ div.thumbs-grid { display: grid; - /* min(THUMBHEIGHT, 100%) keeps a column from ever exceeding the - viewport width, so narrow phones never scroll sideways; on wide - screens the columns are >= THUMBHEIGHT and grow (1fr) to fill. */ - grid-template-columns: - repeat(auto-fill, minmax(min(${render_thumbheight_html}px, 100%), 1fr)); + grid-template-columns: repeat(2, 1fr); grid-auto-flow: dense; gap: 10px; margin: 0 4px; } + @media (min-width: 700px) { + div.thumbs-grid { grid-template-columns: repeat(3, 1fr); } + } + @media (min-width: 1100px) { + div.thumbs-grid { grid-template-columns: repeat(4, 1fr); } + } + @media (min-width: 1600px) { + div.thumbs-grid { grid-template-columns: repeat(6, 1fr); } + } + div.thumbs-grid img { padding: 0; } @@ -385,11 +395,11 @@ cat <<END } } - /* Phone layout: a 2-column square grid, and the navigator/footer/splash - links become large, well-spaced tap targets (~44px). Pure CSS. */ + /* Phone layout: the base rule already gives a 2-column grid; here we just + tighten the gap/margin, and the navigator/footer/splash links become + large, well-spaced tap targets (~44px). Pure CSS. */ @media (max-width: 700px) { div.thumbs-grid { - grid-template-columns: 1fr 1fr; gap: 6px; margin: 0 2px; } diff --git a/src/lib/album-thumbnail-html.source.sh b/src/lib/album-thumbnail-html.source.sh index 8f34695..add5f36 100644 --- a/src/lib/album-thumbnail-html.source.sh +++ b/src/lib/album-thumbnail-html.source.sh @@ -22,6 +22,13 @@ # the single shared grid builder for both the main preview pages and the stats # mini-album galleries. Tile blocks are separated by a single newline; the # template adds the trailing newline. +# +# Three passes: (1) decide the whole page's tiles, recording each tile's layout, +# first-photo index and photo count and accumulating the grid-cell footprint; +# (2) snap that cell total onto a multiple of 12 (_align_page_tiles_to_grid) so +# the fixed 2/3/4/6-column grid is a complete rectangle -- a flush last row -- at +# every width; (3) emit the (possibly merged) tiles. Splitting decide-from-emit +# is what lets pass 2 adjust the layout before any HTML is built. append_preview_grid() { local -n buffer_ref="$1"; shift local -r thumbs_dir="$1"; shift @@ -30,6 +37,7 @@ append_preview_grid() { local -a photos=("$@") local -i i=0 local -i count + local -i total_cells=0 local layout local block # Cap the big 2x2 feature tiles at this many per page; once reached, later @@ -37,27 +45,49 @@ append_preview_grid() { # subdivided/single), so a page never gets crowded with hero tiles. local -ri max_features=2 local -i features_used=0 + # Parallel records of the page's tiles: layout, the first photo's 0-based + # index, and how many photos the tile spans. + local -a tile_layouts=() + local -a tile_starts=() + local -a tile_counts=() + local -i t + # Pass 1: decide every tile (deterministic, seeded off each tile's first photo + # name). A 2x2 feature occupies 4 grid cells; every other tile occupies 1. while (( i < ${#photos[@]} )); do - # Decide this tile's layout from the photos still available; the first - # photo's name is the seeded-random context so the choice is stable. - # Features are only offered until the per-page cap is reached. read -r layout count < <( tile_layout_for "$(( ${#photos[@]} - i ))" "${photos[i]}" \ "$(( features_used < max_features ? 1 : 0 ))" ) if [ "$layout" = feature ]; then (( ++features_used )) + (( total_cells += 4 )) + else + (( ++total_cells )) fi + tile_layouts+=("$layout") + tile_starts+=("$i") + tile_counts+=("$count") + (( i += count )) + done + + # Pass 2: force the cell total onto a multiple of 12 so the grid is flush at + # every column breakpoint (no-op for tiny pages / too-few-singles -- see the + # helper). Per-photo preview numbers are preserved. + _align_page_tiles_to_grid \ + tile_layouts tile_starts tile_counts "$total_cells" + + # Pass 3: emit the tile blocks in order. + for (( t = 0; t < ${#tile_layouts[@]}; t++ )); do block=$(build_tile_block \ - "$thumbs_dir" "$backhref" "$href_prefix" "$layout" "$(( i + 1 ))" \ - "${photos[@]:i:count}") + "$thumbs_dir" "$backhref" "$href_prefix" "${tile_layouts[t]}" \ + "$(( tile_starts[t] + 1 ))" \ + "${photos[@]:${tile_starts[t]}:${tile_counts[t]}}") if [ -z "$buffer_ref" ]; then buffer_ref="$block" else buffer_ref+=$'\n'"$block" fi - (( i += count )) done } diff --git a/src/lib/album-tile-layout.source.sh b/src/lib/album-tile-layout.source.sh index b30981b..cbddb6d 100644 --- a/src/lib/album-tile-layout.source.sh +++ b/src/lib/album-tile-layout.source.sh @@ -66,6 +66,165 @@ tile_layout_for() { printf '%s %s\n' "${names[choice]}" "${counts[choice]}" } +# The valid subdivided-tile layout name for a given photo count (2..4); used when +# a split leaves a smaller-but-still-subdivided remainder. A count of 1 is a +# plain single and never asks here. +_grid_subdivide_layout_for() { + case "$1" in + 2) printf 'two_wide\n' ;; + 3) printf 'squares_wide_top\n' ;; + *) printf 'quad\n' ;; + esac +} + +# Snap a page's tiles onto a grid-cell total that is a multiple of 12 so the +# fixed 2/3/4/6-column overview grid (all divisors of 12) forms a COMPLETE +# rectangle at every breakpoint -- no ragged, cut-off last row at any window +# width. The cell footprint is 4 for a 2x2 "feature" tile and 1 for every other +# tile, so a page's natural total is rarely a multiple of 12. +# +# Two levers reach the nearest reachable multiple of 12, both keeping the SAME +# photos in the SAME order (so per-photo preview numbers / view-page links never +# change), only their visual grouping shifts: +# - PREFERRED, round up: split subdivided tiles into singles (+1 cell per photo +# peeled). Abundant on a normal album (many subdivided tiles) and needs no +# adjacency, so this is the reliable lever. +# - FALLBACK, round down: merge adjacent single tiles into two-up tiles (-1 cell +# per merge). Used when there are too few subdivided cells to round up (e.g. a +# single-heavy page, or THUMB_SUBDIVIDE_PERCENT=0). +# Left untouched when the page has fewer than 12 cells (a tiny stats mini-album or +# the album's short final page) or neither lever can reach a multiple of 12. +# Operates in place on the parallel layout/start/count arrays passed by name. +# The three array arguments are the NAMES of the caller's parallel arrays (passed +# as strings and forwarded by name to the chosen lever helper). We bind only +# read-only local namerefs here (uniquely named so they never alias a caller's +# nameref of the same name -- bash would treat that as a circular reference). +_align_page_tiles_to_grid() { + local -r layouts_name="$1"; shift + local -r starts_name="$1"; shift + local -r counts_name="$1"; shift + local -ri total_cells="$1"; shift + + local -ri remainder=$(( total_cells % 12 )) + if (( total_cells < 12 || remainder == 0 )); then + return + fi + + # How many extra cells splitting every subdivided tile fully could yield. + # shellcheck disable=SC2178 + local -n align_layouts="$layouts_name" + # shellcheck disable=SC2178 + local -n align_counts="$counts_name" + local -i capacity_up=0 i + for (( i = 0; i < ${#align_layouts[@]}; i++ )); do + if [ "${align_layouts[i]}" != single ] \ + && [ "${align_layouts[i]}" != feature ] \ + && (( align_counts[i] >= 2 )); then + capacity_up=$(( capacity_up + align_counts[i] - 1 )) + fi + done + + if (( capacity_up >= 12 - remainder )); then + _grid_split_subdivides_to_add \ + "$layouts_name" "$starts_name" "$counts_name" "$(( 12 - remainder ))" + else + _grid_merge_singles_to_remove \ + "$layouts_name" "$starts_name" "$counts_name" "$remainder" + fi +} + +# Round a page UP to the next multiple of 12 by peeling `to_add` photos off +# subdivided tiles into trailing singles (each peel: +1 cell, photos preserved in +# order). Peeling the tail of a subdivide keeps both pieces contiguous, so the +# kept remainder (a smaller subdivide, or a single when only one photo is left) +# and the peeled singles stay in photo order. Rebuilds the arrays in place. +_grid_split_subdivides_to_add() { + # shellcheck disable=SC2178 + local -n split_layouts="$1"; shift + # shellcheck disable=SC2178 + local -n split_starts="$1"; shift + # shellcheck disable=SC2178 + local -n split_counts="$1"; shift + local -i to_add="$1"; shift + + local -a new_layouts=() new_starts=() new_counts=() + local -i i p peel keep start cnt + for (( i = 0; i < ${#split_layouts[@]}; i++ )); do + start=${split_starts[i]} + cnt=${split_counts[i]} + if (( to_add > 0 )) && [ "${split_layouts[i]}" != single ] \ + && [ "${split_layouts[i]}" != feature ] && (( cnt >= 2 )); then + peel=$(( to_add < cnt - 1 ? to_add : cnt - 1 )) + keep=$(( cnt - peel )) + if (( keep == 1 )); then + new_layouts+=(single) + else + new_layouts+=("$(_grid_subdivide_layout_for "$keep")") + fi + new_starts+=("$start") + new_counts+=("$keep") + for (( p = 0; p < peel; p++ )); do + new_layouts+=(single) + new_starts+=("$(( start + keep + p ))") + new_counts+=(1) + done + to_add=$(( to_add - peel )) + else + new_layouts+=("${split_layouts[i]}") + new_starts+=("$start") + new_counts+=("$cnt") + fi + done + + split_layouts=("${new_layouts[@]}") + split_starts=("${new_starts[@]}") + split_counts=("${new_counts[@]}") +} + +# Round a page DOWN to the previous multiple of 12 by merging `to_remove` pairs of +# adjacent single tiles into two-up "two_wide" tiles (each merge: -1 cell). Walks +# right-to-left building a reversed result, then un-reverses it. Decrements use +# assignment ("k=$(( k - 1 ))"), never a bare "(( --k ))": under set -euo pipefail +# an arithmetic command whose result is 0 (e.g. --k reaching 0) returns status 1 +# and would abort the whole generate; an assignment always returns 0. +_grid_merge_singles_to_remove() { + # shellcheck disable=SC2178 + local -n merge_layouts="$1"; shift + # shellcheck disable=SC2178 + local -n merge_starts="$1"; shift + # shellcheck disable=SC2178 + local -n merge_counts="$1"; shift + local -i merges_left="$1"; shift + + local -a new_layouts=() new_starts=() new_counts=() + local -i k=${#merge_layouts[@]} j + while (( k > 0 )); do + k=$(( k - 1 )) + if (( merges_left > 0 && k > 0 )) \ + && [ "${merge_layouts[k]}" = single ] \ + && [ "${merge_layouts[k - 1]}" = single ]; then + new_layouts+=(two_wide) + new_starts+=("${merge_starts[k - 1]}") + new_counts+=(2) + merges_left=$(( merges_left - 1 )) + k=$(( k - 1 )) + else + new_layouts+=("${merge_layouts[k]}") + new_starts+=("${merge_starts[k]}") + new_counts+=("${merge_counts[k]}") + fi + done + + merge_layouts=() merge_starts=() merge_counts=() + j=${#new_layouts[@]} + while (( j > 0 )); do + j=$(( j - 1 )) + merge_layouts+=("${new_layouts[j]}") + merge_starts+=("${new_starts[j]}") + merge_counts+=("${new_counts[j]}") + done +} + # Render one tile's markup (no trailing newline). A "single" tile is the plain # square thumbnail, byte-identical to the previous per-thumbnail output. A # "feature" tile is the same single thumbnail but with the 'feature' anchor class diff --git a/src/lib/template.source.sh b/src/lib/template.source.sh index 016d2a2..6201647 100644 --- a/src/lib/template.source.sh +++ b/src/lib/template.source.sh @@ -262,7 +262,6 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_stats_page_html|config_html|STATS_PAGE||header' 'render_tarball_include|tarball_include|||footer' 'render_tarball_name_html|context_html|tarball_name|tarball_name|footer' - 'render_thumbheight_html|config_html|THUMBHEIGHT||header' 'render_thumbs_dir_html|context_html|thumbs_dir|thumbs_dir|preview' 'render_title_html|config_html|TITLE||camera header splash stats' 'render_view_next_html|preview_num_next_html|preview_num||details view' diff --git a/tests/cli.sh b/tests/cli.sh index 2fff1a7..e3af898 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -578,9 +578,11 @@ test_generate_cli_overrides_config_values() { test::assert_path_absent "$TEST_TMPDIR/config-dist" test::assert_find_count 0 "$TEST_TMPDIR/cli-dist" '*.tar' test::assert_contains '<title>CLI title</title>' "$page_html" - # THUMBHEIGHT is the grid's minimum cell width (columns grow to fill); it is - # wrapped in min(...,100%) so a column never overflows a narrow phone. - test::assert_contains 'minmax(min(45px, 100%), 1fr)' "$page_html" + # The overview grid uses a FIXED column count per width breakpoint (2/3/4/6), + # not auto-fill, so each page tiles into a complete rectangle (flush last row). + # THUMBHEIGHT no longer drives the grid -- it only sizes the thumbnail files. + test::assert_contains 'grid-template-columns: repeat(2, 1fr)' "$page_html" + test::assert_contains 'grid-template-columns: repeat(6, 1fr)' "$page_html" test::assert_contains 'max-height: 456px;' "$view_html" test::assert_contains 'Next 1 pictures' "$page_html" test::assert_contains 'href="page-2.html" class="arrow">⇒</a>' \ @@ -3287,6 +3289,98 @@ test_render_view_redirects_wraps_when_last_page_full() { test::teardown } +# _align_page_tiles_to_grid snaps a page's grid-cell total onto a multiple of 12 +# (so the fixed 2/3/4/6-column overview grid is a flush rectangle at every +# breakpoint) by merging adjacent single tiles into two-up tiles, WITHOUT +# dropping or reordering any photo. Drive the helper directly with synthetic tile +# arrays and assert the post-merge cell total is a multiple of 12 and every photo +# is still covered exactly once. +test_album_grid_cells_align_to_multiple_of_12() { + local -a layouts=() starts=() counts=() + local -i n cells=0 covered=0 idx + + test::setup + # shellcheck source=/dev/null + source <(sed '$d' "$TEST_SHURIKEN") + + # 40 single tiles => 40 cells; not a multiple of 12. After alignment the + # remainder (40 % 12 == 4) is absorbed by merging 4 single pairs -> 36 cells. + for (( n = 0; n < 40; n++ )); do + layouts+=(single) + starts+=("$n") + counts+=(1) + done + _align_page_tiles_to_grid layouts starts counts 40 + + for (( idx = 0; idx < ${#layouts[@]}; idx++ )); do + if [ "${layouts[idx]}" = feature ]; then + (( cells += 4 )) + else + (( ++cells )) + fi + (( covered += counts[idx] )) + done + + if (( cells % 12 != 0 )); then + printf 'FAIL: aligned grid cell total %d is not a multiple of 12\n' \ + "$cells" >&2 + exit 1 + fi + if (( covered != 40 )); then + printf 'FAIL: alignment changed photo coverage: %d (want 40)\n' \ + "$covered" >&2 + exit 1 + fi + + # A page already on a multiple of 12 (24 singles) must be left untouched. + layouts=() starts=() counts=() + for (( n = 0; n < 24; n++ )); do + layouts+=(single) + starts+=("$n") + counts+=(1) + done + _align_page_tiles_to_grid layouts starts counts 24 + if (( ${#layouts[@]} != 24 )); then + printf 'FAIL: a 24-cell page was modified (%d tiles, want 24)\n' \ + "${#layouts[@]}" >&2 + exit 1 + fi + + # Subdivide-heavy page: 5 singles + 12 two_wide tiles => 17 cells. With plenty + # of subdivided cells the helper rounds UP (splits subdivides into singles) to + # 24, keeping all 5 + 24 = 29 photos. Exercises the preferred split lever. + layouts=() starts=() counts=() + local -i s=0 + for (( n = 0; n < 5; n++ )); do + layouts+=(single); starts+=("$s"); counts+=(1); s=$(( s + 1 )) + done + for (( n = 0; n < 12; n++ )); do + layouts+=(two_wide); starts+=("$s"); counts+=(2); s=$(( s + 2 )) + done + _align_page_tiles_to_grid layouts starts counts 17 + cells=0 + covered=0 + for (( idx = 0; idx < ${#layouts[@]}; idx++ )); do + if [ "${layouts[idx]}" = feature ]; then + (( cells += 4 )) + else + (( ++cells )) + fi + (( covered += counts[idx] )) + done + if (( cells % 12 != 0 )); then + printf 'FAIL: split path cell total %d is not a multiple of 12\n' \ + "$cells" >&2 + exit 1 + fi + if (( covered != 29 )); then + printf 'FAIL: split path changed photo coverage: %d (want 29)\n' \ + "$covered" >&2 + exit 1 + fi + test::teardown +} + test_generate_config_no_splash_keeps_index_redirect() { local config_file local fake_bin @@ -4360,7 +4454,6 @@ BASH render_show_header_bar \ render_source_url_html \ render_stats_page_html \ - render_thumbheight_html \ render_title_html | sort | paste -sd ' ' -) if [ "$output" != "present=$expected_present" ]; then @@ -6753,6 +6846,9 @@ main() { 'view redirects wrap when last page is full' \ test_render_view_redirects_wraps_when_last_page_full test::run_case \ + 'album grid cell total aligns to a multiple of 12' \ + test_album_grid_cells_align_to_multiple_of_12 + test::run_case \ '--generate SPLASH_PAGE=no keeps root index redirect' \ test_generate_config_no_splash_keeps_index_redirect test::run_case \ |
