Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions ui/test/backup-haiku.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
39 changes: 25 additions & 14 deletions ui/widgets/backup/backup.html
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion ui/widgets/backup/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion ui/widgets/backup/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion ui/widgets/backup/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion ui/widgets/backup/i18n/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "نامشخص",
Expand Down
2 changes: 1 addition & 1 deletion ui/widgets/backup/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion ui/widgets/backup/i18n/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "未知",
Expand Down
Loading