selfstore 1.8.3 → 1.8.5
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 +1 -1
- package/dist/advanced.js +1 -1
- package/dist/index.js +1 -1
- package/dist/widgets.d.ts +87 -5
- package/dist/widgets.js +287 -16
- package/dist/widgets.js.map +1 -1
- package/llms.txt +16 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ too, as subpaths of the same install:
|
|
|
162
162
|
|---|---|---|
|
|
163
163
|
| `selfstore` | the simple store + backup files | almost always: start here |
|
|
164
164
|
| `selfstore/flows` | the user journeys (connect a destination, share panel, join an invitation) as headless, tested state machines | you are building the connect/share/join UI and want the ordering and failure rules - one popup per gesture, password proven before attach, merge by default - already right |
|
|
165
|
-
| `selfstore/widgets` | the same journeys as drop-in web components (`<selfstore-connect>`, `<selfstore-gate>`, `<selfstore-share>`, `<selfstore-join>`) - framework-free, themable via CSS custom properties and `::part()`, reworded/localized via a `labels` map | you want the screens ready-made and your app's look on top |
|
|
165
|
+
| `selfstore/widgets` | the same journeys as drop-in web components (`<selfstore-connect>`, `<selfstore-gate>`, `<selfstore-destination>`, `<selfstore-share>`, `<selfstore-join>`) - framework-free, themable via CSS custom properties and `::part()`, reworded/localized via a `labels` map | you want the screens ready-made and your app's look on top |
|
|
166
166
|
| `selfstore/advanced` | the pull-model store (`createLocalStore`), custom `BackupTarget`s, caches, headless status derivation, functional codec | your state lives in its own reactive model (Svelte runes, Redux), or you are writing a destination (S3, your KV...) |
|
|
167
167
|
| `selfstore/groups` | passwordless group encryption: per-member identities, sealed envelopes, signed membership manifests | several PEOPLE share one encrypted store without a shared password ([PEERS.md](./PEERS.md)) |
|
|
168
168
|
| `selfstore/sync` | the bare merge engine (HLC + per-collection strategies, no CRDT runtime) | you only want the algorithm inside your own persistence ([SYNC docs in SPEC.md](./SPEC.md)) |
|
package/dist/advanced.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { RESERVED_STORE_MODES, createLocalStore, deriveStatus, gisDriveAuth, indexedDbCache, isLockableCache, isReservedStoreMode, memoryCache } from './chunk-ELDSPUTS.js';
|
|
2
|
-
export { datedName, pickFromDisk, saveToDisk } from './chunk-DCEXWSM6.js';
|
|
3
2
|
import './chunk-SKWHKM25.js';
|
|
3
|
+
export { datedName, pickFromDisk, saveToDisk } from './chunk-DCEXWSM6.js';
|
|
4
4
|
import { openExisting, isOpenSupported, isSupported, fromSession as fromSession$1, connect as connect$1, fromSession3, connect3, peer, fromSession2, connect2 } from './chunk-TKWU52LK.js';
|
|
5
5
|
import { FILE_ID_KEY, account, renameBackup, deleteBackup, createBackup, listBackups, findOrCreateOwnFile, adopt, preview, fromSession, connect } from './chunk-J5MFXJU6.js';
|
|
6
6
|
export { RESERVED_COLLECTION_PREFIX, checkPasswordPolicy, exportSnapshot, importSnapshot, inspect, isEncrypted } from './chunk-MGDXR3RB.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-ELDSPUTS.js';
|
|
2
2
|
export { gisDriveAuth } from './chunk-ELDSPUTS.js';
|
|
3
|
+
import './chunk-SKWHKM25.js';
|
|
3
4
|
import { saveToDisk } from './chunk-DCEXWSM6.js';
|
|
4
5
|
export { pickFromDisk, saveToDisk } from './chunk-DCEXWSM6.js';
|
|
5
|
-
import './chunk-SKWHKM25.js';
|
|
6
6
|
import { fromSession as fromSession$1, fromSession2, isSupported, connect3, connect2, connect } from './chunk-TKWU52LK.js';
|
|
7
7
|
export { hasDesktopFiles, useDesktopFiles } from './chunk-TKWU52LK.js';
|
|
8
8
|
import { fromSession, connect as connect$1 } from './chunk-J5MFXJU6.js';
|
package/dist/widgets.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as StoreLike, C as ConnectTargets, a as ConnectFlowOptions, b as ConnectKind, c as ConnectFlow, d as ShareEngine, e as ShareLevel, f as ShareFlow, J as JoinEngine, g as JoinFlow, R as ReplicaFlow } from './flows-1EntUZ91.js';
|
|
1
|
+
import { F as FlowHost, S as StoreLike, C as ConnectTargets, a as ConnectFlowOptions, b as ConnectKind, c as ConnectFlow, d as ShareEngine, e as ShareLevel, f as ShareFlow, J as JoinEngine, g as JoinFlow, R as ReplicaFlow } from './flows-1EntUZ91.js';
|
|
2
2
|
import { f as BackupsManager } from './manager-DZ1c_aSS.js';
|
|
3
3
|
import './store-BiQ-BuVl.js';
|
|
4
4
|
import './cache-ilsK3Tki.js';
|
|
@@ -22,6 +22,10 @@ declare abstract class FlowWidget extends HTMLElement {
|
|
|
22
22
|
protected packs(): Record<string, WidgetLabels>;
|
|
23
23
|
/** Build the current view into the given container. */
|
|
24
24
|
protected abstract view(into: HTMLElement): void;
|
|
25
|
+
/** Follow an engine: rerender on every notification, replacing any previous
|
|
26
|
+
* subscription. A no-op before connection and on a null host, so a host may
|
|
27
|
+
* assign the store from a reactive effect that runs more than once. */
|
|
28
|
+
protected follow(host: FlowHost | null): void;
|
|
25
29
|
constructor();
|
|
26
30
|
/** Partial override map, merged over the EN defaults (also the i18n hook). */
|
|
27
31
|
get labels(): WidgetLabels;
|
|
@@ -523,10 +527,88 @@ declare class SelfstoreGateElement extends FlowWidget {
|
|
|
523
527
|
protected view(into: HTMLElement): void;
|
|
524
528
|
}
|
|
525
529
|
|
|
530
|
+
/**
|
|
531
|
+
* <selfstore-destination>: where the data is kept, once it has a home.
|
|
532
|
+
*
|
|
533
|
+
* The counterpart of <selfstore-gate>. The gate asks the question before there
|
|
534
|
+
* is an answer; this panel states the answer and carries the gestures that act
|
|
535
|
+
* on it - export a copy, change destination, stop saving here.
|
|
536
|
+
*
|
|
537
|
+
* const el = document.querySelector('selfstore-destination');
|
|
538
|
+
* el.store = store;
|
|
539
|
+
* el.targets = { drive: driveAuth, file: true }; // enables "change"
|
|
540
|
+
*
|
|
541
|
+
* It DOES the work rather than asking the host to. Exporting a copy, detaching
|
|
542
|
+
* and re-connecting are all the engine's own operations, so a panel that only
|
|
543
|
+
* emitted intentions would leave the same two hundred lines in every app - the
|
|
544
|
+
* very lines this widget exists to delete.
|
|
545
|
+
*
|
|
546
|
+
* The one exception is structural, not a choice: **loading a copy back cannot
|
|
547
|
+
* be done from here.** The library holds no records; the app's `apply` does.
|
|
548
|
+
* The engine exposes `exportBlob()` and no import, so the panel asks for that
|
|
549
|
+
* one through `selfstore-destination-action` and the host runs its own import.
|
|
550
|
+
*
|
|
551
|
+
* Two things stay the host's, deliberately: the WORDS (every label is
|
|
552
|
+
* overridable, and a pack ships per language) and the VETO on what is
|
|
553
|
+
* destructive (`confirmAction`, which defaults to proceeding). Neither is
|
|
554
|
+
* customisation - they are the decisions a library must not take for an app.
|
|
555
|
+
*/
|
|
556
|
+
|
|
557
|
+
/** What the panel is about to do, for the host's veto. */
|
|
558
|
+
interface DestinationAction {
|
|
559
|
+
type: 'detach';
|
|
560
|
+
/** The destination's own name, when it has one. */
|
|
561
|
+
label: string | null;
|
|
562
|
+
}
|
|
563
|
+
declare class SelfstoreDestinationElement extends FlowWidget {
|
|
564
|
+
#private;
|
|
565
|
+
protected defaults(): WidgetLabels;
|
|
566
|
+
protected packs(): Record<string, WidgetLabels>;
|
|
567
|
+
constructor();
|
|
568
|
+
/** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
|
|
569
|
+
get store(): StoreLike | null;
|
|
570
|
+
set store(v: StoreLike | null);
|
|
571
|
+
/** The destinations offered when the user changes their mind. Given them, the
|
|
572
|
+
* panel runs the connect journey in place; without them the button is absent
|
|
573
|
+
* rather than dead - a host that cannot offer a choice should not show one. */
|
|
574
|
+
get targets(): ConnectTargets | null;
|
|
575
|
+
set targets(v: ConnectTargets | null);
|
|
576
|
+
/** Who holds the destination ("someone@example.com"), when the host knows.
|
|
577
|
+
* For Drive that is `driveTarget.account()`: a brand name is not an address,
|
|
578
|
+
* and several accounts look alike. */
|
|
579
|
+
get account(): string | null;
|
|
580
|
+
set account(v: string | null);
|
|
581
|
+
/** Optional glyph per destination kind, same contract as the connect cards. */
|
|
582
|
+
get icons(): Partial<Record<ConnectKind, string>>;
|
|
583
|
+
set icons(v: Partial<Record<ConnectKind, string>> | null);
|
|
584
|
+
/** Veto on what is destructive. Returns false (or throws) to stop; absent
|
|
585
|
+
* means proceed. The panel never invents its own confirm dialog: an app has
|
|
586
|
+
* its own words and its own modal. */
|
|
587
|
+
get confirmAction(): ((a: DestinationAction) => boolean | Promise<boolean>) | null;
|
|
588
|
+
set confirmAction(fn: ((a: DestinationAction) => boolean | Promise<boolean>) | null);
|
|
589
|
+
connectedCallback(): void;
|
|
590
|
+
private host;
|
|
591
|
+
private wire;
|
|
592
|
+
/** Run a gesture, keeping the panel honest while it is in flight. */
|
|
593
|
+
private run;
|
|
594
|
+
/** A portable copy, on the user's own terms. A closed save dialog wrote
|
|
595
|
+
* nothing, so it must not clear the pending-export nudge nor claim success -
|
|
596
|
+
* that is the download that lies about itself. */
|
|
597
|
+
private exportCopy;
|
|
598
|
+
/** Stop saving to this destination. The local data stays, and so does the
|
|
599
|
+
* backup already written: this detaches, it never deletes. */
|
|
600
|
+
private detach;
|
|
601
|
+
/** The connect journey, in place. Built like the gate builds it, so the
|
|
602
|
+
* destinations, the password step and the cancel path are the library's
|
|
603
|
+
* tested ones rather than a second implementation. */
|
|
604
|
+
private connectView;
|
|
605
|
+
protected view(into: HTMLElement): void;
|
|
606
|
+
}
|
|
607
|
+
|
|
526
608
|
/** Register the elements as <PREFIX-connect>, <PREFIX-share>, <PREFIX-join>,
|
|
527
|
-
* <PREFIX-status>, <PREFIX-backups
|
|
528
|
-
* 'selfstore'). Safe to call twice;
|
|
529
|
-
* elements (browser code only). */
|
|
609
|
+
* <PREFIX-status>, <PREFIX-backups>, <PREFIX-gate> and
|
|
610
|
+
* <PREFIX-destination> (default prefix 'selfstore'). Safe to call twice;
|
|
611
|
+
* throws in environments without custom elements (browser code only). */
|
|
530
612
|
declare function defineSelfstoreWidgets(prefix?: string): void;
|
|
531
613
|
|
|
532
|
-
export { type BackupsAction, FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, type ShareAction, type WebdavPreset, type WidgetLabels, defineSelfstoreWidgets };
|
|
614
|
+
export { type BackupsAction, type DestinationAction, FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreDestinationElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, type ShareAction, type WebdavPreset, type WidgetLabels, defineSelfstoreWidgets };
|
package/dist/widgets.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { saveToDisk, datedName } from './chunk-DCEXWSM6.js';
|
|
1
2
|
import { connectFlow, shareFlow, joinFlow } from './chunk-PNX7AJYO.js';
|
|
2
3
|
import { isOpenSupported } from './chunk-TKWU52LK.js';
|
|
3
4
|
import './chunk-W22V5CRM.js';
|
|
@@ -26,6 +27,10 @@ var defaultIcons = {
|
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
// src/widgets/base.ts
|
|
30
|
+
function hostOf(store) {
|
|
31
|
+
if (!store) return null;
|
|
32
|
+
return "flowHost" in store ? store.flowHost : store;
|
|
33
|
+
}
|
|
29
34
|
var baseStyles = `
|
|
30
35
|
:host {
|
|
31
36
|
display: block;
|
|
@@ -376,6 +381,16 @@ var FlowWidget = class extends HTMLElement {
|
|
|
376
381
|
packs() {
|
|
377
382
|
return {};
|
|
378
383
|
}
|
|
384
|
+
/** Follow an engine: rerender on every notification, replacing any previous
|
|
385
|
+
* subscription. A no-op before connection and on a null host, so a host may
|
|
386
|
+
* assign the store from a reactive effect that runs more than once. */
|
|
387
|
+
follow(host) {
|
|
388
|
+
this.unsub?.();
|
|
389
|
+
this.unsub = null;
|
|
390
|
+
if (!this.isConnected || !host) return;
|
|
391
|
+
this.unsub = host.engine.subscribe(() => this.rerender());
|
|
392
|
+
this.rerender();
|
|
393
|
+
}
|
|
379
394
|
constructor() {
|
|
380
395
|
super();
|
|
381
396
|
this.root = this.attachShadow({ mode: "open" });
|
|
@@ -522,7 +537,7 @@ var EN = {
|
|
|
522
537
|
"connect.webdav.signup": "Create an account",
|
|
523
538
|
"connect.webdav.help.more": "How?",
|
|
524
539
|
"connect.done": "Connected. Your data is saved.",
|
|
525
|
-
"connect.done.manual": "Download mode:
|
|
540
|
+
"connect.done.manual": "Download mode: this browser cannot keep the file open.",
|
|
526
541
|
"error.generic": "That did not work. Check the connection and try again.",
|
|
527
542
|
"error.targetUnavailable": "The destination did not answer. Try again in a moment.",
|
|
528
543
|
"error.authExpired": "Access expired: reconnect to continue.",
|
|
@@ -569,7 +584,7 @@ var FR = {
|
|
|
569
584
|
"connect.password.forgot.warn": "Sans le mot de passe, cette sauvegarde ne peut pas \xEAtre ouverte. Vous pouvez repartir d'une sauvegarde vide : l'ancienne sera perdue d\xE9finitivement.",
|
|
570
585
|
"connect.password.forgot.confirm": "Remplacer la sauvegarde",
|
|
571
586
|
"connect.password.forgot.back": "Retour",
|
|
572
|
-
"connect.conflict.title": "
|
|
587
|
+
"connect.conflict.title": "Une sauvegarde existe d\xE9j\xE0 ici",
|
|
573
588
|
"connect.conflict.merge": "Tout garder",
|
|
574
589
|
"connect.conflict.merge.sub": "R\xE9unir la sauvegarde et les donn\xE9es de cet appareil",
|
|
575
590
|
"connect.conflict.resume": "Utiliser la sauvegarde",
|
|
@@ -583,9 +598,9 @@ var FR = {
|
|
|
583
598
|
"connect.webdav.signup": "Cr\xE9er un compte",
|
|
584
599
|
"connect.webdav.help.more": "Comment faire ?",
|
|
585
600
|
"connect.done": "Connect\xE9. Vos donn\xE9es sont enregistr\xE9es.",
|
|
586
|
-
"connect.done.manual": "Mode
|
|
601
|
+
"connect.done.manual": "Mode export : ce navigateur ne peut pas garder le fichier ouvert.",
|
|
587
602
|
"error.generic": "Cela n'a pas fonctionn\xE9. V\xE9rifiez la connexion et r\xE9essayez.",
|
|
588
|
-
"error.targetUnavailable": "La
|
|
603
|
+
"error.targetUnavailable": "La sauvegarde ne r\xE9pond pas. R\xE9essayez dans un instant.",
|
|
589
604
|
"error.authExpired": "Acc\xE8s expir\xE9 : reconnectez-vous pour continuer.",
|
|
590
605
|
"error.decryptFailed": "Cette sauvegarde n'a pas pu \xEAtre ouverte avec ce mot de passe.",
|
|
591
606
|
"error.badFormat": "Ce fichier ne ressemble pas \xE0 une sauvegarde lisible.",
|
|
@@ -1645,7 +1660,7 @@ var EN3 = {
|
|
|
1645
1660
|
};
|
|
1646
1661
|
var FR3 = {
|
|
1647
1662
|
"join.previewing": "Lecture de l'invitation...",
|
|
1648
|
-
"join.title": "Vous \
|
|
1663
|
+
"join.title": "Vous avez re\xE7u une invitation",
|
|
1649
1664
|
"join.from": "Partag\xE9 par {from}",
|
|
1650
1665
|
"join.level.read": "Vous pourrez consulter.",
|
|
1651
1666
|
"join.level.write": "Vous pourrez consulter et modifier.",
|
|
@@ -1949,18 +1964,10 @@ var SelfstoreStatusElement = class extends FlowWidget {
|
|
|
1949
1964
|
this.wire();
|
|
1950
1965
|
}
|
|
1951
1966
|
host() {
|
|
1952
|
-
|
|
1953
|
-
if (!s) return null;
|
|
1954
|
-
return "flowHost" in s ? s.flowHost : s;
|
|
1967
|
+
return hostOf(this.#store);
|
|
1955
1968
|
}
|
|
1956
1969
|
wire() {
|
|
1957
|
-
|
|
1958
|
-
this.unsub?.();
|
|
1959
|
-
this.unsub = null;
|
|
1960
|
-
const host = this.host();
|
|
1961
|
-
if (!host) return;
|
|
1962
|
-
this.unsub = host.engine.subscribe(() => this.rerender());
|
|
1963
|
-
this.rerender();
|
|
1970
|
+
this.follow(this.host());
|
|
1964
1971
|
}
|
|
1965
1972
|
view(into) {
|
|
1966
1973
|
const host = this.host();
|
|
@@ -3512,6 +3519,269 @@ var SelfstoreGateElement = class extends FlowWidget {
|
|
|
3512
3519
|
}
|
|
3513
3520
|
};
|
|
3514
3521
|
|
|
3522
|
+
// src/widgets/destination.ts
|
|
3523
|
+
var EN7 = {
|
|
3524
|
+
"destination.heading": "Where your data is kept",
|
|
3525
|
+
"destination.kind.drive": "Google Drive",
|
|
3526
|
+
"destination.kind.file": "A file on this device",
|
|
3527
|
+
"destination.kind.webdav": "Your own server",
|
|
3528
|
+
"destination.kind.s3": "Your own bucket",
|
|
3529
|
+
"destination.kind.device": "This device only",
|
|
3530
|
+
"destination.encrypted": "Password-protected",
|
|
3531
|
+
"destination.export": "Export a copy",
|
|
3532
|
+
"destination.exported": "The copy is written.",
|
|
3533
|
+
"destination.exportCancelled": "Nothing was written.",
|
|
3534
|
+
"destination.restore": "Restore a copy",
|
|
3535
|
+
"destination.change": "Change destination",
|
|
3536
|
+
"destination.detach": "Stop saving here",
|
|
3537
|
+
"destination.detached": "Stopped. Your data stays on this device.",
|
|
3538
|
+
"destination.back": "Back",
|
|
3539
|
+
"error.generic": "Something went wrong."
|
|
3540
|
+
};
|
|
3541
|
+
var FR7 = {
|
|
3542
|
+
"destination.heading": "O\xF9 vos donn\xE9es sont enregistr\xE9es",
|
|
3543
|
+
"destination.kind.file": "Un fichier sur cet appareil",
|
|
3544
|
+
"destination.kind.webdav": "Votre serveur",
|
|
3545
|
+
"destination.kind.s3": "Votre bucket",
|
|
3546
|
+
"destination.kind.device": "Cet appareil seulement",
|
|
3547
|
+
"destination.encrypted": "Prot\xE9g\xE9e par un mot de passe",
|
|
3548
|
+
"destination.export": "Exporter une copie",
|
|
3549
|
+
"destination.exported": "La copie est \xE9crite.",
|
|
3550
|
+
"destination.exportCancelled": "Rien n'a \xE9t\xE9 \xE9crit.",
|
|
3551
|
+
// "Restaurer", pas "charger" : le premier dit qu'on REVIENT a un etat, le
|
|
3552
|
+
// second pourrait ajouter. Face a "Exporter une copie", la paire se lit
|
|
3553
|
+
// comme l'aller-retour qu'elle est.
|
|
3554
|
+
"destination.restore": "Restaurer une copie",
|
|
3555
|
+
"destination.change": "Changer de sauvegarde",
|
|
3556
|
+
"destination.detach": "Ne plus enregistrer ici",
|
|
3557
|
+
"destination.detached": "Arr\xEAt\xE9. Vos donn\xE9es restent sur cet appareil.",
|
|
3558
|
+
"destination.back": "Retour",
|
|
3559
|
+
"error.generic": "Quelque chose n'a pas fonctionn\xE9."
|
|
3560
|
+
};
|
|
3561
|
+
var DESTINATION_STYLES = `
|
|
3562
|
+
[part~='destination-card'] { align-items: flex-start; }
|
|
3563
|
+
[part~='destination-actions'] {
|
|
3564
|
+
display: flex;
|
|
3565
|
+
flex-wrap: wrap;
|
|
3566
|
+
gap: var(--_gap);
|
|
3567
|
+
}
|
|
3568
|
+
[part~='destination-heading'] {
|
|
3569
|
+
margin: 0;
|
|
3570
|
+
font-size: 1rem;
|
|
3571
|
+
font-weight: 600;
|
|
3572
|
+
}
|
|
3573
|
+
`;
|
|
3574
|
+
function siblingTag(own, name) {
|
|
3575
|
+
const suffix = "-destination";
|
|
3576
|
+
return own.endsWith(suffix) ? `${own.slice(0, -suffix.length)}-${name}` : `selfstore-${name}`;
|
|
3577
|
+
}
|
|
3578
|
+
var SelfstoreDestinationElement = class extends FlowWidget {
|
|
3579
|
+
#store = null;
|
|
3580
|
+
#targets = null;
|
|
3581
|
+
#account = null;
|
|
3582
|
+
#icons = {};
|
|
3583
|
+
#confirm = null;
|
|
3584
|
+
#changing = false;
|
|
3585
|
+
#busy = false;
|
|
3586
|
+
#message = null;
|
|
3587
|
+
defaults() {
|
|
3588
|
+
return EN7;
|
|
3589
|
+
}
|
|
3590
|
+
packs() {
|
|
3591
|
+
return { fr: FR7 };
|
|
3592
|
+
}
|
|
3593
|
+
constructor() {
|
|
3594
|
+
super();
|
|
3595
|
+
this.root.append(h("style", {}, DESTINATION_STYLES));
|
|
3596
|
+
}
|
|
3597
|
+
/** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
|
|
3598
|
+
get store() {
|
|
3599
|
+
return this.#store;
|
|
3600
|
+
}
|
|
3601
|
+
set store(v) {
|
|
3602
|
+
if (v === this.#store) return;
|
|
3603
|
+
this.#store = v;
|
|
3604
|
+
this.wire();
|
|
3605
|
+
}
|
|
3606
|
+
/** The destinations offered when the user changes their mind. Given them, the
|
|
3607
|
+
* panel runs the connect journey in place; without them the button is absent
|
|
3608
|
+
* rather than dead - a host that cannot offer a choice should not show one. */
|
|
3609
|
+
get targets() {
|
|
3610
|
+
return this.#targets;
|
|
3611
|
+
}
|
|
3612
|
+
set targets(v) {
|
|
3613
|
+
this.#targets = v;
|
|
3614
|
+
this.rerender();
|
|
3615
|
+
}
|
|
3616
|
+
/** Who holds the destination ("someone@example.com"), when the host knows.
|
|
3617
|
+
* For Drive that is `driveTarget.account()`: a brand name is not an address,
|
|
3618
|
+
* and several accounts look alike. */
|
|
3619
|
+
get account() {
|
|
3620
|
+
return this.#account;
|
|
3621
|
+
}
|
|
3622
|
+
set account(v) {
|
|
3623
|
+
this.#account = v || null;
|
|
3624
|
+
this.rerender();
|
|
3625
|
+
}
|
|
3626
|
+
/** Optional glyph per destination kind, same contract as the connect cards. */
|
|
3627
|
+
get icons() {
|
|
3628
|
+
return this.#icons;
|
|
3629
|
+
}
|
|
3630
|
+
set icons(v) {
|
|
3631
|
+
this.#icons = v ?? {};
|
|
3632
|
+
this.rerender();
|
|
3633
|
+
}
|
|
3634
|
+
/** Veto on what is destructive. Returns false (or throws) to stop; absent
|
|
3635
|
+
* means proceed. The panel never invents its own confirm dialog: an app has
|
|
3636
|
+
* its own words and its own modal. */
|
|
3637
|
+
get confirmAction() {
|
|
3638
|
+
return this.#confirm;
|
|
3639
|
+
}
|
|
3640
|
+
set confirmAction(fn) {
|
|
3641
|
+
this.#confirm = fn;
|
|
3642
|
+
}
|
|
3643
|
+
connectedCallback() {
|
|
3644
|
+
super.connectedCallback();
|
|
3645
|
+
this.wire();
|
|
3646
|
+
}
|
|
3647
|
+
host() {
|
|
3648
|
+
return hostOf(this.#store);
|
|
3649
|
+
}
|
|
3650
|
+
wire() {
|
|
3651
|
+
this.follow(this.host());
|
|
3652
|
+
}
|
|
3653
|
+
/** Run a gesture, keeping the panel honest while it is in flight. */
|
|
3654
|
+
async run(job) {
|
|
3655
|
+
this.#busy = true;
|
|
3656
|
+
this.#message = null;
|
|
3657
|
+
this.rerender();
|
|
3658
|
+
try {
|
|
3659
|
+
this.#message = await job();
|
|
3660
|
+
} catch {
|
|
3661
|
+
this.#message = this.t("error.generic");
|
|
3662
|
+
} finally {
|
|
3663
|
+
this.#busy = false;
|
|
3664
|
+
this.rerender();
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
/** A portable copy, on the user's own terms. A closed save dialog wrote
|
|
3668
|
+
* nothing, so it must not clear the pending-export nudge nor claim success -
|
|
3669
|
+
* that is the download that lies about itself. */
|
|
3670
|
+
exportCopy() {
|
|
3671
|
+
const host = this.host();
|
|
3672
|
+
if (!host) return Promise.resolve();
|
|
3673
|
+
return this.run(async () => {
|
|
3674
|
+
const blob = await host.engine.exportBlob();
|
|
3675
|
+
const written = await saveToDisk(blob, datedName(host.backupName));
|
|
3676
|
+
if (!written) return this.t("destination.exportCancelled");
|
|
3677
|
+
host.engine.markDownloaded();
|
|
3678
|
+
return this.t("destination.exported");
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
/** Stop saving to this destination. The local data stays, and so does the
|
|
3682
|
+
* backup already written: this detaches, it never deletes. */
|
|
3683
|
+
detach(label) {
|
|
3684
|
+
const host = this.host();
|
|
3685
|
+
if (!host) return Promise.resolve();
|
|
3686
|
+
return this.run(async () => {
|
|
3687
|
+
try {
|
|
3688
|
+
if (this.#confirm && !await this.#confirm({ type: "detach", label })) return null;
|
|
3689
|
+
} catch {
|
|
3690
|
+
return null;
|
|
3691
|
+
}
|
|
3692
|
+
await host.engine.detachTarget();
|
|
3693
|
+
this.emit("selfstore-destination-detached");
|
|
3694
|
+
return this.t("destination.detached");
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
/** The connect journey, in place. Built like the gate builds it, so the
|
|
3698
|
+
* destinations, the password step and the cancel path are the library's
|
|
3699
|
+
* tested ones rather than a second implementation. */
|
|
3700
|
+
connectView(into) {
|
|
3701
|
+
const el = document.createElement(
|
|
3702
|
+
siblingTag(this.localName, "connect")
|
|
3703
|
+
);
|
|
3704
|
+
el.store = this.#store;
|
|
3705
|
+
el.targets = this.#targets;
|
|
3706
|
+
el.icons = this.#icons;
|
|
3707
|
+
el.labels = this.labels;
|
|
3708
|
+
const done = () => {
|
|
3709
|
+
this.#changing = false;
|
|
3710
|
+
this.rerender();
|
|
3711
|
+
};
|
|
3712
|
+
el.addEventListener("selfstore-connected", done);
|
|
3713
|
+
el.addEventListener("selfstore-cancelled", done);
|
|
3714
|
+
put(
|
|
3715
|
+
into,
|
|
3716
|
+
h(
|
|
3717
|
+
"button",
|
|
3718
|
+
{ part: "link destination-back", type: "button", onclick: done },
|
|
3719
|
+
this.t("destination.back")
|
|
3720
|
+
),
|
|
3721
|
+
el
|
|
3722
|
+
);
|
|
3723
|
+
}
|
|
3724
|
+
view(into) {
|
|
3725
|
+
const host = this.host();
|
|
3726
|
+
if (!host) return;
|
|
3727
|
+
if (this.#changing && this.#targets) return this.connectView(into);
|
|
3728
|
+
const { targetKind, label, encrypted } = host.engine.state;
|
|
3729
|
+
const kind = this.t(`destination.kind.${targetKind}`);
|
|
3730
|
+
const title = kind === `destination.kind.${targetKind}` ? label ?? targetKind : kind;
|
|
3731
|
+
const sub = [this.#account, label && label !== title ? label : null].filter(Boolean).join(" \xB7 ");
|
|
3732
|
+
const icon = this.#icons[targetKind];
|
|
3733
|
+
const status = document.createElement(siblingTag(this.localName, "status"));
|
|
3734
|
+
status.setAttribute("variant", "row");
|
|
3735
|
+
status.labels = this.labels;
|
|
3736
|
+
status.icons = this.#icons;
|
|
3737
|
+
status.store = this.#store;
|
|
3738
|
+
const act = (key, part, onclick) => {
|
|
3739
|
+
const label2 = this.t(key);
|
|
3740
|
+
if (!label2) return null;
|
|
3741
|
+
return h(
|
|
3742
|
+
"button",
|
|
3743
|
+
{ part: `button ${part}`, type: "button", disabled: this.#busy ? "" : null, onclick },
|
|
3744
|
+
label2
|
|
3745
|
+
);
|
|
3746
|
+
};
|
|
3747
|
+
put(
|
|
3748
|
+
into,
|
|
3749
|
+
this.heading("destination-heading", "destination.heading"),
|
|
3750
|
+
h(
|
|
3751
|
+
"div",
|
|
3752
|
+
{ part: "card destination-card" },
|
|
3753
|
+
icon ? h("img", { part: "icon", src: icon, alt: "" }) : null,
|
|
3754
|
+
h(
|
|
3755
|
+
"div",
|
|
3756
|
+
{},
|
|
3757
|
+
h("div", { part: "title" }, title),
|
|
3758
|
+
sub ? h("div", { part: "sub" }, sub) : null,
|
|
3759
|
+
encrypted ? h("div", { part: "sub" }, this.t("destination.encrypted")) : null
|
|
3760
|
+
)
|
|
3761
|
+
),
|
|
3762
|
+
status,
|
|
3763
|
+
h(
|
|
3764
|
+
"div",
|
|
3765
|
+
{ part: "destination-actions" },
|
|
3766
|
+
act("destination.export", "destination-export", () => void this.exportCopy()),
|
|
3767
|
+
// The library holds no records, so loading a copy back is the host's
|
|
3768
|
+
// own gesture - see the note at the top of this file.
|
|
3769
|
+
act(
|
|
3770
|
+
"destination.restore",
|
|
3771
|
+
"destination-restore",
|
|
3772
|
+
() => this.emit("selfstore-destination-action", { action: "restore" })
|
|
3773
|
+
),
|
|
3774
|
+
this.#targets ? act("destination.change", "destination-change", () => {
|
|
3775
|
+
this.#changing = true;
|
|
3776
|
+
this.rerender();
|
|
3777
|
+
}) : null,
|
|
3778
|
+
act("destination.detach", "destination-detach", () => void this.detach(label))
|
|
3779
|
+
),
|
|
3780
|
+
this.#message ? h("div", { part: "sub destination-message", role: "status" }, this.#message) : null
|
|
3781
|
+
);
|
|
3782
|
+
}
|
|
3783
|
+
};
|
|
3784
|
+
|
|
3515
3785
|
// src/entries/widgets.ts
|
|
3516
3786
|
function defineSelfstoreWidgets(prefix = "selfstore") {
|
|
3517
3787
|
const define = (name, ctor) => {
|
|
@@ -3523,8 +3793,9 @@ function defineSelfstoreWidgets(prefix = "selfstore") {
|
|
|
3523
3793
|
define(`${prefix}-status`, SelfstoreStatusElement);
|
|
3524
3794
|
define(`${prefix}-backups`, SelfstoreBackupsElement);
|
|
3525
3795
|
define(`${prefix}-gate`, SelfstoreGateElement);
|
|
3796
|
+
define(`${prefix}-destination`, SelfstoreDestinationElement);
|
|
3526
3797
|
}
|
|
3527
3798
|
|
|
3528
|
-
export { FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, defineSelfstoreWidgets };
|
|
3799
|
+
export { FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreDestinationElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, defineSelfstoreWidgets };
|
|
3529
3800
|
//# sourceMappingURL=widgets.js.map
|
|
3530
3801
|
//# sourceMappingURL=widgets.js.map
|