wayBetter for X 1.0.2

This commit is contained in:
CRTkafa 2026-10-02 02:54:04 +07:00
parent 785c8cc72c
commit 435f05bd3e
212 changed files with 10208 additions and 2345 deletions

View file

@ -49,7 +49,7 @@ const fileName = (item: Item) => (item.kind === 'plugin' ? item.file! : decodeUR
/** What each permission is called and means, in plain words (docs/PLUGINS.md → Permissions): `site.gen.permission.<id>.label|plain`. */
const PERMISSIONS = new Set(['storage', 'ui', 'clipboard', 'account', 'users', 'commands', 'network', 'network:none']);
const permissionKey = (p: string) => `site.gen.permission.${p === 'network:none' ? 'networkNone' : p}`;
const CATEGORIES = new Set(['reading', 'writing', 'filters', 'focus', 'tools', 'dark', 'light', 'language']);
const CATEGORIES = new Set(['reading', 'writing', 'filters', 'focus', 'tools', 'dark', 'light', 'language', 'icons']);
/**
* A description for display. A plugin header keeps at most 300 characters, so a long one reaches
* the catalog cut mid-word: end it at the last whole word instead.
@ -271,6 +271,7 @@ function itemHtml(page: Page, item: Item): string {
[t('site.gen.item.file'), `<code>${esc(file)}</code>`],
[t('site.gen.item.size'), `${kb(item.size)} <span class="it-dim">${esc(t('site.gen.item.bytes', { count: item.size.toLocaleString('en-US') }))}</span>`],
...(updated ? [[t('site.gen.item.updated'), `<time datetime="${updated}">${esc(dateLabel(updated, lang))}</time>`] as [string, string]] : []),
...(item.kind === 'icons' ? [[t('site.gen.item.dataOnly'), esc(t('site.gen.item.dataOnlyIcons'))] as [string, string]] : []),
...(item.kind === 'strings' ? [
[t('site.gen.item.language'), `<span lang="${esc(item.language ?? '')}">${esc(item.language ?? '')}</span>`],
...(coverageOf(item) ? [[t('site.gen.item.coverage'), esc(t('site.gen.item.coverageValue', coverageOf(item)!))] as [string, string]] : []),
@ -302,7 +303,7 @@ function itemHtml(page: Page, item: Item): string {
return `<section class="it-top">
<div class="it-glow" aria-hidden="true"${item.colors ? ` style="--it-accent:${esc(item.colors.accent)}"` : ''}></div>
<nav class="it-crumbs wrap" aria-label="${esc(t('site.gen.breadcrumb'))}">
<ol><li><a href="${rel}store/">${esc(t('site.gen.store'))}</a></li><li><a href="${rel}store/?kind=${item.kind}">${esc(t(item.kind === 'theme' ? 'site.gen.themes' : item.kind === 'strings' ? 'site.gen.strings' : 'site.gen.plugins'))}</a></li><li aria-current="page"${own(page)}>${esc(item.name)}</li></ol>
<ol><li><a href="${rel}store/">${esc(t('site.gen.store'))}</a></li><li><a href="${rel}store/?kind=${item.kind}">${esc(t(item.kind === 'theme' ? 'site.gen.themes' : item.kind === 'strings' ? 'site.gen.strings' : item.kind === 'icons' ? 'site.gen.icons' : 'site.gen.plugins'))}</a></li><li aria-current="page"${own(page)}>${esc(item.name)}</li></ol>
</nav>
<div class="wrap it-grid">
${mediaHtml(page, item)}
@ -314,7 +315,7 @@ function itemHtml(page: Page, item: Item): string {
${item.kind === 'plugin' ? `<ul class="gal-perms" aria-label="${esc(t('site.gen.permissions'))}">${permissionChips(page, item.permissions)}</ul>` : swatches(item)}
<div class="it-actions" data-install-host>
<a class="btn btn-brush" ${installAttrs(page, item)}>${esc(t('site.gen.item.install'))}</a>
<a class="btn btn-ghost" href="${esc(fileHref)}" download="${esc(file)}" data-download>${esc(t('site.gen.item.download', { extension: item.kind === 'theme' ? '.json' : item.kind === 'strings' ? '.strings.json' : '.plugin.js' }))}</a>
<a class="btn btn-ghost" href="${esc(fileHref)}" download="${esc(file)}" data-download>${esc(t('site.gen.item.download', { extension: item.kind === 'theme' ? '.json' : item.kind === 'strings' ? '.strings.json' : item.kind === 'icons' ? '.icons.json' : '.plugin.js' }))}</a>
<a class="btn btn-ghost it-share" href="${esc(address)}" data-share data-share-title="${esc(`${item.name} · wayBetter Store`)}">${esc(t('site.gen.item.share'))}</a>
</div>
<p class="it-note">${app.oneClickSince ? th('site.gen.item.note', { app: app.name, version: app.oneClickSince }, how) : th('site.gen.item.noteAny', { app: app.name }, how)}</p>
@ -328,7 +329,7 @@ function itemHtml(page: Page, item: Item): string {
<div class="it-main">
${permissions}${palette}
<section class="it-block" aria-labelledby="it-source">
<h2 class="it-h" id="it-source">${esc(t(item.kind === 'plugin' ? 'site.gen.item.code' : item.kind === 'strings' ? 'site.gen.item.packFile' : 'site.gen.item.themeFile'))}</h2>
<h2 class="it-h" id="it-source">${esc(t(item.kind === 'plugin' ? 'site.gen.item.code' : item.kind === 'strings' || item.kind === 'icons' ? 'site.gen.item.packFile' : 'site.gen.item.themeFile'))}</h2>
<details class="it-src" data-source data-source-url="${esc(fileHref)}" data-source-lang="${item.kind === 'plugin' ? 'js' : 'json'}">
<summary><span class="it-src-t">${esc(t('site.gen.item.read', { file }))}</span><span class="it-src-m">${kb(item.size)} &middot; ${esc(t('site.gen.item.exactBytes'))}</span></summary>
<div class="code">
@ -343,9 +344,9 @@ function itemHtml(page: Page, item: Item): string {
<ol class="it-steps">
<li>${th('site.gen.item.how.link', { kind: item.kind }, { ...bold, code1: '<code>waybetter://</code>' })}</li>
<li>${th('site.gen.item.how.check', {}, bold)}</li>
<li>${th(item.kind === 'plugin' ? 'site.gen.item.how.confirmPlugin' : item.kind === 'strings' ? 'site.gen.item.how.confirmStrings' : 'site.gen.item.how.confirmTheme', {}, bold)}</li>
<li>${th(item.kind === 'plugin' ? 'site.gen.item.how.confirmPlugin' : item.kind === 'strings' ? 'site.gen.item.how.confirmStrings' : item.kind === 'icons' ? 'site.gen.item.how.confirmIcons' : 'site.gen.item.how.confirmTheme', {}, bold)}</li>
</ol>
<p class="it-fine">${th(item.kind === 'plugin' ? 'site.gen.item.how.finePlugin' : item.kind === 'strings' ? 'site.gen.item.how.fineStrings' : 'site.gen.item.how.fineTheme')} ${th('site.gen.item.how.vouched')}</p>
<p class="it-fine">${th(item.kind === 'plugin' ? 'site.gen.item.how.finePlugin' : item.kind === 'strings' ? 'site.gen.item.how.fineStrings' : item.kind === 'icons' ? 'site.gen.item.how.fineIcons' : 'site.gen.item.how.fineTheme')} ${th('site.gen.item.how.vouched')}</p>
<div class="it-manual">
<h3>${esc(t('site.gen.item.manual.title'))}</h3>
<p>${th(app.page && app.oneClickSince ? 'site.gen.item.manual.text' : 'site.gen.item.manual.textNoPage', { app: app.name, version: app.oneClickSince ?? '', file }, { a1: `<a href="${rel}${app.page ?? ''}">`, a1End: '</a>', a2: `<a href="${esc(fileHref)}" download="${esc(file)}">`, a2End: '</a>' })}</p>
@ -366,7 +367,7 @@ function itemHtml(page: Page, item: Item): string {
${related.length ? `<section class="it-more">
<div class="wrap">
<header class="st-shelf-head"><h2 class="st-h">${esc(t(item.kind === 'theme' ? 'site.gen.item.moreThemes' : item.kind === 'strings' ? 'site.gen.item.moreStrings' : 'site.gen.item.morePlugins'))}</h2><p><a href="${rel}store/?kind=${item.kind}">${esc(t('site.gen.item.seeAll', { count: site.catalog.items.filter(other => other.kind === item.kind).length }))}</a></p></header>
<header class="st-shelf-head"><h2 class="st-h">${esc(t(item.kind === 'theme' ? 'site.gen.item.moreThemes' : item.kind === 'strings' ? 'site.gen.item.moreStrings' : item.kind === 'icons' ? 'site.gen.item.moreIcons' : 'site.gen.item.morePlugins'))}</h2><p><a href="${rel}store/?kind=${item.kind}">${esc(t('site.gen.item.seeAll', { count: site.catalog.items.filter(other => other.kind === item.kind).length }))}</a></p></header>
<div class="gal-grid">${related.map(other => card(page, other, 'h3')).join('\n')}</div>
</div>
</section>` : ''}`;
@ -553,7 +554,8 @@ function gitDates(root: string): Map<string, string> {
const relOf = (path: string) => '../'.repeat(path.split('/').length - 2) || './';
export function storePages(root: string): Plugin {
const catalogPath = resolve(root, 'public/store/catalog.json');
// catalog.v3.json is the full list (every kind, format 3), catalog.json what every app knows (format 2): the same items, so the pages read the fuller one.
const catalogPath = resolve(root, existsSync(resolve(root, 'public/store/catalog.v3.json')) ? 'public/store/catalog.v3.json' : 'public/store/catalog.json');
const featuredPath = resolve(root, '../../store/featured.json');
const itemFrame = resolve(root, 'store/_item.html');
const localesDir = resolve(root, 'locales');
@ -566,8 +568,10 @@ export function storePages(root: string): Plugin {
const load = (): Loaded => {
if (cache) return cache;
const catalog = JSON.parse(readFileSync(catalogPath, 'utf8')) as Catalog;
if (catalog.format !== 2) throw new Error('store catalog: unsupported format');
const signed = JSON.parse(readFileSync(catalogPath, 'utf8')) as Omit<Catalog, 'items'> & { items: Array<Item | { kind: 'builtin' }> };
if (signed.format !== 2 && signed.format !== 3) throw new Error('store catalog: unsupported format');
// Built-in items (area plugins) are listed by the app's Store with their switch; the site has its own area pages (areas.ts), so they are not pages here.
const catalog: Catalog = { ...signed, items: signed.items.filter((item): item is Item => item.kind !== 'builtin') };
const slugs = itemSlugs(catalog.items);
const files = gitDates(root);
// Store files by their path below the site root ("store/themes/nord.json").