@gjsify/adwaita-web 0.64.2 → 0.64.3-next.260
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/dist/adwaita-web.css +20 -1
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/elements/adw-about-dialog.d.ts +53 -7
- package/lib/types/language-host.d.ts +1 -0
- package/lib/types/namespace/glib.d.ts +4 -2
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +13 -12
- package/scss/_about_dialog.scss +10 -2
- package/scss/_icons.generated.scss +12 -0
- package/src/adw-about-dialog.spec.ts +253 -12
- package/src/elements/adw-about-dialog.ts +258 -54
- package/src/elements/adw-preferences-row.ts +2 -1
- package/src/glib.spec.ts +62 -0
- package/src/language-host.ts +10 -0
- package/src/namespace/glib.ts +14 -2
- package/src/styles.generated.ts +1 -1
|
@@ -31,9 +31,12 @@
|
|
|
31
31
|
// support-url a support-forum URL (a main-page Support Questions row)
|
|
32
32
|
// issue-url an issue-tracker URL (a main-page Report an Issue row)
|
|
33
33
|
// copyright the copyright line (Legal page)
|
|
34
|
-
// license the license text
|
|
35
|
-
//
|
|
36
|
-
// license-
|
|
34
|
+
// license the custom license text (Legal page, Pango markup); setting it makes
|
|
35
|
+
// `license-type` custom, as in libadwaita
|
|
36
|
+
// license-type a `Gtk.License`: its integer value or its nick (`mit-x11`, `gpl-3-0`);
|
|
37
|
+
// a known type shows the stock warranty line and clears `license`
|
|
38
|
+
// license-url NOT in libadwaita: a URL a custom `license` text links to
|
|
39
|
+
// translator-credits the translators, one per line (a "Translated by" credits group)
|
|
37
40
|
// release-notes AppStream release-notes markup — a What's New row + page,
|
|
38
41
|
// the buffer from the core's `releaseNotesBuffer`
|
|
39
42
|
// release-notes-version the version the What's New heading names, else `version`
|
|
@@ -44,12 +47,18 @@
|
|
|
44
47
|
// AdwAboutDialog `developers` / `designers` / `artists` / `documenters` properties
|
|
45
48
|
// — entries may be `Name`, `Name <email>` or `Name https://url`, parsed by the
|
|
46
49
|
// core's `parseCreditPerson` (`parse_person`). `add_link (title, url)` appends a
|
|
47
|
-
// Details-page link row, as on GTK
|
|
50
|
+
// Details-page link row, as on GTK; `add_credit_section`, `add_acknowledgement_section`
|
|
51
|
+
// and `add_legal_section` append a Credits, Acknowledgements or Legal group.
|
|
48
52
|
//
|
|
49
|
-
// NOT implemented here
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// refused by name: it needs an AppStream parser.
|
|
53
|
+
// NOT implemented here: the link rows' context menu ("Open Link" / "Copy Link Address", right
|
|
54
|
+
// click or long press) of libadwaita's private `AdwLinkRow`, the debug page's "Save As…" button (`debug-info-filename`), which
|
|
55
|
+
// needs a file dialog, and `add_other_app` / `other-apps-title`. `new_from_appdata` is
|
|
56
|
+
// refused by name: it needs an AppStream parser. An `add_*_section` call with no people
|
|
57
|
+
// draws no row, where libadwaita 1.9 leaves a Credits row that opens a blank page.
|
|
58
|
+
//
|
|
59
|
+
// Events (CustomEvent, bubbles): `notify::license` and `notify::license-type` (detail
|
|
60
|
+
// `{ license, licenseType }`), in the order `adw_about_dialog_set_license` and
|
|
61
|
+
// `set_license_type` notify them.
|
|
53
62
|
//
|
|
54
63
|
// Events (CustomEvent, bubbles): `notify::open` (detail `{ open }`), `closed`, and
|
|
55
64
|
// `activate-link` (detail `{ uri }`, CANCELABLE — the signal's `true_handled`
|
|
@@ -67,16 +76,29 @@
|
|
|
67
76
|
|
|
68
77
|
import {
|
|
69
78
|
ADW_ABOUT_DIALOG_LABELS,
|
|
79
|
+
linkRowIconName,
|
|
80
|
+
linkRowTooltip,
|
|
81
|
+
ADW_LICENSE_DEFAULTS,
|
|
70
82
|
type AdwCreditPerson,
|
|
71
|
-
|
|
83
|
+
type AdwCreditsSectionInput,
|
|
84
|
+
type AdwLegalSectionInput,
|
|
85
|
+
type AdwLicenseState,
|
|
72
86
|
type ReleaseNotesSegment,
|
|
73
87
|
Toast,
|
|
74
88
|
aboutDialogVisibility,
|
|
89
|
+
acknowledgementSections,
|
|
75
90
|
creditsSections,
|
|
91
|
+
isLicenseType,
|
|
76
92
|
legalSectionVisible,
|
|
93
|
+
legalSections,
|
|
94
|
+
legalVisible,
|
|
95
|
+
licenseTypeForNick,
|
|
77
96
|
refuseNewFromAppdata,
|
|
78
97
|
releaseNotesBuffer,
|
|
98
|
+
setLicense,
|
|
99
|
+
setLicenseType,
|
|
79
100
|
stripMnemonic,
|
|
101
|
+
GTK_LICENSE,
|
|
80
102
|
} from '@gjsify/adwaita-core';
|
|
81
103
|
|
|
82
104
|
import { webClipboardHost } from '../clipboard-host.js';
|
|
@@ -84,6 +106,7 @@ import type { AdwToastOverlay } from './adw-toast-overlay.js';
|
|
|
84
106
|
import './adw-toast-overlay.js';
|
|
85
107
|
import { createGtkImage } from './gtk-image.js';
|
|
86
108
|
import { AdwModalSurface } from './modal-surface.js';
|
|
109
|
+
import { renderPangoMarkup } from '../pango-markup.js';
|
|
87
110
|
import { WidgetElement } from '../widget-element.js';
|
|
88
111
|
|
|
89
112
|
/** A template label with its mnemonic marker removed — this renderer has no accelerator layer. */
|
|
@@ -108,6 +131,15 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
108
131
|
private _artists: string[] = [];
|
|
109
132
|
private _documenters: string[] = [];
|
|
110
133
|
|
|
134
|
+
/** `license` + `license-type`, one state driven by the core transitions. */
|
|
135
|
+
private _licenseState: AdwLicenseState = { ...ADW_LICENSE_DEFAULTS };
|
|
136
|
+
/** Set while an attribute is being written back from `_licenseState`. */
|
|
137
|
+
private _reflecting = false;
|
|
138
|
+
/** `add_credit_section`, `add_acknowledgement_section` and `add_legal_section`, in call order. */
|
|
139
|
+
private _creditSections: AdwCreditsSectionInput[] = [];
|
|
140
|
+
private _acknowledgementSections: AdwCreditsSectionInput[] = [];
|
|
141
|
+
private _legalSections: AdwLegalSectionInput[] = [];
|
|
142
|
+
|
|
111
143
|
/** `add_link` rows, in order; `has_custom_links` latches with the first. */
|
|
112
144
|
private _links: { title: string; url: string }[] = [];
|
|
113
145
|
/**
|
|
@@ -129,7 +161,9 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
129
161
|
'issue-url',
|
|
130
162
|
'copyright',
|
|
131
163
|
'license',
|
|
164
|
+
'license-type',
|
|
132
165
|
'license-url',
|
|
166
|
+
'translator-credits',
|
|
133
167
|
'release-notes',
|
|
134
168
|
'release-notes-version',
|
|
135
169
|
'debug-info',
|
|
@@ -214,14 +248,32 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
214
248
|
this.setAttribute('copyright', value);
|
|
215
249
|
}
|
|
216
250
|
|
|
251
|
+
/** `license`: the custom licence text, `''` for a stock licence. */
|
|
217
252
|
get license(): string {
|
|
218
|
-
return this.
|
|
253
|
+
return this._licenseState.license;
|
|
219
254
|
}
|
|
220
255
|
|
|
221
256
|
set license(value: string) {
|
|
222
257
|
this.setAttribute('license', value);
|
|
223
258
|
}
|
|
224
259
|
|
|
260
|
+
/** `license-type`: a `Gtk.License` value (see `GTK_LICENSE`). */
|
|
261
|
+
get licenseType(): number {
|
|
262
|
+
return this._licenseState.licenseType;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
set licenseType(value: number) {
|
|
266
|
+
this.setAttribute('license-type', String(value));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
get translatorCredits(): string {
|
|
270
|
+
return this.getAttribute('translator-credits') ?? '';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
set translatorCredits(value: string) {
|
|
274
|
+
this.setAttribute('translator-credits', value);
|
|
275
|
+
}
|
|
276
|
+
|
|
225
277
|
get licenseUrl(): string {
|
|
226
278
|
return this.getAttribute('license-url') ?? '';
|
|
227
279
|
}
|
|
@@ -260,6 +312,28 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
260
312
|
if (this._initialized) this._render();
|
|
261
313
|
}
|
|
262
314
|
|
|
315
|
+
/** `adw_about_dialog_add_credit_section`: a Credits group; `name` may be `null`. */
|
|
316
|
+
add_credit_section(name: string | null, people: string[]): void {
|
|
317
|
+
this._creditSections.push({ title: name, people: [...people] });
|
|
318
|
+
if (this._initialized) this._render();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** `adw_about_dialog_add_acknowledgement_section`: an Acknowledgements group. */
|
|
322
|
+
add_acknowledgement_section(name: string | null, people: string[]): void {
|
|
323
|
+
this._acknowledgementSections.push({ title: name, people: [...people] });
|
|
324
|
+
if (this._initialized) this._render();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* `adw_about_dialog_add_legal_section`: a Legal-page section. A `license_type` outside
|
|
329
|
+
* `Gtk.License` is rejected, like the property setter.
|
|
330
|
+
*/
|
|
331
|
+
add_legal_section(title: string, copyright: string | null, license_type: number, license: string | null): void {
|
|
332
|
+
if (!isLicenseType(license_type)) return;
|
|
333
|
+
this._legalSections.push({ title, copyright, licenseType: license_type, license });
|
|
334
|
+
if (this._initialized) this._render();
|
|
335
|
+
}
|
|
336
|
+
|
|
263
337
|
get open(): boolean {
|
|
264
338
|
return this.hasAttribute('open');
|
|
265
339
|
}
|
|
@@ -353,6 +427,13 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
353
427
|
|
|
354
428
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
355
429
|
if (oldValue === newValue) return;
|
|
430
|
+
// Before the guard: the two attributes are ONE state, and a value set before the
|
|
431
|
+
// element connects has to be in it when the first render reads it.
|
|
432
|
+
if (name === 'license' || name === 'license-type') {
|
|
433
|
+
if (!this._reflecting) this._applyLicenseAttribute(name, newValue);
|
|
434
|
+
if (this._initialized && !this._reflecting) this._render();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
356
437
|
// Before the guard: the heading reads `version` as it is when the NOTES change.
|
|
357
438
|
if (name === 'release-notes' || name === 'release-notes-version') {
|
|
358
439
|
this._releaseNotesSegments = releaseNotesBuffer(
|
|
@@ -374,6 +455,53 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
374
455
|
this._render();
|
|
375
456
|
}
|
|
376
457
|
|
|
458
|
+
/** Run the core setter an attribute maps to, notify what it notified, and write the pair back. */
|
|
459
|
+
private _applyLicenseAttribute(name: string, value: string | null): void {
|
|
460
|
+
const transition =
|
|
461
|
+
name === 'license'
|
|
462
|
+
? setLicense(this._licenseState, value ?? '')
|
|
463
|
+
: setLicenseType(this._licenseState, this._parseLicenseType(value));
|
|
464
|
+
this._licenseState = transition.state;
|
|
465
|
+
this._reflectLicense();
|
|
466
|
+
for (const property of transition.notify) {
|
|
467
|
+
this.dispatchEvent(
|
|
468
|
+
new CustomEvent(`notify::${property}`, {
|
|
469
|
+
bubbles: true,
|
|
470
|
+
detail: { license: this._licenseState.license, licenseType: this._licenseState.licenseType },
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** An integer or a `Gtk.License` nick; anything else is out of range, so the setter refuses it. */
|
|
477
|
+
private _parseLicenseType(value: string | null): number {
|
|
478
|
+
if (value === null) return GTK_LICENSE.UNKNOWN;
|
|
479
|
+
if (/^-?\d+$/.test(value.trim())) return Number(value);
|
|
480
|
+
return licenseTypeForNick(value.trim()) ?? -1;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Write `_licenseState` back onto the attributes, which a setter may have moved: a stock
|
|
485
|
+
* licence clears `license`. A `license-type` the app wrote as a nick is left as it wrote it.
|
|
486
|
+
*/
|
|
487
|
+
private _reflectLicense(): void {
|
|
488
|
+
const { license, licenseType } = this._licenseState;
|
|
489
|
+
this._reflecting = true;
|
|
490
|
+
try {
|
|
491
|
+
if (license.length === 0) this.removeAttribute('license');
|
|
492
|
+
else if (this.getAttribute('license') !== license) this.setAttribute('license', license);
|
|
493
|
+
|
|
494
|
+
const written = this.getAttribute('license-type');
|
|
495
|
+
if (licenseType === GTK_LICENSE.UNKNOWN) {
|
|
496
|
+
if (written !== null) this.removeAttribute('license-type');
|
|
497
|
+
} else if (written === null || this._parseLicenseType(written) !== licenseType) {
|
|
498
|
+
this.setAttribute('license-type', String(licenseType));
|
|
499
|
+
}
|
|
500
|
+
} finally {
|
|
501
|
+
this._reflecting = false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
377
505
|
/** Reflect the presented state onto the host + reset the navigation stack. */
|
|
378
506
|
private _syncOpen(): void {
|
|
379
507
|
this.classList.toggle('open', this.open);
|
|
@@ -396,22 +524,24 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
396
524
|
designers: this._designers,
|
|
397
525
|
artists: this._artists,
|
|
398
526
|
documenters: this._documenters,
|
|
527
|
+
translatorCredits: this.translatorCredits,
|
|
528
|
+
creditSections: this._creditSections,
|
|
399
529
|
});
|
|
400
530
|
}
|
|
401
531
|
|
|
402
|
-
/**
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return legalSectionVisible(this.copyright, licenseType, this.license);
|
|
532
|
+
/** What `update_legal` reads: the dialog's own copyright + licence, then the added sections. */
|
|
533
|
+
private _legalInput() {
|
|
534
|
+
return {
|
|
535
|
+
copyright: this.copyright,
|
|
536
|
+
licenseType: this._licenseState.licenseType,
|
|
537
|
+
license: this._licenseState.license,
|
|
538
|
+
legalSections: this._legalSections,
|
|
539
|
+
};
|
|
411
540
|
}
|
|
412
541
|
|
|
413
542
|
private _render(): void {
|
|
414
543
|
const sections = this._creditsSections();
|
|
544
|
+
const acknowledgements = acknowledgementSections(this._acknowledgementSections);
|
|
415
545
|
const visibility = aboutDialogVisibility({
|
|
416
546
|
applicationIcon: this.applicationIcon,
|
|
417
547
|
applicationName: this.applicationName,
|
|
@@ -425,7 +555,8 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
425
555
|
releaseNotes: this.releaseNotes,
|
|
426
556
|
hasCustomLinks: this._links.length > 0,
|
|
427
557
|
hasCredits: sections.length > 0,
|
|
428
|
-
hasLegal: this.
|
|
558
|
+
hasLegal: legalVisible(this._legalInput()),
|
|
559
|
+
hasAcknowledgements: acknowledgements.length > 0,
|
|
429
560
|
});
|
|
430
561
|
|
|
431
562
|
this._navView?.remove();
|
|
@@ -441,6 +572,7 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
441
572
|
}
|
|
442
573
|
if (visibility.creditsRow) this._navView.append(this._buildCreditsPage(sections));
|
|
443
574
|
if (visibility.legalRow) this._navView.append(this._buildLegalPage());
|
|
575
|
+
if (visibility.acknowledgementsRow) this._navView.append(this._buildAcknowledgementsPage(acknowledgements));
|
|
444
576
|
|
|
445
577
|
// Straight into the overlay's content box: appended to the overlay itself, the view
|
|
446
578
|
// is re-homed a microtask later, and that move drops the focus `present` put in it.
|
|
@@ -583,6 +715,9 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
583
715
|
const creditsLegalRows: HTMLElement[] = [];
|
|
584
716
|
if (visibility.creditsRow) creditsLegalRows.push(this._buildNavRow(rowLabel('creditsRow'), 'credits'));
|
|
585
717
|
if (visibility.legalRow) creditsLegalRows.push(this._buildNavRow(rowLabel('legalRow'), 'legal'));
|
|
718
|
+
if (visibility.acknowledgementsRow) {
|
|
719
|
+
creditsLegalRows.push(this._buildNavRow(rowLabel('acknowledgementsRow'), 'acknowledgements'));
|
|
720
|
+
}
|
|
586
721
|
|
|
587
722
|
if (visibility.detailsGroup) body.appendChild(this._buildGroup('details', detailsGroupRows));
|
|
588
723
|
if (visibility.supportGroup) body.appendChild(this._buildGroup('support', supportGroupRows));
|
|
@@ -711,20 +846,46 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
711
846
|
private _buildLinkRow(title: string, uri: string): HTMLElement {
|
|
712
847
|
const row = document.createElement('adw-action-row');
|
|
713
848
|
row.setAttribute('title', title);
|
|
849
|
+
this._makeLinkRow(row, uri);
|
|
850
|
+
return row;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* `AdwLinkRow`: `adw_link_row_init` + `adw_link_row_set_uri`. The `link` class and the
|
|
855
|
+
* LINK state flag, accessible role LINK, the unescaped URI as tooltip, and a suffix glyph
|
|
856
|
+
* that says "leaves the app" (an envelope for `mailto:`) instead of a navigation chevron.
|
|
857
|
+
* Activation is `_activateLink`, which emits `activate-link` and then opens the URI.
|
|
858
|
+
*/
|
|
859
|
+
private _makeLinkRow(row: HTMLElement, uri: string): void {
|
|
714
860
|
row.setAttribute('activatable', '');
|
|
715
|
-
row.
|
|
861
|
+
row.setAttribute('role', 'link');
|
|
862
|
+
row.classList.add('link', 'adw-about-dialog-link-row');
|
|
863
|
+
const tooltip = linkRowTooltip(uri);
|
|
864
|
+
if (tooltip !== null) row.setAttribute('tooltip-text', tooltip);
|
|
716
865
|
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
866
|
+
const icon = createGtkImage(linkRowIconName(uri), 'adw-about-dialog-link-icon');
|
|
867
|
+
icon.setAttribute('slot', 'suffix');
|
|
868
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
869
|
+
row.appendChild(icon);
|
|
720
870
|
|
|
721
871
|
row.addEventListener('activated', () => this._activateLink(uri));
|
|
722
|
-
|
|
723
|
-
return row;
|
|
724
872
|
}
|
|
725
873
|
|
|
726
874
|
private _buildCreditsPage(sections: ReturnType<typeof creditsSections>): HTMLElement {
|
|
727
|
-
|
|
875
|
+
return this._buildCreditGroupsPage('credits', ADW_ABOUT_DIALOG_LABELS.creditsPage, sections);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private _buildAcknowledgementsPage(sections: ReturnType<typeof acknowledgementSections>): HTMLElement {
|
|
879
|
+
return this._buildCreditGroupsPage('acknowledgements', ADW_ABOUT_DIALOG_LABELS.acknowledgementsPage, sections);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Credits and Acknowledgements are one structure: a group per section, a row per person. */
|
|
883
|
+
private _buildCreditGroupsPage(
|
|
884
|
+
tag: string,
|
|
885
|
+
title: string,
|
|
886
|
+
sections: ReturnType<typeof creditsSections>,
|
|
887
|
+
): HTMLElement {
|
|
888
|
+
const { page, body } = this._buildPage(tag, title, false);
|
|
728
889
|
|
|
729
890
|
for (const section of sections) {
|
|
730
891
|
const rows = section.people.map((person) => this._buildCreditRow(person));
|
|
@@ -746,12 +907,7 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
746
907
|
|
|
747
908
|
if (person.link !== null) {
|
|
748
909
|
const uri = person.uri ?? '';
|
|
749
|
-
|
|
750
|
-
row.classList.add('adw-about-dialog-link-row');
|
|
751
|
-
const chevron = createGtkImage('go-next', 'adw-about-dialog-chevron');
|
|
752
|
-
chevron.setAttribute('slot', 'suffix');
|
|
753
|
-
row.appendChild(chevron);
|
|
754
|
-
row.addEventListener('activated', () => this._activateLink(uri));
|
|
910
|
+
this._makeLinkRow(row, uri);
|
|
755
911
|
}
|
|
756
912
|
|
|
757
913
|
return row;
|
|
@@ -759,36 +915,84 @@ export class AdwAboutDialog extends WidgetElement {
|
|
|
759
915
|
|
|
760
916
|
private _buildLegalPage(): HTMLElement {
|
|
761
917
|
const { page, body } = this._buildPage('legal', ADW_ABOUT_DIALOG_LABELS.legalPage, false);
|
|
918
|
+
const input = this._legalInput();
|
|
919
|
+
const sections = legalSections(input);
|
|
920
|
+
const hasDefault = legalSectionVisible(input.copyright, input.licenseType, input.license);
|
|
921
|
+
|
|
922
|
+
for (const [index, section] of sections.entries()) {
|
|
923
|
+
if (section.title !== null) {
|
|
924
|
+
const heading = document.createElement('p');
|
|
925
|
+
heading.className = 'adw-about-dialog-legal-title';
|
|
926
|
+
heading.textContent = section.title;
|
|
927
|
+
body.appendChild(heading);
|
|
928
|
+
}
|
|
762
929
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
930
|
+
// `license-url` is the one input libadwaita does not have: the DEFAULT section's custom
|
|
931
|
+
// text becomes a link to it.
|
|
932
|
+
const label = document.createElement('p');
|
|
933
|
+
label.className = 'adw-about-dialog-license';
|
|
934
|
+
if (index === 0 && hasDefault && this.licenseUrl && input.licenseType === GTK_LICENSE.CUSTOM) {
|
|
935
|
+
this._renderLegalLabel(label, this.copyright);
|
|
936
|
+
if (this.copyright.length > 0) label.append('\n\n');
|
|
937
|
+
label.append(this._buildLicenseUrlLink());
|
|
938
|
+
} else {
|
|
939
|
+
this._renderLegalLabel(label, section.markup);
|
|
940
|
+
}
|
|
941
|
+
if (section.markup.length > 0) body.appendChild(label);
|
|
768
942
|
}
|
|
769
943
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
944
|
+
return page;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/** The custom licence text as a link to `license-url`. Cancelable like every other link. */
|
|
948
|
+
private _buildLicenseUrlLink(): HTMLElement {
|
|
949
|
+
const url = this.licenseUrl;
|
|
950
|
+
const link = document.createElement('a');
|
|
951
|
+
link.href = url;
|
|
952
|
+
link.rel = 'noopener noreferrer';
|
|
953
|
+
link.textContent = this.license;
|
|
954
|
+
link.addEventListener('click', (event) => {
|
|
955
|
+
event.preventDefault();
|
|
956
|
+
this._activateLink(url);
|
|
957
|
+
});
|
|
958
|
+
return link;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* A legal label's markup: Pango markup, with the `<a href>` GtkLabel adds on top — the
|
|
963
|
+
* stock warranty line is one. Each link raises the cancelable `activate-link`. Only
|
|
964
|
+
* `http`, `https` and `mailto` targets become links; any other is drawn as its text,
|
|
965
|
+
* since the default action hands the URI to `window.open`.
|
|
966
|
+
*/
|
|
967
|
+
private _renderLegalLabel(target: HTMLElement, markup: string): void {
|
|
968
|
+
target.replaceChildren();
|
|
969
|
+
// `\x22` is `"`: an odd number of literal quotes derails check-adwaita-connect-rebind's masker.
|
|
970
|
+
const anchor = /<a\s+href="([^\x22]*)">([\s\S]*?)<\/a>/g;
|
|
971
|
+
let last = 0;
|
|
972
|
+
const text = (value: string) => {
|
|
973
|
+
if (value.length === 0) return;
|
|
974
|
+
const span = document.createElement('span');
|
|
975
|
+
renderPangoMarkup(span, value);
|
|
976
|
+
target.appendChild(span);
|
|
977
|
+
};
|
|
978
|
+
for (const match of markup.matchAll(anchor)) {
|
|
979
|
+
text(markup.slice(last, match.index));
|
|
980
|
+
last = match.index + match[0].length;
|
|
981
|
+
|
|
982
|
+
const uri = match[1]!.replace(/&/g, '&');
|
|
983
|
+
const link = document.createElement('a');
|
|
984
|
+
renderPangoMarkup(link, match[2]!);
|
|
985
|
+
if (/^(https?:|mailto:)/i.test(uri)) {
|
|
986
|
+
link.href = uri;
|
|
778
987
|
link.rel = 'noopener noreferrer';
|
|
779
|
-
link.textContent = this.license;
|
|
780
988
|
link.addEventListener('click', (event) => {
|
|
781
989
|
event.preventDefault();
|
|
782
|
-
this._activateLink(
|
|
990
|
+
this._activateLink(uri);
|
|
783
991
|
});
|
|
784
|
-
license.appendChild(link);
|
|
785
|
-
} else {
|
|
786
|
-
license.textContent = this.license;
|
|
787
992
|
}
|
|
788
|
-
|
|
993
|
+
target.appendChild(link);
|
|
789
994
|
}
|
|
790
|
-
|
|
791
|
-
return page;
|
|
995
|
+
text(markup.slice(last));
|
|
792
996
|
}
|
|
793
997
|
|
|
794
998
|
/** Show the header title only once the main page is scrolled (AdwAboutDialog). */
|
|
@@ -121,7 +121,8 @@ export class AdwPreferencesRow extends TooltipElement {
|
|
|
121
121
|
bindSlottedChildren(this, [{ name: 'child', into: this._contentEl }]).install(this._titleEl, this._contentEl);
|
|
122
122
|
bindEmptySections(this._contentEl);
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
// An author-set role wins: `AdwLinkRow` is a row whose accessible role is LINK.
|
|
125
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'listitem');
|
|
125
126
|
this._render();
|
|
126
127
|
}
|
|
127
128
|
|
package/src/glib.spec.ts
CHANGED
|
@@ -3,15 +3,24 @@
|
|
|
3
3
|
// that stops being exported fails here.
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
GLIB_LANGUAGE_TAG_VECTORS,
|
|
6
7
|
GLIB_PATH_VECTORS,
|
|
7
8
|
GLIB_TIMER_VECTORS,
|
|
9
|
+
GLIB_UNICHAR_VECTORS,
|
|
8
10
|
GLIB_URI_VECTORS,
|
|
11
|
+
GLIB_VARIANT_VECTORS,
|
|
12
|
+
driveGLibLanguageTagVectors,
|
|
9
13
|
driveGLibPathVectors,
|
|
10
14
|
driveGLibTimerVectors,
|
|
15
|
+
driveGLibUnicharVectors,
|
|
11
16
|
driveGLibUriVectors,
|
|
17
|
+
driveGLibVariantVectors,
|
|
18
|
+
type GLibLanguageLike,
|
|
12
19
|
type GLibPathsLike,
|
|
13
20
|
type GLibTimersLike,
|
|
21
|
+
type GLibUnicharLike,
|
|
14
22
|
type GLibUriLike,
|
|
23
|
+
type GLibVariantLike,
|
|
15
24
|
} from '@gjsify/adwaita-core/conformance';
|
|
16
25
|
import { describe, expect, it } from '@gjsify/unit';
|
|
17
26
|
|
|
@@ -46,11 +55,64 @@ export const GLibTest = async () => {
|
|
|
46
55
|
{ describe, it, expect },
|
|
47
56
|
GLIB_URI_VECTORS,
|
|
48
57
|
);
|
|
58
|
+
await driveGLibVariantVectors(
|
|
59
|
+
{ name: 'adwaita-web', isOracle: false, GLib: GLib as unknown as GLibVariantLike },
|
|
60
|
+
{ describe, it, expect },
|
|
61
|
+
GLIB_VARIANT_VECTORS,
|
|
62
|
+
);
|
|
63
|
+
await driveGLibUnicharVectors(
|
|
64
|
+
{ name: 'adwaita-web', isOracle: false, GLib: GLib as unknown as GLibUnicharLike },
|
|
65
|
+
{ describe, it, expect },
|
|
66
|
+
GLIB_UNICHAR_VECTORS,
|
|
67
|
+
);
|
|
68
|
+
await driveGLibLanguageTagVectors(
|
|
69
|
+
{
|
|
70
|
+
name: 'adwaita-web',
|
|
71
|
+
isOracle: false,
|
|
72
|
+
GLib: GLib as unknown as GLibLanguageLike,
|
|
73
|
+
withTags: (tags, read) => {
|
|
74
|
+
const original = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
|
75
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
76
|
+
value: { languages: tags, language: tags[0] },
|
|
77
|
+
configurable: true,
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
return read();
|
|
81
|
+
} finally {
|
|
82
|
+
if (original) Object.defineProperty(globalThis, 'navigator', original);
|
|
83
|
+
else delete (globalThis as { navigator?: unknown }).navigator;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{ describe, it, expect },
|
|
88
|
+
GLIB_LANGUAGE_TAG_VECTORS,
|
|
89
|
+
);
|
|
49
90
|
await describe('adwaita-web: GLib door', async () => {
|
|
50
91
|
await it('answers the host-bound path questions with the root and no data dirs', () => {
|
|
51
92
|
expect(GLib.get_current_dir()).toBe('/');
|
|
52
93
|
expect(GLib.get_system_data_dirs().length).toBe(0);
|
|
53
94
|
});
|
|
95
|
+
await it('answers C alone where the host names no language', () => {
|
|
96
|
+
const original = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
|
97
|
+
Object.defineProperty(globalThis, 'navigator', { value: undefined, configurable: true });
|
|
98
|
+
try {
|
|
99
|
+
expect(GLib.get_language_names().join(',')).toBe('C');
|
|
100
|
+
} finally {
|
|
101
|
+
if (original) Object.defineProperty(globalThis, 'navigator', original);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
await it('falls back to navigator.language when languages is empty', () => {
|
|
105
|
+
const original = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
|
106
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
107
|
+
value: { languages: [], language: 'fr-CA' },
|
|
108
|
+
configurable: true,
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
expect(GLib.get_language_names().join(',')).toBe('fr_CA,fr,C');
|
|
112
|
+
} finally {
|
|
113
|
+
if (original) Object.defineProperty(globalThis, 'navigator', original);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
54
116
|
await it('has a vector row for every vector', () => {
|
|
55
117
|
expect(GLIB_TIMER_VECTORS.length > 0 && GLIB_PATH_VECTORS.length > 0).toBe(true);
|
|
56
118
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// What the browser knows of the user's languages, for `GLib.get_language_names()`: the ordered
|
|
2
|
+
// `navigator.languages`, or `navigator.language` where a host has only that. Read at CALL time, never
|
|
3
|
+
// at import — the user can change the list while the page is open, and a module may be evaluated where
|
|
4
|
+
// there is no `navigator` at all (SSR, a worker without one).
|
|
5
|
+
|
|
6
|
+
export function hostLanguageTags(): string[] {
|
|
7
|
+
const host = typeof navigator === 'undefined' ? undefined : navigator;
|
|
8
|
+
if (host?.languages && host.languages.length > 0) return [...host.languages];
|
|
9
|
+
return host?.language ? [host.language] : [];
|
|
10
|
+
}
|
package/src/namespace/glib.ts
CHANGED
|
@@ -7,24 +7,34 @@
|
|
|
7
7
|
// in `glib-paths.ts` of the core; the host has no filesystem, so `get_current_dir()` is `"/"` and
|
|
8
8
|
// `get_system_data_dirs()` is `[]`. A port that knows better replaces the member in its own file.
|
|
9
9
|
//
|
|
10
|
-
// `
|
|
10
|
+
// `get_language_names` is the core's `glib-language.ts` over the host's language tags
|
|
11
|
+
// (`language-host.ts`: `navigator.languages`), read when it is CALLED. The tag rule is in the core's header.
|
|
12
|
+
//
|
|
13
|
+
// `uri_escape_string` is the core's `glib-uri.ts`, a pure function over UTF-8 bytes;
|
|
14
|
+
// `unichar_isalpha` is `glib-unichar.ts`, a generated table (no `\p{L}`: no ICU on NativeScript).
|
|
11
15
|
//
|
|
12
16
|
// `Bytes` and `Error` are the ADR 0100 subset a `Gio.File` reads and fails with (`get_data`,
|
|
13
17
|
// `matches`, `code`, `message`), re-exported from the core's `glib-bytes.ts` / `glib-error.ts`.
|
|
14
18
|
//
|
|
19
|
+
// `Variant` is the ADR 0118 subset (`b i u d s` and arrays of them) `Gio.Settings` stores array
|
|
20
|
+
// keys with, re-exported from the core's `glib-variant.ts`.
|
|
21
|
+
//
|
|
15
22
|
// A TRUE SUBSET: everything else in GLib (`MainLoop`, `Source`, `timeout_add_seconds`,
|
|
16
23
|
// `get_home_dir`, …) is absent, and the `gi://GLib` arm refuses it by name.
|
|
17
24
|
|
|
18
|
-
export { GLibBytes as Bytes, GLibError as Error } from '@gjsify/adwaita-core';
|
|
25
|
+
export { GLibBytes as Bytes, GLibError as Error, GLibVariant as Variant } from '@gjsify/adwaita-core';
|
|
19
26
|
|
|
20
27
|
import {
|
|
21
28
|
buildFilenamev,
|
|
22
29
|
createGLibTimers,
|
|
23
30
|
getCurrentDir,
|
|
24
31
|
getSystemDataDirs,
|
|
32
|
+
languageNamesFromTags,
|
|
25
33
|
MAXUINT32 as GLIB_MAXUINT32,
|
|
34
|
+
unicharIsalpha,
|
|
26
35
|
uriEscapeString,
|
|
27
36
|
} from '@gjsify/adwaita-core';
|
|
37
|
+
import { hostLanguageTags } from '../language-host.js';
|
|
28
38
|
|
|
29
39
|
const glib = createGLibTimers();
|
|
30
40
|
|
|
@@ -43,4 +53,6 @@ export const MAXUINT32 = GLIB_MAXUINT32;
|
|
|
43
53
|
export const build_filenamev = buildFilenamev;
|
|
44
54
|
export const get_current_dir = getCurrentDir;
|
|
45
55
|
export const get_system_data_dirs = getSystemDataDirs;
|
|
56
|
+
export const get_language_names = (): string[] => languageNamesFromTags(hostLanguageTags());
|
|
46
57
|
export const uri_escape_string = uriEscapeString;
|
|
58
|
+
export const unichar_isalpha = unicharIsalpha;
|