Wall of People Groups: the poster print counts its photos while they load, and says how many could not load
The HD poster prints on its frame's 'load', which waits for every photo: up to 2,107 photos, 132 MB, which can take minutes on a slow connection, and a photo that fails prints as a grey box without a word. The print button now reads "Preparing the poster: n of 2,107 photos" while the frame loads, and if any photo could not load, a toast says how many print as grey boxes. The counting is src/printWatch.js (it never decides when to print; the frame's own 'load' does). It finishes at 'load', at the frame's 10-minute ceiling, or at once if the frame is gone (closed), so the busy button never outlives its frame. The busy state's fallback moves from 120 s to that ceiling, because the frame is now kept until it has loaded (printInPlace, exp-shepherd/print-floor). Headless, fake timers and a fake frame (_qa-cycle/exp-shepherd/check-wall-print-watch.mjs): progress climbs as photos load; 3 photos with no pixels are counted as failed; a load that never ends finishes once at the ceiling and not before; a frame removed at 60 s ends the watch within a tick; 4 planted defects (failures not counted, no ceiling, no progress, a removed frame holding the watch) each caught.
This commit is contained in:
parent
3a004068b7
commit
7e309d1777
2 changed files with 64 additions and 9 deletions
|
|
@ -8,9 +8,9 @@
|
|||
<span class="btn-label">{{ isLandscape ? $t('wall.toPortrait') : $t('wall.toLandscape') }}</span>
|
||||
<span class="current-mode">{{ isLandscape ? $t('wall.currentLandscape') : $t('wall.currentPortrait') }}</span>
|
||||
</button>
|
||||
<!-- Loading every card's HD photo can take a while: the button shows it is busy and cannot
|
||||
start a second load while the first is still running. -->
|
||||
<button @click="onPrint" class="control-btn" :class="{ busy: printing }" :disabled="printing" :aria-busy="printing ? 'true' : 'false'">{{ $t('wall.printPoster') }}</button>
|
||||
<!-- Loading every card's HD photo can take a while: the button shows it is busy, counts the
|
||||
photos as they arrive, and cannot start a second load while the first is still running. -->
|
||||
<button @click="onPrint" class="control-btn" :class="{ busy: printing }" :disabled="printing" :aria-busy="printing ? 'true' : 'false'">{{ printLabel }}</button>
|
||||
<button @click="resetZoom" class="control-btn">{{ $t('wall.resetZoom') }}</button>
|
||||
<!-- BLESS (default): name, photo and a blessing verse under every group, made to print.
|
||||
SOCIAL: the interactive card (pray, comments, share). -->
|
||||
|
|
@ -54,6 +54,7 @@
|
|||
import { WallCanvas, groupLink } from '../wallCanvas.js'
|
||||
import { buildBlessPosterHtml, screenPoster, PRESETS, DEFAULT_PRESET } from '../blessPoster.js'
|
||||
import { openPrintFrame } from '@map/composables/printInPlace.js'
|
||||
import { watchPrintFrame } from '../printWatch.js'
|
||||
import { useHostedHelp } from '@map/composables/useHostedHelp.js'
|
||||
// Wall of People Groups: a zoomable canvas wall (wallCanvas.js), Bless or Social cards, and the poster print.
|
||||
export default {
|
||||
|
|
@ -72,8 +73,9 @@ export default {
|
|||
// Each label is isolated left to right (U+2066 … U+2069): "5 × 8 ft · 3×" would otherwise be reordered
|
||||
// by an RTL page into "×ft · 3 8 × 5" (an <option> cannot hold a <bdi>).
|
||||
posterPresets: Object.entries(PRESETS).map(([id, p]) => ({ id, label: '\u2066' + p.label + (p.sheets > 1 ? ' · ' + p.sheets + '×' : '') + '\u2069' })),
|
||||
printing: false,
|
||||
toast: '', // a short status line over the wall (e.g. "Copied!"), cleared by flash() // the HD poster print is loading photos (drives the button's busy state)
|
||||
printing: false, // the HD poster print is loading photos (drives the button's busy state)
|
||||
printProgress: null, // { n, total, failed }: the print frame's photos, counted while they load
|
||||
toast: '', // a short status line over the wall (e.g. "Copied!"), cleared by flash()
|
||||
cardWidth: 50,
|
||||
cardHeight: 70,
|
||||
cardSpacing: 8, // Increased horizontal spacing (was 2)
|
||||
|
|
@ -99,6 +101,11 @@ export default {
|
|||
|
||||
loadError() {
|
||||
return this.$store?.error || null;
|
||||
},
|
||||
|
||||
printLabel() {
|
||||
const p = this.printing && this.printProgress
|
||||
return p ? this.$t('wall.printPreparing', { n: p.n.toLocaleString('en-US'), total: p.total.toLocaleString('en-US') }) : this.$t('wall.printPoster');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -161,10 +168,19 @@ export default {
|
|||
this.printing = true;
|
||||
const texts = this.posterTexts(groups.length);
|
||||
const win = openPrintFrame(buildBlessPosterHtml(groups, this.posterPreset, texts), { autoprint: true });
|
||||
// Busy until the photos have loaded and the print dialog is up (the frame prints on its own 'load').
|
||||
const done = () => { this.printing = false; };
|
||||
if (win) { try { win.addEventListener('load', () => setTimeout(done, 300), { once: true }); } catch (_) { done(); } setTimeout(done, 120000); }
|
||||
else done();
|
||||
// Busy until the photos have loaded and the print dialog is up (the frame prints on its own 'load',
|
||||
// which waits for every photo). Never silently incomplete: the button counts the photos as they
|
||||
// arrive (printWatch.js), and a photo that could not load is counted out loud, since it prints as
|
||||
// a grey box.
|
||||
const done = () => { this.printing = false; this.printProgress = null; };
|
||||
if (!win) { done(); return; }
|
||||
watchPrintFrame(win, {
|
||||
onProgress: (c) => { this.printProgress = c; },
|
||||
onDone: (c) => {
|
||||
if (c && c.failed) this.flash(this.$t('wall.printMissing', { n: c.failed.toLocaleString('en-US') }), 10000);
|
||||
setTimeout(done, 300);
|
||||
},
|
||||
});
|
||||
},
|
||||
// The wall in Bless mode IS the poster (WYSIWYG): the same sheets, cards, title, chart and footer that
|
||||
// print, from the same blessPoster layout. Re-planned when the groups, the size or the language change.
|
||||
|
|
|
|||
39
plugin-designs/wall-of-people-groups/src/printWatch.js
Normal file
39
plugin-designs/wall-of-people-groups/src/printWatch.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* printWatch.js — counts a print frame's photos while they load, and how many could not load.
|
||||
*
|
||||
* The frame prints on its own 'load' (library/composables/printInPlace.js), and 'load' waits for every
|
||||
* photo, so this never decides WHEN to print: it only reports. The HD poster loads up to 2,107 photos
|
||||
* (132 MB), which can take minutes on a slow connection, so the reader sees a count instead of a busy
|
||||
* button. A photo that could not load is complete but has no pixels, and prints as a grey box, so the
|
||||
* caller says how many rather than printing silently incomplete.
|
||||
*/
|
||||
|
||||
/** { n, total, failed } for the frame's images right now, or null when the frame cannot be read. */
|
||||
export function countImages(win) {
|
||||
let imgs = null
|
||||
try { imgs = win.document.images } catch (_) { return null }
|
||||
if (!imgs) return null
|
||||
let n = 0, failed = 0
|
||||
for (const im of imgs) if (im.complete) { n++; if (!im.naturalWidth) failed++ }
|
||||
return { n, total: imgs.length, failed }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls onProgress(count) now and every `every` ms while the frame loads, then onDone(count) exactly once:
|
||||
* at the frame's 'load', at `ceilingMs` for a load that never ends (the frame's own ceiling in
|
||||
* printInPlace.js), or with null as soon as the frame is gone (closed). Returns stop(), which ends the
|
||||
* watch without a count.
|
||||
*/
|
||||
export function watchPrintFrame(win, { onProgress = () => {}, onDone = () => {}, every = 500, ceilingMs = 600000 } = {}) {
|
||||
let iv = null, cap = null, over = false
|
||||
const finish = (c) => { if (over) return; over = true; clearInterval(iv); clearTimeout(cap); onDone(c) }
|
||||
// A frame removed before its 'load' (an older printInPlace dropped it 60 s after creation) never fires
|
||||
// 'load': end the watch then, rather than holding the busy button until the ceiling.
|
||||
const tick = () => { if (win.closed) return finish(null); const c = countImages(win); if (c && c.total) onProgress(c) }
|
||||
tick()
|
||||
if (over) return () => {}
|
||||
iv = setInterval(tick, every)
|
||||
cap = setTimeout(() => finish(countImages(win)), ceilingMs)
|
||||
try { win.addEventListener('load', () => finish(countImages(win)), { once: true }) } catch (_) { finish(null) }
|
||||
return () => finish(null)
|
||||
}
|
||||
Loading…
Reference in a new issue