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": "未知",