Versie 0.3.3

This commit is contained in:
Atreides Assets committed 2026-10-30 12:28:48 +01:00
1 parent 1cf6eeed29
commit 6e741a51fe
57 files changed
+1362 -2135

No files matched your search

+55 -93
View File
@@ -1,4 +1,4 @@
// Generated, do not edit. sha256:5dea810da6d84ed187a1154955f4194498f812771e3ddaa6b6dc7c547a305b8d
// Generated, do not edit. sha256:867f87b26d0b27fe51f4a0cb77d5196bad8cd8b58a6856168c9cad25f5776d99
import { t, localeTag } from '../i18n/index.js';
import { CONFIG } from '../../shared/config.js';
import { esc } from '../ui/escape.js';
@@ -8,7 +8,8 @@ import { refreshAttention } from '../ui/attention.js';
import { npubFromHex, shortKey, secretFromText, publicKeyOf, nsecFromSecret, newSecret } from '../nostr/keys.js';
import { keySigner, extensionSigner, nip98Header } from '../nostr/nip98.js';
import { qrSvg, invoiceQrText } from '../ui/qr.js';
import { CHECK, CHEVRON } from '../ui/icons.js';
import { CHECK, CHEVRON, IMAGE } from '../ui/icons.js';
import { setPageHint } from '../ui/page-hint.js';
import { splashMarkHtml } from '../ui/splash.js';
import { expanders } from '../ui/expand.js';
import { supportInfo, supportMessages, sendSupport, supportAttachment } from '../data/support.js';
@@ -21,24 +22,21 @@ function buyerOf(s) {
return { text: shortKey(npub ?? s.buyer, 10), title: npub ?? s.buyer ?? '' };
}
function lines(s) {
function standHtml(s) {
const out = [];
const line = (text, title = '') => out.push({ text, title });
if (s.why === 'develop' || s.why === 'forced-develop') line(t('license.cardDevelop'));
line(s.level === 'full' ? t('license.cardFull') : t('license.cardFree'));
if (s.installed) {
const buyer = buyerOf(s);
line(t('license.cardFacts', {
buyer: buyer.text, issued: s.issued ?? '',
from: s.installed.majorFrom,
const range = t('license.cardFacts', {
issued: s.issued ?? '', from: s.installed.majorFrom,
to: s.installed.majorTo === 'unlimited' ? t('license.unlimited') : s.installed.majorTo,
}), buyer.title);
if (!s.installedCovers) line(t('license.cardOutOfRange', { version: s.version }));
});
const [before, after] = t('license.cardFor', { npub: '§' }).split('§');
out.push(`<p class="t-body-sm">${esc(before)}<span class="badge badge-accent" title="${esc(`${buyer.title}\n${range}`)}">${esc(buyer.text)}</span>${esc(after)}</p>`);
} else {
out.push(`<p class="t-body-sm">${esc(t(s.level === 'full' ? 'license.cardFull' : 'license.cardFree'))}</p>`);
}
if (BROKEN.has(s.why)) line(`${t('license.cardProblem')} ${t(`license.why_${s.why}`)}.`);
if (s.level !== 'full') line(t('license.cardBuy'));
return out;
if (BROKEN.has(s.why)) out.push(`<p class="t-body-sm">${esc(`${t('license.cardProblem')} ${t(`license.why_${s.why}`)}.`)}</p>`);
return out.join('');
}
async function installAndReload(text, message) {
@@ -57,17 +55,16 @@ async function installAndReload(text, message) {
const INFO = ['Nostr', 'Why', 'SignIn', 'Zap', 'Privacy'];
const link = (url, html) => `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${html}</a>`;
const payHtml = (expand) => `<div class="lic-head"><div class="t-label">${say('license.payTitle')}</div></div>
<p class="t-body-sm">${say('license.payIntro')}</p>`
+ ['easy', 'other'].map(group => `<div class="lic-apps"><div class="t-caption">${say(`license.payGroup_${group}`)}</div>`
+ CONFIG.LICENSE.PAY_APPS.filter(a => a.group === group)
.map(a => `<p class="t-body-sm">${link(a.url, `<strong>${esc(a.name)}</strong>`)} ${say(`license.payApp_${a.id}`)}</p>`).join('')
+ '</div>').join('')
+ `<p class="t-caption">${say('license.payNote')}</p>
<details class="lic-more" ${expand.attr('nostr')}><summary class="t-body-sm">${say('license.nostrMore')}</summary>`
+ INFO.map(k => `<p class="t-body-sm"><strong>${say(`license.info${k}Head`)}</strong> ${say(`license.info${k}`)}</p>`).join('')
+ CONFIG.LICENSE.INFO_LINKS.map(l => `<p class="t-body-sm">${link(l.url, say(`license.link_${l.id}`))}</p>`).join('')
+ '</details>';
const legendRows = () => [
say('license.payIntro'),
...['easy', 'other'].flatMap(group => [{ heading: say(`license.payGroup_${group}`) },
...CONFIG.LICENSE.PAY_APPS.filter(a => a.group === group)
.map(a => `${link(a.url, `<strong>${esc(a.name)}</strong>`)} ${say(`license.payApp_${a.id}`)}`)]),
say('license.payNote'),
{ heading: say('license.nostrMore') },
...INFO.map(k => `<strong>${say(`license.info${k}Head`)}</strong> ${say(`license.info${k}`)}`),
...CONFIG.LICENSE.INFO_LINKS.map(l => link(l.url, say(`license.link_${l.id}`))),
];
let signer = null;
let newKey = null;
@@ -91,12 +88,12 @@ async function drawSignIn(box, s) {
const who = t(signer.kind === 'extension' ? 'license.signedInExtension' : 'license.signedIn', { npub: '§' });
const [before, after] = who.split('§');
const other = s.installed && s.buyer && s.buyer !== signer.pubkey;
box.innerHTML = `${head}
<p class="t-body-sm">${esc(before)}${npubText(signer.pubkey)}${esc(after)}</p>
box.innerHTML = `
<div class="flex-between gap-sm"><span class="t-body-sm">${esc(before)}${npubText(signer.pubkey)}${esc(after)}</span>
<button class="btn btn-ghost btn-sm" type="button" data-signout>${say('license.signOut')}</button></div>
${other ? `<p class="t-body-sm">${say('license.otherBuyer')}</p>` : ''}
<div data-licenses class="mt-sm"></div>
<p class="t-body-sm" data-signin-msg></p>
<div class="flex gap-sm"><button class="btn btn-secondary btn-sm" type="button" data-signout>${say('license.signOut')}</button></div>`;
<p class="t-body-sm" data-signin-msg></p>`;
box.querySelector('[data-signout]').onclick = () => { signer = null; drawSignIn(box, s); };
await drawLicenses(box.querySelector('[data-licenses]'), box.querySelector('[data-signin-msg]'), shop);
return;
@@ -147,7 +144,7 @@ async function drawSignIn(box, s) {
<button class="btn btn-secondary btn-sm" type="button" data-key>${say('license.signInKey')}</button>
<button class="btn btn-ghost btn-sm" type="button" data-new>${say('license.signInNew')}</button>
</div>
${hasExtension ? '' : `<p class="t-caption mt-sm">${say('license.signInExtensionNone')}</p>`}
${hasExtension ? '' : `<p class="t-caption t-muted lic-status">${say('license.signInExtensionNone')}</p>`}
<form class="mt-md" data-key-form hidden autocomplete="off">
<label class="form-label" for="lic-nsec">${say('license.signInKeyLabel')}</label>
<input class="input t-mono mt-sm" id="lic-nsec" type="password" autocomplete="off" spellcheck="false">
@@ -189,6 +186,8 @@ const AWAY = new Set(['marketService.unreachable', 'marketService.timeout', 'mar
async function drawBuy() {
const box = buyBox;
if (!box) return;
box.hidden = !signer && !purchase;
if (box.hidden) return;
const head = `<div class="lic-head"><div class="t-label">${say('license.buyTitle')}</div></div>`;
if (purchase) {
@@ -223,7 +222,6 @@ async function drawBuy() {
: sale ? say('license.buyClosed') : esc(problem ?? '');
box.innerHTML = `${head}
<p class="t-body-sm${problem ? ' t-neg' : ''}">${price}</p>
${sale?.open && !signer ? `<p class="t-body-sm">${say('license.buySignInFirst')}</p>` : ''}
<div class="flex gap-sm"><button class="btn btn-primary btn-sm" type="button" data-buy-go${sale?.open && signer ? '' : ' disabled'}>${say('license.buyButton')}</button></div>
<p class="t-body-sm mt-sm" data-buy-msg></p>`;
box.querySelector('[data-buy-go]').onclick = async (e) => {
@@ -305,10 +303,10 @@ async function drawSupport() {
<textarea class="input mt-sm" id="lic-support" rows="3" maxlength="${info.maxText}" placeholder="${esc(t('license.supportPlaceholder'))}">${esc(supportDraft)}</textarea>
<div data-support-file></div>
<div class="flex gap-sm mt-sm">
<button class="btn btn-secondary btn-sm" type="button" data-support-pick>${say('license.supportAttach')}</button>
<input type="file" accept="${esc(info.attachmentTypes.join(','))}" data-support-input hidden>
<button class="btn btn-primary btn-sm" type="button" data-support-send>${say('license.supportSend')}</button>
<span class="t-caption">${say('license.supportAttachHint', { mb })}</span>
<input type="file" accept="${esc(info.attachmentTypes.join(','))}" data-support-input hidden>
<button class="btn btn-ghost btn-sm lic-attach" type="button" data-support-pick
title="${esc(`${t('license.supportAttach')}: ${t('license.supportAttachHint', { mb })}`)}" aria-label="${esc(t('license.supportAttach'))}">${IMAGE}</button>
</div>
<p class="t-body-sm mt-sm" data-support-msg></p>
${nostr}`;
@@ -474,10 +472,12 @@ function download(name, text) {
const mark = (v) => {
if (v === true) return `<span class="lic-feat-yes" title="${esc(t('license.featIncluded'))}">${CHECK}</span>`;
if (v === false) return `<span class="lic-feat-no" title="${esc(t('license.featExcluded'))}">-</span>`;
if (v === 'partial') return `<span class="lic-feat-part" title="${esc(t('license.featPartialTitle'))}">${esc(t('license.featPartial'))}</span>`;
if (v === 'partial') return `<span class="lic-feat-part" title="${esc(t('license.featPartialTitle'))}">${CHECK}</span>`;
return `<span class="lic-feat-open" title="${esc(t('license.featUndecided'))}">?</span>`;
};
const MARKS = '.lic-feat-yes, .lic-feat-no, .lic-feat-part, .lic-feat-open';
const inFree = f => (f.has ? CONFIG.LICENSE.FREE.HAS[f.has] : f.free);
function groupFree(items) {
@@ -502,23 +502,21 @@ function featuresHtml(expand) {
<ul class="lic-feat-sub">${sub}</ul>
</details>`;
}).join('');
const open = CONFIG.LICENSE.FEATURES.some(f => inFree(f) === null);
return `
<div class="lic-hero">${splashMarkHtml()}<div class="t-label">${esc(t('license.featTitle'))}</div></div>
<div class="lic-feat">
<div class="lic-feat-row lic-feat-cols"><span></span><span>${esc(t('license.featFull'))}</span><span>${esc(t('license.featFree'))}</span></div>
${groups}
</div>
${open ? `<p class="t-caption">${esc(t('license.featFreeOpen'))}</p>` : ''}`;
</div>`;
}
function placeShot(shot, row) {
function placeShot(shot, anchor) {
const gap = 8, pad = 8;
const r = row.getBoundingClientRect();
const r = anchor.getBoundingClientRect();
const w = shot.offsetWidth, h = shot.offsetHeight;
let top = r.top - gap - h;
if (top < pad) top = r.bottom + gap;
const left = Math.min(Math.max(pad, r.left + 24), window.innerWidth - pad - w);
const left = Math.min(Math.max(pad, r.left + r.width / 2 - w / 2), window.innerWidth - pad - w);
shot.style.left = `${left}px`;
shot.style.top = `${Math.min(Math.max(pad, top), window.innerHeight - pad - h)}px`;
}
@@ -531,12 +529,12 @@ function bindFeatures(root) {
document.body.appendChild(shot);
let current = null;
let on = null;
const show = (row) => {
if (row === on && !shot.hidden) return;
on = row;
const show = (row, anchor = row) => {
if (anchor === on && !shot.hidden) return;
on = anchor;
root.querySelectorAll('[data-feature]').forEach(r => r.classList.toggle('is-on', r === row));
shot.hidden = false;
placeShot(shot, row);
placeShot(shot, anchor);
const id = row.dataset.feature;
if (id === current) return;
current = id;
@@ -555,8 +553,9 @@ function bindFeatures(root) {
};
const over = (e) => {
if (!root.isConnected) { stop(); return; }
const row = e.target.closest?.('[data-feature]');
if (row && root.contains(row)) show(row); else hide();
const mark = e.target.closest?.(MARKS);
const row = mark?.closest('[data-feature]');
if (row && root.contains(row)) show(row, mark); else hide();
};
const stop = () => {
shot.remove();
@@ -574,35 +573,27 @@ function bindFeatures(root) {
export async function render(el) {
const s = licenseSnapshot() ?? await loadLicense();
const expand = await expanders('license', {
nostr: false, ...Object.fromEntries(CONFIG.LICENSE.FEATURE_GROUPS.map(g => [groupKey(g), false])),
});
const expand = await expanders('license', Object.fromEntries(CONFIG.LICENSE.FEATURE_GROUPS.map(g => [groupKey(g), false])));
el.innerHTML = `
<div class="lic-layout">
<div class="lic-main">
<div class="card lic-card" data-features>${featuresHtml(expand)}</div>
</div>
<div class="lic-side">
<div class="card lic-card">
<div class="lic-head">
<div class="t-label">${esc(t('license.cardTitle'))}</div>
<span class="badge badge-neutral">${esc(t('license.cardVersion', { version: s.version }))}</span>
<span class="badge badge-accent">${esc(t('license.cardVersion', { version: s.version }))}</span>
</div>
${lines(s).map(r => `<p class="t-body-sm"${r.title ? ` title="${esc(r.title)}"` : ''}>${esc(r.text)}</p>`).join('')}
<div class="lic-drop" id="lic-drop" tabindex="0" role="button">
<span class="t-body-sm">${esc(t('license.dropHere'))}</span>
<span class="t-caption">${esc(t('license.dropOr'))}</span>
<input type="file" id="lic-file" accept=".json,application/json" hidden>
</div>
<span id="lic-msg" class="t-body-sm"></span>
${standHtml(s)}
<section class="lic-section" data-signin></section>
<section class="lic-section" data-buy></section>
<section class="lic-section" data-buy hidden></section>
</div>
<div class="card lic-card">${payHtml(expand)}</div>
</div>
<div class="lic-main">
<div class="card lic-card" data-features>${featuresHtml(expand)}</div>
<div class="card lic-card lic-chat-card" data-support hidden></div>
</div>
</div>`;
setPageHint(el, { title: t('license.payTitle'), rows: legendRows() });
expand.bind(el);
bindFeatures(el.querySelector('[data-features]'));
buyBox = el.querySelector('[data-buy]');
@@ -610,33 +601,4 @@ export async function render(el) {
drawSignIn(el.querySelector('[data-signin]'), s);
markLicenseSeen().then(() => refreshAttention()).catch(() => { });
const message = el.querySelector('#lic-msg');
const install = (text) => installAndReload(text, message);
const MAX = 64 * 1024;
const zone = el.querySelector('#lic-drop');
const picker = el.querySelector('#lic-file');
async function fromFile(file) {
if (!file) return;
if (file.size > MAX) {
message.className = 't-body-sm t-neg';
message.textContent = t('license.dropTooBig');
return;
}
await install(await file.text());
}
zone.onclick = () => picker.click();
zone.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); picker.click(); } };
picker.onchange = () => fromFile(picker.files?.[0]);
zone.ondragover = (e) => { e.preventDefault(); zone.classList.add('is-over'); };
zone.ondragleave = () => zone.classList.remove('is-over');
zone.ondrop = (e) => {
e.preventDefault();
zone.classList.remove('is-over');
fromFile(e.dataTransfer?.files?.[0]);
};
}