@gjsify/site 0.64.3-next.261 → 0.64.3-next.264

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,6 +107,7 @@ Lives in the root `package.json`, next to `ship` and `flatpak`.
107
107
  | `components` | `[{id, name, summary?, match?}]`: the separately installed parts of the product, in install order, see below. Optional — without it the dialog is one undivided list. |
108
108
  | `downloads` | `[{platform, format, url?, label?, available?, component?}]`: targets the release assets do not cover, e.g. browser stores or a source install. `platform` is an OS or `firefox` / `chromium`; `format` adds `store`, `xpi`, `source`. `url` is required unless `available` is `false`, which lists the target as "Not yet available" and never recommends it. `component` is a `components[].id`. |
109
109
  | `releaseAssets` | `false` ignores the GitHub release assets, so `downloads` is the whole list. Default `true`. |
110
+ | `relatedApps` | `[url \| {url, name, summary, icon}]`: sites of other apps, listed in order under "More apps by <developer>" on the landing page, see below. Optional — without it there is no section and nothing is fetched. |
110
111
  | `support`, `legal` | `support`: `{kind, url}` links, or `{kind: "sepa", name, iban, bic?, reference?}`, a bank transfer shown as IBAN plus an EPC QR code. `legal`: `{label, url}` links. Both empty by default: nothing is invented. |
111
112
 
112
113
  A `sepa` entry carries no amount, and its IBAN is checked when the config loads — length per
@@ -140,6 +141,28 @@ The recommendation ("Recommended for your system") is made once per part, so eac
140
141
  highlights its own best match, and the big button at the top of the dialog offers the first
141
142
  declared component: the thing to install first.
142
143
 
144
+ ### Related apps
145
+
146
+ Every build also writes `app.json` next to `index.html` — `{schema: 1, id, name, summary, icon, url,
147
+ languages, developer}`, names and summaries per language from the metainfo, `icon` and `url`
148
+ absolute. `relatedApps` reads those of other sites:
149
+
150
+ ```json
151
+ "relatedApps": [
152
+ "https://example.org/notes/",
153
+ { "url": "https://example.org/plain/", "name": "Plain", "summary": "No app.json here", "icon": "https://example.org/plain/icon.png" }
154
+ ]
155
+ ```
156
+
157
+ A string is a site address: the build fetches `<url>app.json` and renders a card (icon, name,
158
+ summary in the page's language, else English) linking to the app's page in that language. An
159
+ object is an inline entry for an app that publishes no `app.json`; nothing is fetched for it.
160
+ The site's own address and app id are dropped, so all apps can share one list.
161
+
162
+ The section is plain HTML rendered at build time. A site that cannot be reached, or whose
163
+ `app.json` is invalid, is served from the last good copy in the build cache; with none, that card
164
+ is skipped with a warning. The build never fails because of it.
165
+
143
166
  ### Screenshots
144
167
 
145
168
  Nothing to configure: the page shows the metainfo `<screenshots>`, in document order, with their
@@ -1115,6 +1115,74 @@ app-support {
1115
1115
  margin-block: 1.5rem 0;
1116
1116
  }
1117
1117
 
1118
+ /* Related apps: same card list as the links, with the app's own icon */
1119
+
1120
+ .related {
1121
+ margin-block: 2rem;
1122
+ }
1123
+
1124
+ .related h2 {
1125
+ font-size: 1.15rem;
1126
+ }
1127
+
1128
+ .related-list {
1129
+ border: 1px solid var(--line);
1130
+ border-radius: var(--radius-lg);
1131
+ background: var(--surface);
1132
+ overflow: hidden;
1133
+ list-style: none;
1134
+ padding: 0;
1135
+ }
1136
+
1137
+ .related-list li + li {
1138
+ border-top: 1px solid var(--line);
1139
+ }
1140
+
1141
+ .related-list a {
1142
+ display: flex;
1143
+ align-items: center;
1144
+ gap: 0.875rem;
1145
+ padding: 0.75rem 1rem;
1146
+ text-decoration: none;
1147
+ color: inherit;
1148
+ }
1149
+
1150
+ .related-list a:hover {
1151
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
1152
+ }
1153
+
1154
+ .related-icon {
1155
+ flex: none;
1156
+ width: 3rem;
1157
+ height: 3rem;
1158
+ border-radius: 10px;
1159
+ object-fit: contain;
1160
+ }
1161
+
1162
+ .related-icon-blank {
1163
+ display: flex;
1164
+ align-items: center;
1165
+ justify-content: center;
1166
+ background: var(--bg);
1167
+ color: var(--muted);
1168
+ }
1169
+
1170
+ .related-text {
1171
+ display: flex;
1172
+ flex-direction: column;
1173
+ min-width: 0;
1174
+ }
1175
+
1176
+ .related-name {
1177
+ font-weight: 600;
1178
+ }
1179
+
1180
+ .related-summary {
1181
+ color: var(--muted);
1182
+ font-size: 0.85rem;
1183
+ overflow-wrap: anywhere;
1184
+ }
1185
+
1118
1186
  /* Footer */
1119
1187
 
1120
1188
  .footer {
@@ -51,6 +51,8 @@ export declare function downloadWidget(ctx: DownloadContext): string;
51
51
  */
52
52
  export declare function badges(ctx: Pick<RenderContext, 'data' | 't' | 'lang'>): string;
53
53
  export declare function linkCard(href: string, title: string, glyph: IconName): string;
54
+ /** Other apps of the same developer, resolved at build time so the page needs no script. */
55
+ export declare function relatedSection(ctx: Pick<RenderContext, 'data' | 'lang' | 't'>): string;
54
56
  /** A bank transfer as a card: the details to read or copy, and an EPC QR code that fills them into a banking app. */
55
57
  export declare function sepaCard(entry: SiteSupportSepa, t: Translate): string;
56
58
  /** The title and description of one page; also what the build checks for weak ones. */
@@ -1,4 +1,4 @@
1
- import { epcPayload, formatIban, isBrowserPlatform, PLATFORM_ORDER, pickByLanguage, } from '../site-data/index.js';
1
+ import { epcPayload, formatIban, isBrowserPlatform, PLATFORM_ORDER, pickByLanguage, relatedText, relatedUrl, } from '../site-data/index.js';
2
2
  import { readableText } from './color.js';
3
3
  import { attrs, esc, fmt, plain } from './html.js';
4
4
  import { icon } from './icons.js';
@@ -466,6 +466,32 @@ ${items.join('\n')}
466
466
  </ul>
467
467
  </section>`;
468
468
  }
469
+ function relatedCard(app, lang) {
470
+ const summary = relatedText(app.summary, lang);
471
+ const mark = app.icon
472
+ ? `<img class="related-icon" src="${esc(app.icon)}" alt="" width="48" height="48" loading="lazy">`
473
+ : `<span class="related-icon related-icon-blank">${icon('package')}</span>`;
474
+ return `<li>
475
+ <a href="${esc(relatedUrl(app, lang))}">
476
+ ${mark}
477
+ <span class="related-text"><span class="related-name">${esc(relatedText(app.name, lang))}</span>${summary ? `<span class="related-summary">${esc(summary)}</span>` : ''}</span>
478
+ </a>
479
+ </li>`;
480
+ }
481
+ /** Other apps of the same developer, resolved at build time so the page needs no script. */
482
+ export function relatedSection(ctx) {
483
+ const { data, lang, t } = ctx;
484
+ if (data.relatedApps.length === 0)
485
+ return '';
486
+ const developer = data.app.developer.name;
487
+ const heading = developer ? fmt(t('More apps by {developer}'), { developer }) : esc(t('More apps'));
488
+ return `<section class="related">
489
+ <h2>${heading}</h2>
490
+ <ul class="related-list">
491
+ ${data.relatedApps.map((app) => relatedCard(app, lang)).join('\n')}
492
+ </ul>
493
+ </section>`;
494
+ }
469
495
  /** A bank transfer as a card: the details to read or copy, and an EPC QR code that fills them into a banking app. */
470
496
  export function sepaCard(entry, t) {
471
497
  const grouped = formatIban(entry.iban);
@@ -708,6 +734,7 @@ ${blocks(data.app.description, lang)}
708
734
  ${releases(ctx)}
709
735
  ${support(ctx)}
710
736
  ${links(ctx)}
737
+ ${relatedSection(ctx)}
711
738
  ${tags(ctx)}
712
739
  </div>
713
740
  </main>
@@ -54,6 +54,39 @@ function parseSupport(entry, where) {
54
54
  }
55
55
  return { kind: 'sepa', name, iban, bic, reference };
56
56
  }
57
+ function httpUrl(value, where) {
58
+ if (!/^https?:\/\//.test(value))
59
+ throw new Error(`${where} must be an http(s) address`);
60
+ return value;
61
+ }
62
+ /** Strings are site addresses to fetch `app.json` from; objects are inline entries for apps without one. */
63
+ function parseRelatedApps(raw) {
64
+ const value = raw.relatedApps;
65
+ if (value === undefined)
66
+ return [];
67
+ if (!Array.isArray(value))
68
+ throw new Error('gjsify.site.relatedApps must be a list');
69
+ return value.map((entry, index) => {
70
+ const where = `gjsify.site.relatedApps[${index}]`;
71
+ if (typeof entry === 'string')
72
+ return { url: withSlash(httpUrl(entry, where)) };
73
+ if (!isRecord(entry))
74
+ throw new Error(`${where} must be a site address or an object`);
75
+ const field = (key) => (entry[key] === undefined ? undefined : stringField(entry, key, where));
76
+ const url = withSlash(httpUrl(stringField(entry, 'url', where), `${where}.url`));
77
+ const name = field('name');
78
+ if (name === undefined) {
79
+ for (const key of ['summary', 'icon']) {
80
+ if (entry[key] !== undefined)
81
+ throw new Error(`${where}.${key} needs "name": inline entries have all three`);
82
+ }
83
+ return { url };
84
+ }
85
+ const icon = field('icon');
86
+ return { url, name, summary: field('summary'), icon: icon && httpUrl(icon, `${where}.icon`) };
87
+ });
88
+ }
89
+ const withSlash = (url) => (url.endsWith('/') ? url : `${url}/`);
57
90
  const PLATFORMS = ['linux', 'darwin', 'win32', 'android', 'web', 'firefox', 'chromium'];
58
91
  const FORMATS = [
59
92
  'flathub',
@@ -149,7 +182,7 @@ export function parseSiteConfig(manifest) {
149
182
  return {
150
183
  metainfo: requireString(raw, 'metainfo'),
151
184
  base: normalizeBase(requireString(raw, 'base')),
152
- url: url.endsWith('/') ? url : `${url}/`,
185
+ url: withSlash(url),
153
186
  languages: languages,
154
187
  defaultLanguage,
155
188
  sourceLanguage: optionalString(raw, 'sourceLanguage') ?? 'en',
@@ -167,6 +200,7 @@ export function parseSiteConfig(manifest) {
167
200
  label: stringField(entry, 'label', `gjsify.site.legal[${index}]`),
168
201
  url: stringField(entry, 'url', `gjsify.site.legal[${index}]`),
169
202
  })),
203
+ relatedApps: parseRelatedApps(raw),
170
204
  };
171
205
  }
172
206
  /** The `kind` labels the support page knows by name; the page catalogues translate them. */
@@ -7,6 +7,7 @@ export * from './metainfo.js';
7
7
  export * from './network.js';
8
8
  export * from './oars.js';
9
9
  export * from './po.js';
10
+ export * from './related.js';
10
11
  export * from './screenshots.js';
11
12
  export * from './sepa.js';
12
13
  export * from './types.js';
@@ -7,6 +7,7 @@ export * from './metainfo.js';
7
7
  export * from './network.js';
8
8
  export * from './oars.js';
9
9
  export * from './po.js';
10
+ export * from './related.js';
10
11
  export * from './screenshots.js';
11
12
  export * from './sepa.js';
12
13
  export * from './types.js';
@@ -8,6 +8,7 @@ import { fetchLatestRelease } from './github.js';
8
8
  import { parseMetainfo } from './metainfo.js';
9
9
  import { minimumAge } from './oars.js';
10
10
  import { completeLocalized, languageCandidates, parseCatalog } from './po.js';
11
+ import { loadRelatedApps } from './related.js';
11
12
  import { resolveScreenshots } from './screenshots.js';
12
13
  /** Reads `<dir>/<lang>.po` for every language, trying `pt-BR` and `pt_BR` spellings; missing files are skipped. */
13
14
  export async function loadCatalogs(dir, languages) {
@@ -122,6 +123,11 @@ export async function loadSiteData(options) {
122
123
  const developer = metainfo.developer.name
123
124
  ? metainfo.developer
124
125
  : { id: manifest.gjsify?.flatpak?.developer?.id, name: manifest.gjsify?.flatpak?.developer?.name ?? '' };
126
+ const relatedApps = await loadRelatedApps(config, metainfo.id, {
127
+ offline: options.offline,
128
+ network: options.network,
129
+ warn,
130
+ });
125
131
  const releases = metainfo.releases.map((release) => ({
126
132
  ...release,
127
133
  description: completeBlocks(release.description),
@@ -160,6 +166,7 @@ export async function loadSiteData(options) {
160
166
  hasReleases,
161
167
  },
162
168
  stats,
169
+ relatedApps,
163
170
  warnings,
164
171
  };
165
172
  }
@@ -0,0 +1,23 @@
1
+ import { type NetworkOptions } from './network.js';
2
+ import type { AppManifest, RelatedApp, SiteConfig, SiteData } from './types.js';
3
+ /** Where a site publishes its `app.json`. */
4
+ export declare const APP_MANIFEST_FILE = "app.json";
5
+ /** The `app.json` of a site: what another site needs to list this app. Icon URL is absolute. */
6
+ export declare function buildAppManifest(data: SiteData, icon: string | undefined): AppManifest;
7
+ /** Validates what came off the wire; returns why not when it is unusable. */
8
+ export declare function parseAppManifest(raw: unknown): RelatedApp | string;
9
+ export interface RelatedAppsOptions {
10
+ /** Never touch the network; only a copy from an earlier build can answer. */
11
+ offline?: boolean;
12
+ network?: NetworkOptions;
13
+ warn: (message: string) => void;
14
+ }
15
+ /**
16
+ * Resolves `gjsify.site.relatedApps` in configured order. The site's own entry is dropped, so every
17
+ * app can share one list. Never throws: an entry that cannot be had is skipped with a warning.
18
+ */
19
+ export declare function loadRelatedApps(config: SiteConfig, ownId: string, options: RelatedAppsOptions): Promise<RelatedApp[]>;
20
+ /** A text in the reader's language, else English, else whatever the app has. */
21
+ export declare function relatedText(text: Record<string, string>, lang: string): string;
22
+ /** The page of a related app in the reader's language, or its start page when it has no such one. */
23
+ export declare function relatedUrl(app: RelatedApp, lang: string): string;
@@ -0,0 +1,102 @@
1
+ import { fetchJson } from './network.js';
2
+ import { pickByLanguage } from './po.js';
3
+ /** Where a site publishes its `app.json`. */
4
+ export const APP_MANIFEST_FILE = 'app.json';
5
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ const urlKey = (url) => url.replace(/\/+$/, '').toLowerCase();
7
+ const sameUrl = (a, b) => urlKey(a) === urlKey(b);
8
+ const HTTP_URL = /^https?:\/\//;
9
+ /** The `app.json` of a site: what another site needs to list this app. Icon URL is absolute. */
10
+ export function buildAppManifest(data, icon) {
11
+ const texts = (text) => ({
12
+ [data.config.sourceLanguage]: text.source,
13
+ ...text.byLanguage,
14
+ });
15
+ return {
16
+ schema: 1,
17
+ id: data.app.id,
18
+ name: texts(data.app.name),
19
+ summary: texts(data.app.summary),
20
+ icon,
21
+ url: data.config.url,
22
+ languages: [...data.config.languages],
23
+ developer: { id: data.app.developer.id, name: data.app.developer.name },
24
+ };
25
+ }
26
+ function stringRecord(value) {
27
+ if (!isRecord(value))
28
+ return undefined;
29
+ const entries = Object.entries(value);
30
+ if (!entries.every(([, text]) => typeof text === 'string'))
31
+ return undefined;
32
+ return Object.fromEntries(entries);
33
+ }
34
+ /** Validates what came off the wire; returns why not when it is unusable. */
35
+ export function parseAppManifest(raw) {
36
+ if (!isRecord(raw))
37
+ return 'not a JSON object';
38
+ if (raw.schema !== 1)
39
+ return `unknown schema ${JSON.stringify(raw.schema)}`;
40
+ const name = stringRecord(raw.name);
41
+ if (!name || Object.keys(name).length === 0)
42
+ return 'no "name"';
43
+ const summary = stringRecord(raw.summary) ?? {};
44
+ if (typeof raw.url !== 'string' || !HTTP_URL.test(raw.url))
45
+ return 'no http(s) "url"';
46
+ const languages = Array.isArray(raw.languages) ? raw.languages.filter((l) => typeof l === 'string') : [];
47
+ const icon = typeof raw.icon === 'string' && HTTP_URL.test(raw.icon) ? raw.icon : undefined;
48
+ return {
49
+ id: typeof raw.id === 'string' ? raw.id : undefined,
50
+ name,
51
+ summary,
52
+ icon,
53
+ url: raw.url.endsWith('/') ? raw.url : `${raw.url}/`,
54
+ languages,
55
+ };
56
+ }
57
+ const offlineFetch = () => Promise.reject(new Error('offline build'));
58
+ async function loadOne(entry, options) {
59
+ if (entry.name !== undefined) {
60
+ return {
61
+ name: { en: entry.name },
62
+ summary: entry.summary === undefined ? {} : { en: entry.summary },
63
+ icon: entry.icon,
64
+ url: entry.url,
65
+ languages: [],
66
+ };
67
+ }
68
+ const file = `${entry.url}${APP_MANIFEST_FILE}`;
69
+ try {
70
+ // A TTL of zero: always ask first, and let the cache answer only when the site does not.
71
+ const raw = await fetchJson(file, { accept: 'application/json' }, { ...options.network, cacheTtlMs: 0, fetch: options.offline ? offlineFetch : options.network?.fetch });
72
+ const parsed = parseAppManifest(raw);
73
+ if (typeof parsed === 'string') {
74
+ options.warn(`Related app ${file}: ${parsed}; skipping it`);
75
+ return undefined;
76
+ }
77
+ return parsed;
78
+ }
79
+ catch (error) {
80
+ options.warn(`Related app ${file} is unavailable (${String(error)}) and no earlier copy exists; skipping it`);
81
+ return undefined;
82
+ }
83
+ }
84
+ /**
85
+ * Resolves `gjsify.site.relatedApps` in configured order. The site's own entry is dropped, so every
86
+ * app can share one list. Never throws: an entry that cannot be had is skipped with a warning.
87
+ */
88
+ export async function loadRelatedApps(config, ownId, options) {
89
+ const candidates = config.relatedApps.filter((entry) => !sameUrl(entry.url, config.url));
90
+ const loaded = await Promise.all(candidates.map((entry) => loadOne(entry, options)));
91
+ return loaded.filter((app) => app !== undefined && app.id !== ownId && !sameUrl(app.url, config.url));
92
+ }
93
+ /** A text in the reader's language, else English, else whatever the app has. */
94
+ export function relatedText(text, lang) {
95
+ return pickByLanguage(text, lang) ?? text.en ?? Object.values(text)[0] ?? '';
96
+ }
97
+ /** The page of a related app in the reader's language, or its start page when it has no such one. */
98
+ export function relatedUrl(app, lang) {
99
+ if (app.languages.length === 0)
100
+ return app.url;
101
+ return `${app.url}${app.languages.includes(lang) ? lang : app.languages[0]}/`;
102
+ }
@@ -73,6 +73,47 @@ export interface SiteDownloadConfig {
73
73
  /** Id of the `gjsify.site.components` entry this target installs. */
74
74
  component?: string;
75
75
  }
76
+ /**
77
+ * One entry of `gjsify.site.relatedApps`: the site of another app, whose `app.json` is fetched at
78
+ * build time. With `name` set it is an inline entry instead — for an app that publishes no
79
+ * `app.json` (not built with gjsify) — and nothing is fetched.
80
+ */
81
+ export interface SiteRelatedAppConfig {
82
+ url: string;
83
+ name?: string;
84
+ summary?: string;
85
+ icon?: string;
86
+ }
87
+ /**
88
+ * `app.json`, published next to every site's `index.html` so other sites can list the app. Bump
89
+ * `schema` on a breaking change; a reader ignores a schema it does not know.
90
+ */
91
+ export interface AppManifest {
92
+ schema: 1;
93
+ id: string;
94
+ /** Keyed by language, the metainfo's source text under `sourceLanguage`. */
95
+ name: Record<string, string>;
96
+ summary: Record<string, string>;
97
+ /** Absolute URL of the app icon. */
98
+ icon?: string;
99
+ /** The site's own address. */
100
+ url: string;
101
+ /** Languages the site has pages for: `<url><lang>/` exists. */
102
+ languages: string[];
103
+ developer: {
104
+ id?: string;
105
+ name: string;
106
+ };
107
+ }
108
+ /** A related app as the page shows it: an `app.json` or an inline entry, validated. */
109
+ export interface RelatedApp {
110
+ id?: string;
111
+ name: Record<string, string>;
112
+ summary: Record<string, string>;
113
+ icon?: string;
114
+ url: string;
115
+ languages: string[];
116
+ }
76
117
  /** The `gjsify.site` block of the root package.json, with defaults applied. */
77
118
  export interface SiteConfig {
78
119
  metainfo: string;
@@ -107,6 +148,8 @@ export interface SiteConfig {
107
148
  releaseAssets: boolean;
108
149
  support: SiteSupportEntry[];
109
150
  legal: SiteLegalLink[];
151
+ /** Sites of other apps to list under "More apps by <developer>", in order. Empty: no section. */
152
+ relatedApps: SiteRelatedAppConfig[];
110
153
  }
111
154
  /**
112
155
  * One entry of the metainfo `<screenshots>` list, with its artwork located on disk. The same images
@@ -247,6 +290,8 @@ export interface SiteData {
247
290
  hasReleases: boolean;
248
291
  };
249
292
  stats: Stats;
293
+ /** Resolved `gjsify.site.relatedApps`, in configured order, without this site's own app. */
294
+ relatedApps: RelatedApp[];
250
295
  /** Non-fatal problems met while collecting data, already logged. */
251
296
  warnings: string[];
252
297
  }
package/lib/site.js CHANGED
@@ -13,7 +13,7 @@ import { ogCardSvg } from './render/og-card.js';
13
13
  import { absolute, localized, pageMeta, pageUrl, renderPage, renderRoot, renderSupportPage, supportUrl, } from './render/pages.js';
14
14
  import { renderOverview } from './render/overview.js';
15
15
  import { robotsTxt, seoWarnings, sitemapXml } from './render/seo.js';
16
- import { UI_SOURCE_LANGUAGE, loadCatalogs, loadSiteData, mergeCatalogs, parseMetainfo, parseSiteConfig, pickByLanguage, translator, } from './site-data/index.js';
16
+ import { UI_SOURCE_LANGUAGE, loadCatalogs, loadSiteData, mergeCatalogs, parseMetainfo, parseSiteConfig, pickByLanguage, translator, APP_MANIFEST_FILE, buildAppManifest, } from './site-data/index.js';
17
17
  /**
18
18
  * This package's own directory: `lib/` holds the client bundle, `po/` the page catalogues.
19
19
  *
@@ -150,6 +150,8 @@ export async function buildSite(options) {
150
150
  await writeFile(join(target.out, lang, 'support', 'index.html'), renderSupportPage(ctx));
151
151
  }
152
152
  }
153
+ const manifest = buildAppManifest(data, iconUrl ? absolute(data, iconUrl) : undefined);
154
+ await writeFile(join(target.out, APP_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
153
155
  await writeFile(join(target.out, 'index.html'), renderRoot(data, assets, iconUrl));
154
156
  const address = (build) => Object.fromEntries(data.config.languages.map((lang) => [lang, absolute(data, build(data, lang))]));
155
157
  const pages = [address(pageUrl), ...(hasSupport ? [address(supportUrl)] : [])];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/site",
3
- "version": "0.64.3-next.261",
3
+ "version": "0.64.3-next.264",
4
4
  "description": "Static landing page generator for a GNOME-style app, rendered from its AppStream metainfo — downloads, screenshots, release notes, support links, no JavaScript required.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -54,26 +54,26 @@
54
54
  "license": "MIT",
55
55
  "dependencies": {
56
56
  "@girs/gdkpixbuf-2.0": "5.4.0",
57
- "@gjsify/console": "0.64.3-next.261",
58
- "@gjsify/crypto": "0.64.3-next.261",
59
- "@gjsify/fs": "0.64.3-next.261",
60
- "@gjsify/module": "0.64.3-next.261",
61
- "@gjsify/os": "0.64.3-next.261",
62
- "@gjsify/path": "0.64.3-next.261",
63
- "@gjsify/process": "0.64.3-next.261",
64
- "@gjsify/url": "0.64.3-next.261",
57
+ "@gjsify/console": "0.64.3-next.264",
58
+ "@gjsify/crypto": "0.64.3-next.264",
59
+ "@gjsify/fs": "0.64.3-next.264",
60
+ "@gjsify/module": "0.64.3-next.264",
61
+ "@gjsify/os": "0.64.3-next.264",
62
+ "@gjsify/path": "0.64.3-next.264",
63
+ "@gjsify/process": "0.64.3-next.264",
64
+ "@gjsify/url": "0.64.3-next.264",
65
65
  "fast-xml-parser": "^5.11.2",
66
66
  "qrcode-generator": "^2.0.4"
67
67
  },
68
68
  "devDependencies": {
69
- "@gjsify/cli": "0.64.3-next.261",
69
+ "@gjsify/cli": "0.64.3-next.264",
70
70
  "@gjsify/node-gi": "file:../../node-gi/node-gi",
71
- "@gjsify/unit": "0.64.3-next.261",
71
+ "@gjsify/unit": "0.64.3-next.264",
72
72
  "@types/node": "^25.9.2",
73
73
  "typescript": "^6.0.3"
74
74
  },
75
75
  "gjsify": {
76
76
  "tier": 2
77
77
  },
78
- "gitHead": "886de21be840429ce043cdbf8195172530e230d4"
78
+ "gitHead": "0f5ca82aab25bf9df8d1e790d28483b1f90fe3a9"
79
79
  }
package/po/de.po CHANGED
@@ -168,6 +168,12 @@ msgstr "Ältere Versionen"
168
168
  msgid "Links"
169
169
  msgstr "Links"
170
170
 
171
+ msgid "More apps by {developer}"
172
+ msgstr "Weitere Apps von {developer}"
173
+
174
+ msgid "More apps"
175
+ msgstr "Weitere Apps"
176
+
171
177
  msgid "Copied"
172
178
  msgstr "Kopiert"
173
179
 
package/po/site.pot CHANGED
@@ -167,6 +167,12 @@ msgstr ""
167
167
  msgid "Links"
168
168
  msgstr ""
169
169
 
170
+ msgid "More apps by {developer}"
171
+ msgstr ""
172
+
173
+ msgid "More apps"
174
+ msgstr ""
175
+
170
176
  msgid "Copied"
171
177
  msgstr ""
172
178