From af65fd1232a778e9861d1ff8727e43aa594e1d8c Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:45:10 -0400 Subject: [PATCH] Show Backup haikus only when every line fits the card Pick the first shuffled haiku whose lines all fit the card width and height instead of replacing lines that do not fit with plain rows. Require at least two lines. Add nine short haikus in every locale and split the one-line haiku. --- ui/test/backup-haiku.test.mjs | 32 +++++++++++++++++++++++ ui/widgets/backup/backup.html | 39 ++++++++++++++++++----------- ui/widgets/backup/i18n/de.json | 2 +- ui/widgets/backup/i18n/en.json | 2 +- ui/widgets/backup/i18n/es.json | 2 +- ui/widgets/backup/i18n/fa.json | 2 +- ui/widgets/backup/i18n/fr.json | 2 +- ui/widgets/backup/i18n/zh-Hans.json | 2 +- 8 files changed, 63 insertions(+), 20 deletions(-) create mode 100644 ui/test/backup-haiku.test.mjs diff --git a/ui/test/backup-haiku.test.mjs b/ui/test/backup-haiku.test.mjs new file mode 100644 index 00000000..ecc6387a --- /dev/null +++ b/ui/test/backup-haiku.test.mjs @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { test } from 'node:test'; + +const page = fs.readFileSync(new URL('../widgets/backup/backup.html', import.meta.url), 'utf8'); +const source = /function pickHaiku\([\s\S]*?\n\}/.exec(page)?.[0]; +const pickHaiku = new Function(`${source}; return pickHaiku;`)(); + +const charW = line => line.length * 6; +const pick = (haikus, usableW, H = 200) => pickHaiku(haikus, charW, usableW, H, 2, 8, 11, 16); + +test('a haiku with one line too wide is skipped whole', () => { + const tooWide = ['Hope is not', 'a strategy.']; + const fits = ['Back it', 'up.']; + assert.deepEqual(pick([tooWide, fits], 70), fits); +}); + +test('the first haiku that fits is shown', () => { + const first = ['Trust, but', 'verify.']; + assert.deepEqual(pick([first, ['Backup or', 'regret.']], 100), first); +}); + +test('no haiku is shown when none fits', () => { + assert.equal(pick([['Hope is not', 'a strategy.']], 40), null); + assert.equal(pick(null, 100), null); +}); + +test('a haiku taller than the card is skipped', () => { + const tall = ['Nightly run.', 'Morning calm.', 'Data lives.']; + const short = ['Trust, but', 'verify.']; + assert.deepEqual(pick([tall, short], 100, 40), short); +}); diff --git a/ui/widgets/backup/backup.html b/ui/widgets/backup/backup.html index d853a244..4fd59c3f 100644 --- a/ui/widgets/backup/backup.html +++ b/ui/widgets/backup/backup.html @@ -180,8 +180,10 @@ const HAIKUS = (wt('ui.haikus', 'Trust, but|verify.\nBackup or|regret.\nNothing lasts|forever.\nNightly run.|Morning calm.|Data lives.\n' + 'Backup first.|Delete later.|Sleep better.\nLogs green.|Coffee warm.|All good.\nHope is not|a strategy.\n' - + 'Keep it safe.\nExpect the|unexpected.\nWorries don\'t.|Good backups|do.\nPrepare early.|Panic rarely.|Smile.') -).split('\n').map(line => line.split('|')) + + 'Keep it|safe.\nExpect the|unexpected.\nWorries don\'t.|Good backups|do.\nPrepare early.|Panic rarely.|Smile.\n' + + 'RAID is not|a backup.\nLuck is not|a plan.\nBack it|up.\nCopy.|Verify.\nTest the|restore.\n' + + 'Save now.|Thank later.\nBits safe.|Mind calm.\n3-2-1.|Done.\nSleep|easy.') +).split('\n').map(line => line.split('|')).filter(lines => lines.length >= 2); /* Safe rounded rect — avoids ctx.roundRect which has iframe bugs in WebKit */ function rrect(ctx, x, y, w, h, r) { @@ -200,8 +202,15 @@ ctx.closePath(); } +/* Text is never clipped. A haiku shows whole or not at all. */ +function pickHaiku(haikus, textW, usableW, H, top, rowStep, textH, textStep) { + return (haikus || []).find(lines => + top + rowStep + (lines.length - 1) * textStep + textH <= H + && lines.every(line => textW(line) + 7 <= usableW)) || null; +} + /* ── Canvas placeholder drawing ── */ -function drawPh(canvas, haiku, isFront, seed) { +function drawPh(canvas, haikus, isFront, seed) { const dpr = Math.min(window.devicePixelRatio || 1, 2); /* cap DPR at 2 */ const W = canvas.offsetWidth; const H = canvas.offsetHeight; @@ -260,6 +269,10 @@ const TEXT_H = 11; /* font size for haiku lines */ const TEXT_STEP = TEXT_H + GAP; + ctx.font = `700 ${px(TEXT_H)}px -apple-system, BlinkMacSystemFont, system-ui, sans-serif`; + const textW = line => ctx.measureText(line).width / k; + const haiku = pickHaiku(haikus, textW, usableW, H, 2, STEP, TEXT_H, TEXT_STEP); + /* Build row plan */ const plan = []; if (haiku && haiku.length > 0) { @@ -287,21 +300,19 @@ for (const entry of plan) { if (y > H) break; - ctx.font = `700 ${px(TEXT_H)}px -apple-system, BlinkMacSystemFont, system-ui, sans-serif`; - const textW = entry.type === 'text' ? ctx.measureText(entry.line).width / k : 0; - /* A line wider than the card is drawn as a plain row. Text is never clipped. */ - if (entry.type === 'text' && textW + 7 <= usableW) { + if (entry.type === 'text') { + const lineW = textW(entry.line); ctx.textBaseline = 'top'; ctx.textAlign = 'left'; /* Small left dash */ - const ldW = Math.min(4 + Math.floor(rng() * 10), usableW - textW - 3); + const ldW = Math.min(4 + Math.floor(rng() * 10), usableW - lineW - 3); ctx.fillStyle = dashC; bar(PAD, y + TEXT_H/2 - 1, ldW, 2, 1); /* Text */ ctx.fillStyle = textC; ctx.fillText(entry.line, px(PAD + ldW + 3), px(y)); - const rdX = PAD + ldW + 3 + textW + 3; - const rdW = Math.max(4, usableW - (ldW + 3 + textW + 3)); + const rdX = PAD + ldW + 3 + lineW + 3; + const rdW = Math.max(4, usableW - (ldW + 3 + lineW + 3)); ctx.fillStyle = dashC; if (rdW > 0 && rdX + rdW <= W - PAD) bar(rdX, y + TEXT_H/2 - 1, rdW, 2, 1); y += TEXT_STEP; @@ -336,13 +347,13 @@ /* Draw once the canvas has a real size. A card whose height comes from aspect-ratio layout has no size at draw time. */ -function observeDraw(canvas, haiku, isFront, seed) { +function observeDraw(canvas, haikus, isFront, seed) { let lastW = 0, lastH = 0; const ro = new ResizeObserver(() => { const w = canvas.offsetWidth, h = canvas.offsetHeight; if (!w || !h || (w === lastW && h === lastH)) return; lastW = w; lastH = h; - drawPh(canvas, haiku, isFront, seed); + drawPh(canvas, haikus, isFront, seed); }); ro.observe(canvas); } @@ -365,11 +376,11 @@ function assignHaikus(slots) { /* Shuffle pool randomly — different every reload */ const pool = [...HAIKUS].sort(() => Math.random()-0.5); - if (size==='small') return slots.map(j => j ? pool[0] : null); + if (size==='small') return slots.map(j => j ? pool : null); const activeIdxs = slots.map((j,i)=>j?i:-1).filter(i=>i>=0); if (!activeIdxs.length) return slots.map(()=>null); const chosen = activeIdxs[Math.floor(Math.random()*activeIdxs.length)]; - return slots.map((j,i) => (i===chosen&&j) ? pool[0] : null); + return slots.map((j,i) => (i===chosen&&j) ? pool : null); } async function boot() { diff --git a/ui/widgets/backup/i18n/de.json b/ui/widgets/backup/i18n/de.json index a01e5d4c..f734a882 100644 --- a/ui/widgets/backup/i18n/de.json +++ b/ui/widgets/backup/i18n/de.json @@ -27,7 +27,7 @@ "slots.kopiaHref.hint": "Die Adresse, unter der dein Browser diesen Dienst öffnet. Leer lassen, um die Adresse oben zu verwenden, sofern dein Browser sie erreicht.", "slots.customName.label": "Anzeigename", "slots.customName.placeholder": "Wird auf der Karte angezeigt", - "ui.haikus": "Vertraue, aber|prüfe nach.\nSichern oder|bereuen.\nNichts hält|für immer.\nNachtlauf.|Ruhiger Morgen.|Daten leben.\nErst sichern.|Dann löschen.|Besser schlafen.\nLogs grün.|Kaffee warm.|Alles gut.\nHoffnung ist|keine Strategie.\nGut aufbewahren.\nRechne mit|dem Unerwarteten.\nSorgen helfen nicht.|Gute Backups|schon.\nFrüh vorbereiten.|Selten hadern.|Lächeln.", + "ui.haikus": "Vertraue, aber|prüfe nach.\nSichern oder|bereuen.\nNichts hält|für immer.\nNachtlauf.|Ruhiger Morgen.|Daten leben.\nErst sichern.|Dann löschen.|Besser schlafen.\nLogs grün.|Kaffee warm.|Alles gut.\nHoffnung ist|keine Strategie.\nGut|aufbewahren.\nRechne mit|dem Unerwarteten.\nSorgen helfen nicht.|Gute Backups|schon.\nFrüh vorbereiten.|Selten hadern.|Lächeln.\nRAID ist|kein Backup.\nGlück ist|kein Plan.\nEinfach|sichern.\nKopieren.|Prüfen.\nWiederherstellung|testen.\nJetzt sichern.|Später danken.\nBits sicher.|Kopf frei.\n3-2-1.|Fertig.\nRuhig|schlafen.", "ui.backupStatus": "Backup-Status", "ui.running": "läuft", "ui.unknown": "unbekannt", diff --git a/ui/widgets/backup/i18n/en.json b/ui/widgets/backup/i18n/en.json index d57fadb0..3455ad9f 100644 --- a/ui/widgets/backup/i18n/en.json +++ b/ui/widgets/backup/i18n/en.json @@ -27,7 +27,7 @@ "slots.kopiaHref.hint": "The address your browser uses for this service. Leave blank to use the one above, if your browser can reach it.", "slots.customName.label": "Display Name", "slots.customName.placeholder": "Shown on the card", - "ui.haikus": "Trust, but|verify.\nBackup or|regret.\nNothing lasts|forever.\nNightly run.|Morning calm.|Data lives.\nBackup first.|Delete later.|Sleep better.\nLogs green.|Coffee warm.|All good.\nHope is not|a strategy.\nKeep it safe.\nExpect the|unexpected.\nWorries don't.|Good backups|do.\nPrepare early.|Panic rarely.|Smile.", + "ui.haikus": "Trust, but|verify.\nBackup or|regret.\nNothing lasts|forever.\nNightly run.|Morning calm.|Data lives.\nBackup first.|Delete later.|Sleep better.\nLogs green.|Coffee warm.|All good.\nHope is not|a strategy.\nKeep it|safe.\nExpect the|unexpected.\nWorries don't.|Good backups|do.\nPrepare early.|Panic rarely.|Smile.\nRAID is not|a backup.\nLuck is not|a plan.\nBack it|up.\nCopy.|Verify.\nTest the|restore.\nSave now.|Thank later.\nBits safe.|Mind calm.\n3-2-1.|Done.\nSleep|easy.", "ui.backupStatus": "Backup status", "ui.running": "running", "ui.unknown": "unknown", diff --git a/ui/widgets/backup/i18n/es.json b/ui/widgets/backup/i18n/es.json index 5fb64fbb..d0b32f7a 100644 --- a/ui/widgets/backup/i18n/es.json +++ b/ui/widgets/backup/i18n/es.json @@ -27,7 +27,7 @@ "slots.kopiaHref.hint": "La dirección con la que tu navegador abre este servicio. Déjalo en blanco para usar la de arriba, si tu navegador puede acceder a ella.", "slots.customName.label": "Nombre visible", "slots.customName.placeholder": "Se muestra en la tarjeta", - "ui.haikus": "Confía, pero|verifica.\nHaz copias|o lo lamentarás.\nNada dura|para siempre.\nCopia nocturna.|Despertar tranquilo.|Los datos viven.\nPrimero copia.|Luego borra.|Duerme mejor.\nRegistros en verde.|Café caliente.|Todo bien.\nLa esperanza no|es una estrategia.\nGuárdalo a salvo.\nEspera lo|inesperado.\nPreocuparse no sirve.|Las buenas copias|sí.\nPrepárate antes.|Pánico, poco.|Sonríe.", + "ui.haikus": "Confía, pero|verifica.\nHaz copias|o lo lamentarás.\nNada dura|para siempre.\nCopia nocturna.|Despertar tranquilo.|Los datos viven.\nPrimero copia.|Luego borra.|Duerme mejor.\nRegistros en verde.|Café caliente.|Todo bien.\nLa esperanza no|es una estrategia.\nGuárdalo|a salvo.\nEspera lo|inesperado.\nPreocuparse no sirve.|Las buenas copias|sí.\nPrepárate antes.|Pánico, poco.|Sonríe.\nRAID no es|una copia.\nLa suerte no|es un plan.\nHaz una|copia.\nCopia.|Verifica.\nPrueba la|restauración.\nGuarda ahora.|Agradece luego.\nBits a salvo.|Mente en calma.\n3-2-1.|Listo.\nDuerme|tranquilo.", "ui.backupStatus": "Estado de la copia", "ui.running": "en curso", "ui.unknown": "desconocido", diff --git a/ui/widgets/backup/i18n/fa.json b/ui/widgets/backup/i18n/fa.json index 1d9fb8a3..d9e8fac8 100644 --- a/ui/widgets/backup/i18n/fa.json +++ b/ui/widgets/backup/i18n/fa.json @@ -27,7 +27,7 @@ "slots.kopiaHref.hint": "آدرسی که مرورگرتان برای این سرویس استفاده می‌کند. اگر مرورگرتان به آدرس بالا دسترسی دارد، خالی بگذارید.", "slots.customName.label": "نام نمایشی", "slots.customName.placeholder": "روی کارت نمایش داده می‌شود", - "ui.haikus": "اعتماد کن،|اما بررسی هم بکن.\nپشتیبان بگیر|یا پشیمان شو.\nهیچ‌چیز|همیشگی نیست.\nاجرای شبانه.|آرامش صبح.|داده زنده است.\nاول پشتیبان.|بعد حذف.|بهتر بخواب.\nگزارش‌ها سبز.|قهوه گرم.|همه‌چیز خوب.\nامید|راهبرد نیست.\nامنش نگه دار.\nمنتظر|غیرمنتظره‌ها باش.\nنگرانی کاری نمی‌کند.|پشتیبان خوب|می‌کند.\nزود آماده شو.|کمتر بترس.|لبخند بزن.", + "ui.haikus": "اعتماد کن،|اما بررسی هم بکن.\nپشتیبان بگیر|یا پشیمان شو.\nهیچ‌چیز|همیشگی نیست.\nاجرای شبانه.|آرامش صبح.|داده زنده است.\nاول پشتیبان.|بعد حذف.|بهتر بخواب.\nگزارش‌ها سبز.|قهوه گرم.|همه‌چیز خوب.\nامید|راهبرد نیست.\nامنش|نگه دار.\nمنتظر|غیرمنتظره‌ها باش.\nنگرانی کاری نمی‌کند.|پشتیبان خوب|می‌کند.\nزود آماده شو.|کمتر بترس.|لبخند بزن.\nRAID|پشتیبان نیست.\nشانس|برنامه نیست.\nپشتیبان|بگیر.\nکپی کن.|بررسی کن.\nبازیابی را|تست کن.\nالان ذخیره کن.|بعدا تشکر کن.\nبیت‌ها امن.|ذهن آرام.\n۳-۲-۱.|تمام.\nآسوده|بخواب.", "ui.backupStatus": "وضعیت پشتیبان‌گیری", "ui.running": "در حال اجرا", "ui.unknown": "نامشخص", diff --git a/ui/widgets/backup/i18n/fr.json b/ui/widgets/backup/i18n/fr.json index cf08f1dd..4dd5df54 100644 --- a/ui/widgets/backup/i18n/fr.json +++ b/ui/widgets/backup/i18n/fr.json @@ -27,7 +27,7 @@ "slots.kopiaHref.hint": "L’adresse que votre navigateur utilise pour ce service. Laissez vide pour utiliser celle ci-dessus, si votre navigateur peut l’atteindre.", "slots.customName.label": "Nom affiché", "slots.customName.placeholder": "Affiché sur la carte", - "ui.haikus": "Faire confiance,|mais vérifier.\nSauvegarder|ou regretter.\nRien ne dure|toujours.\nExécution nocturne.|Matin tranquille.|Les données vivent.\nSauvegarder d’abord.|Supprimer ensuite.|Mieux dormir.\nJournaux au vert.|Café chaud.|Tout va bien.\nL’espoir n’est|pas une stratégie.\nGardez-le en sûreté.\nAttendez-vous|à l’imprévu.\nS’inquiéter n’aide pas.|Les bonnes sauvegardes|si.\nPréparez-vous tôt.|Paniquez peu.|Souriez.", + "ui.haikus": "Faire confiance,|mais vérifier.\nSauvegarder|ou regretter.\nRien ne dure|toujours.\nExécution nocturne.|Matin tranquille.|Les données vivent.\nSauvegarder d’abord.|Supprimer ensuite.|Mieux dormir.\nJournaux au vert.|Café chaud.|Tout va bien.\nL’espoir n’est|pas une stratégie.\nGardez-le|en sûreté.\nAttendez-vous|à l’imprévu.\nS’inquiéter n’aide pas.|Les bonnes sauvegardes|si.\nPréparez-vous tôt.|Paniquez peu.|Souriez.\nLe RAID n’est|pas une copie.\nLa chance n’est|pas un plan.\nSauvegardez|tout.\nCopier.|Vérifier.\nTestez la|restauration.\nSauvez tôt.|Merci plus tard.\nBits à l’abri.|Esprit serein.\n3-2-1.|Terminé.\nDormez|tranquille.", "ui.backupStatus": "État de la sauvegarde", "ui.running": "en cours", "ui.unknown": "inconnu", diff --git a/ui/widgets/backup/i18n/zh-Hans.json b/ui/widgets/backup/i18n/zh-Hans.json index b8b2127c..ef1825a8 100644 --- a/ui/widgets/backup/i18n/zh-Hans.json +++ b/ui/widgets/backup/i18n/zh-Hans.json @@ -27,7 +27,7 @@ "slots.kopiaHref.hint": "浏览器访问此服务所用的地址。留空则使用上方地址(前提是浏览器能访问它)。", "slots.customName.label": "显示名称", "slots.customName.placeholder": "显示在卡片上", - "ui.haikus": "信任,|但要验证。\n要么备份,|要么后悔。\n没有什么|能永远留存。\n夜里运行。|清晨安心。|数据仍在。\n先备份。|再删除。|睡得更香。\n日志全绿。|咖啡尚温。|一切安好。\n指望侥幸|算不上策略。\n妥善保存。\n有备|无患。\n担心没有用。|好的备份|才有用。\n及早准备。|少些惊慌。|保持微笑。", + "ui.haikus": "信任,|但要验证。\n要么备份,|要么后悔。\n没有什么|能永远留存。\n夜里运行。|清晨安心。|数据仍在。\n先备份。|再删除。|睡得更香。\n日志全绿。|咖啡尚温。|一切安好。\n指望侥幸|算不上策略。\n妥善|保存。\n有备|无患。\n担心没有用。|好的备份|才有用。\n及早准备。|少些惊慌。|保持微笑。\nRAID|不是备份。\n运气|不是计划。\n记得|备份。\n复制。|验证。\n测试|恢复。\n现在备份。|以后感谢。\n数据安全。|内心平静。\n3-2-1。|完成。\n安心|入睡。", "ui.backupStatus": "备份状态", "ui.running": "运行中", "ui.unknown": "未知",