feat: download all images of a share link as one ZIP
Twenty pictures meant twenty taps. Now a single button above the grid, a plain GET link so it works without JavaScript, on a page whose recipients are mostly on a phone. Written to stream rather than buffer. The existing /api/items/zip loads every object into memory and appends them all before the consumer attaches; for a share of full-resolution posters that is gigabytes at once against a 3g limit. This route reads one file at a time and waits for archiver's `entry` event before reading the next. Measured: 440 MB of images, 26 s, RSS peaked at 196 MB against a 120 MB baseline, and the archive verifies. Guards, all checked: download-locked links return 403 and the button does not render; a passphrase-protected link needs the proof cookie (401 without); revoked 410, unknown 404; five packages per link and address per ten minutes; 200 files and 500 MB, past which the archive closes with a HINWEIS.txt saying what happened and where to get the rest. Counted as one download. Name collisions inside the archive are numbered, since duplicates only surface at extraction time where the second silently overwrites the first. Archive naming pulled into testable helpers with 7 tests (104 total). Also fixes a bug the screenshot caught: `.bild a` matched the per-image download button inside figcaption as well as the image link, painting it paper-coloured with near-white text. It had been effectively invisible since the page was written; I had looked at it twice without seeing it. Scoped to `.bild > a`. Contrast measured from real pixels afterwards: 7.73 on both buttons, 5.08 on the muted text, all above AA. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
This commit is contained in:
4 files changed
+168
-2
No files matched your search
@@ -76,10 +76,20 @@ const meldung: Record<string, { h: string; t: string }> = {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{share.allow_download && bilder.length > 1 && (
|
||||
<div class="alle">
|
||||
<a class="knopf gross" href={`/api/s/${slug}/zip`}>
|
||||
Alle {bilder.length} Bilder herunterladen
|
||||
</a>
|
||||
<span class="alle-fein">als ZIP-Datei, in voller Auflösung</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{share.allow_download ? (
|
||||
<p class="hinweis">
|
||||
<b>Herunterladen</b> speichert die Datei in voller Auflösung.
|
||||
Am Handy geht es auch so: lange auf ein Bild drücken und speichern.
|
||||
{bilder.length > 1 && <> Oder alle auf einmal, oben.</>}
|
||||
</p>
|
||||
) : (
|
||||
<p class="hinweis">
|
||||
@@ -151,12 +161,20 @@ const meldung: Record<string, { h: string; t: string }> = {
|
||||
.raster { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; }
|
||||
.raster.einzeln { grid-template-columns: minmax(0, 1fr); max-width: 720px; }
|
||||
.bild { margin: 0; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
|
||||
.bild a { display: block; line-height: 0; background: var(--paper); }
|
||||
/* Nur der **direkte** Bildlink, nicht jeder Link in der Figur: `.bild a`
|
||||
traf auch den Herunterladen-Knopf in der Unterzeile und übermalte ihn mit
|
||||
Papierfarbe — heller Text auf hellem Grund, praktisch unsichtbar. */
|
||||
.bild > a { display: block; line-height: 0; background: var(--paper); }
|
||||
.bild img { width: 100%; height: auto; display: block; }
|
||||
figcaption { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; }
|
||||
.mass { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; color: var(--soft); }
|
||||
.knopf { background: var(--accent); color: #fff; text-decoration: none; font-size: 13px; font-weight: 600;
|
||||
.knopf { background: var(--accent); color: var(--card); text-decoration: none; font-size: 13px; font-weight: 600;
|
||||
padding: 8px 14px; border-radius: 3px; white-space: nowrap; }
|
||||
/* Der Sammel-Download steht über den Bildern: Bei zwanzig Kacheln findet
|
||||
ihn unten niemand, und einzeln tippen will man dann erst recht nicht. */
|
||||
.alle { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 0 0 18px; }
|
||||
.knopf.gross { font-size: 15px; padding: 13px 22px; border-radius: var(--radius-sm); }
|
||||
.alle-fein { font-size: 12.5px; color: var(--soft); }
|
||||
.fein { font-size: 12px; color: var(--soft); }
|
||||
.leer { color: var(--soft); }
|
||||
/* Steht über den Bildern: Unter zwanzig Kacheln würde ihn niemand lesen. */
|
||||
|
||||
Reference in new issue
Block a user