dsh-session-cloud 0.1.0

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/lib/client.js ADDED
@@ -0,0 +1,1912 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-cloud-sync", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/client/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ apply: () => apply,
25
+ inject: () => inject
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/client/CloudSyncCard.tsx
30
+ var import_react = require("react");
31
+
32
+ // src/client/pure.ts
33
+ function isValidServerUrl(text) {
34
+ if (text === "") return true;
35
+ let url;
36
+ try {
37
+ url = new URL(text);
38
+ } catch {
39
+ return false;
40
+ }
41
+ return url.protocol === "http:" || url.protocol === "https:";
42
+ }
43
+ function isValidDeviceName(text) {
44
+ return text === "" || /^[a-zA-Z0-9-_]{1,32}$/.test(text);
45
+ }
46
+ var MINUTE_MS = 6e4;
47
+ var HOUR_MS = 36e5;
48
+ var DAY_MS = 864e5;
49
+ function relativeTime(at, now) {
50
+ if (at <= 0) return null;
51
+ const diff = Math.max(0, now - at);
52
+ if (diff < MINUTE_MS) return { key: "time.justNow" };
53
+ if (diff < HOUR_MS) return { key: "time.minutesAgo", count: Math.max(1, Math.round(diff / MINUTE_MS)) };
54
+ if (diff < DAY_MS) return { key: "time.hoursAgo", count: Math.max(1, Math.round(diff / HOUR_MS)) };
55
+ return { key: "time.daysAgo", count: Math.max(1, Math.round(diff / DAY_MS)) };
56
+ }
57
+ function projectStatusLine(input) {
58
+ if (input.phase === "unconfigured") return { kind: "unconfigured" };
59
+ if (input.phase === "failed") return { kind: "failed", error: input.error, detail: input.errorDetail };
60
+ return {
61
+ kind: "connected",
62
+ count: input.cloudCount,
63
+ lastSync: relativeTime(input.lastSyncAt, input.now),
64
+ warn: input.error === "syncFailed" || input.error === "deviceConflict",
65
+ error: input.error,
66
+ detail: input.errorDetail
67
+ };
68
+ }
69
+ function errorKeyOf(error) {
70
+ switch (error) {
71
+ case "unreachable":
72
+ return "error.unreachable";
73
+ case "tokenInvalid":
74
+ return "error.tokenInvalid";
75
+ case "userMismatch":
76
+ return "error.userMismatch";
77
+ case "passphraseMismatch":
78
+ return "error.passphraseMismatch";
79
+ case "deviceConflict":
80
+ return "error.deviceConflict";
81
+ default:
82
+ return "error.unknown";
83
+ }
84
+ }
85
+ function canRunServerAction(serverUrlDraft, tokenAvailable) {
86
+ return serverUrlDraft !== "" && isValidServerUrl(serverUrlDraft) && tokenAvailable;
87
+ }
88
+ var TRIGGER_RETRY_MS = 1e3;
89
+ var TRIGGER_TIMEOUT_MS = 2e4;
90
+ function triggerAction(echoAt, issuedAt, lastSentAt, now) {
91
+ if (echoAt >= issuedAt) return "done";
92
+ if (now - issuedAt >= TRIGGER_TIMEOUT_MS) return "timeout";
93
+ if (now - lastSentAt >= TRIGGER_RETRY_MS) return "resend";
94
+ return "wait";
95
+ }
96
+ function parseCatalog(raw) {
97
+ if (raw === "") return null;
98
+ try {
99
+ const parsed = JSON.parse(raw);
100
+ if (typeof parsed.at !== "number" || !Array.isArray(parsed.sessions) || typeof parsed.selfDevice !== "string") return null;
101
+ return parsed;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+ function parseRestoreResult(raw) {
107
+ if (raw === "") return null;
108
+ try {
109
+ const parsed = JSON.parse(raw);
110
+ if (typeof parsed.at !== "number" || typeof parsed.ok !== "number" || typeof parsed.failed !== "number") return null;
111
+ return parsed;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ function parseMappings(raw) {
117
+ if (raw === "") return [];
118
+ try {
119
+ const parsed = JSON.parse(raw);
120
+ if (!Array.isArray(parsed)) return [];
121
+ return parsed.filter((pair) => typeof pair?.from === "string" && typeof pair?.to === "string");
122
+ } catch {
123
+ return [];
124
+ }
125
+ }
126
+ function parseStartupNotice(raw) {
127
+ if (raw === "") return null;
128
+ try {
129
+ const parsed = JSON.parse(raw);
130
+ if (typeof parsed.at !== "number" || typeof parsed.count !== "number" || parsed.at <= 0) return null;
131
+ return parsed;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ function encodeRestoreRequest(at, items) {
137
+ return JSON.stringify({ at, items });
138
+ }
139
+ function encodeMappingDelete(at, from) {
140
+ return JSON.stringify({ at, from });
141
+ }
142
+ function rowKeyOf(entry) {
143
+ return `${entry.device}/${entry.sessionId}`;
144
+ }
145
+ function isRowRestorable(entry) {
146
+ return !entry.existsLocal && !entry.versionIncompatible;
147
+ }
148
+ function defaultSelectedKeys(catalog) {
149
+ return catalog.sessions.filter((entry) => entry.device !== catalog.selfDevice && isRowRestorable(entry)).map(rowKeyOf);
150
+ }
151
+ function deviceOptions(catalog) {
152
+ return [...new Set(catalog.sessions.map((entry) => entry.device))].sort();
153
+ }
154
+ function filterEntries(entries, filter) {
155
+ if (filter === "") return entries;
156
+ return entries.filter((entry) => entry.device === filter);
157
+ }
158
+ function catalogErrorKeyOf(error) {
159
+ if (error === "tokenInvalid") return "error.tokenInvalid";
160
+ if (error === "userMismatch") return "error.userMismatch";
161
+ if (error === "unconfigured") return "status.unconfigured";
162
+ return "error.unreachable";
163
+ }
164
+
165
+ // src/client/CloudSyncCard.tsx
166
+ var import_jsx_runtime = require("react/jsx-runtime");
167
+ var EYE_GLYPH = "\u{1F441}";
168
+ var TRASH_GLYPH = "\u{1F5D1}";
169
+ var SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
170
+ var NOT_A_CREDENTIAL = {
171
+ autoComplete: "new-password",
172
+ "data-lpignore": "true",
173
+ "data-1p-ignore": "true",
174
+ "data-bwignore": "true",
175
+ "data-form-type": "other"
176
+ };
177
+ function relativeText(t, spec) {
178
+ return spec.key === "time.justNow" ? t("time.justNow") : t(spec.key, { count: spec.count });
179
+ }
180
+ function CloudSyncCard(props) {
181
+ const { t } = props;
182
+ const state = props.useCloudSyncCard((snapshot) => snapshot);
183
+ const [showToken, setShowToken] = (0, import_react.useState)(false);
184
+ const [showPassphrase, setShowPassphrase] = (0, import_react.useState)(false);
185
+ if (!state.available) return null;
186
+ const disabled = !state.writable;
187
+ const line = state.statusLine;
188
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: "dsh-cloud-sync-card", children: [
189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__header", children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__name", children: t("title") }),
191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__description", children: t("description") })
192
+ ] }),
193
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__body", children: [
194
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-cloud-sync-card__section", children: [
195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "dsh-cloud-sync-card__section-title", children: t("section.connection") }),
196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__field", children: [
197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-cloud-sync-card__field-head", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "dsh-cloud-sync-card__label", htmlFor: "dsh-cloud-sync-server-url", children: t("field.serverUrl") }) }),
198
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
199
+ "input",
200
+ {
201
+ id: "dsh-cloud-sync-server-url",
202
+ className: "dsh-cloud-sync-card__input",
203
+ type: "text",
204
+ value: state.serverUrlDraft,
205
+ placeholder: t("field.serverUrl.placeholder"),
206
+ disabled,
207
+ ...state.serverUrlInvalid ? { "aria-invalid": true } : {},
208
+ onChange: (event) => {
209
+ props.editServerUrl(event.target.value);
210
+ },
211
+ onBlur: () => {
212
+ props.commitServerUrl();
213
+ },
214
+ onKeyDown: (event) => {
215
+ if (event.key === "Enter") props.commitServerUrl();
216
+ }
217
+ }
218
+ ),
219
+ state.serverUrlInvalid ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-cloud-sync-card__error", children: t("invalid.serverUrl") }) : null
220
+ ] }),
221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__field", children: [
222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-cloud-sync-card__field-head", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "dsh-cloud-sync-card__label", htmlFor: "dsh-cloud-sync-username", children: t("field.username") }) }),
223
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
224
+ "input",
225
+ {
226
+ id: "dsh-cloud-sync-username",
227
+ className: "dsh-cloud-sync-card__input",
228
+ type: "text",
229
+ autoComplete: "off",
230
+ value: state.usernameDraft,
231
+ placeholder: state.usernameDraft === "" ? t("field.username.hint") : void 0,
232
+ disabled,
233
+ onChange: (event) => {
234
+ props.editUsername(event.target.value);
235
+ },
236
+ onBlur: () => {
237
+ props.commitUsername();
238
+ },
239
+ onKeyDown: (event) => {
240
+ if (event.key === "Enter") props.commitUsername();
241
+ }
242
+ }
243
+ )
244
+ ] }),
245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__field", children: [
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__field-head", children: [
247
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "dsh-cloud-sync-card__label", htmlFor: "dsh-cloud-sync-token", children: t("field.token") }),
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
249
+ "span",
250
+ {
251
+ className: state.tokenConfigured ? "dsh-cloud-sync-card__badge" : "dsh-cloud-sync-card__badge dsh-cloud-sync-card__badge--muted",
252
+ children: t(state.tokenConfigured ? "field.configured" : "field.unconfigured")
253
+ }
254
+ )
255
+ ] }),
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__input-row", children: [
257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
258
+ "input",
259
+ {
260
+ id: "dsh-cloud-sync-token",
261
+ className: "dsh-cloud-sync-card__input",
262
+ type: showToken ? "text" : "password",
263
+ ...NOT_A_CREDENTIAL,
264
+ placeholder: state.tokenConfigured ? SECRET_MASK : void 0,
265
+ value: state.tokenDraft,
266
+ disabled,
267
+ onChange: (event) => {
268
+ props.editToken(event.target.value);
269
+ },
270
+ onBlur: () => {
271
+ props.commitToken();
272
+ },
273
+ onKeyDown: (event) => {
274
+ if (event.key === "Enter") props.commitToken();
275
+ }
276
+ }
277
+ ),
278
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
279
+ "button",
280
+ {
281
+ type: "button",
282
+ className: "dsh-cloud-sync-card__icon-button",
283
+ "aria-label": t(showToken ? "field.secret.hide" : "field.secret.show"),
284
+ disabled,
285
+ onClick: () => {
286
+ setShowToken(!showToken);
287
+ },
288
+ children: EYE_GLYPH
289
+ }
290
+ )
291
+ ] })
292
+ ] }),
293
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__field", children: [
294
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__field-head", children: [
295
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "dsh-cloud-sync-card__label", htmlFor: "dsh-cloud-sync-passphrase", children: t("field.passphrase") }),
296
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
297
+ "span",
298
+ {
299
+ className: state.passphraseConfigured ? "dsh-cloud-sync-card__badge" : "dsh-cloud-sync-card__badge dsh-cloud-sync-card__badge--muted",
300
+ children: t(state.passphraseConfigured ? "field.configured" : "field.unconfigured")
301
+ }
302
+ )
303
+ ] }),
304
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__input-row", children: [
305
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
306
+ "input",
307
+ {
308
+ id: "dsh-cloud-sync-passphrase",
309
+ className: "dsh-cloud-sync-card__input",
310
+ type: showPassphrase ? "text" : "password",
311
+ ...NOT_A_CREDENTIAL,
312
+ placeholder: state.passphraseConfigured ? SECRET_MASK : void 0,
313
+ value: state.passphraseDraft,
314
+ disabled,
315
+ onChange: (event) => {
316
+ props.editPassphrase(event.target.value);
317
+ },
318
+ onBlur: () => {
319
+ props.commitPassphrase();
320
+ },
321
+ onKeyDown: (event) => {
322
+ if (event.key === "Enter") props.commitPassphrase();
323
+ }
324
+ }
325
+ ),
326
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
327
+ "button",
328
+ {
329
+ type: "button",
330
+ className: "dsh-cloud-sync-card__icon-button",
331
+ "aria-label": t(showPassphrase ? "field.secret.hide" : "field.secret.show"),
332
+ disabled,
333
+ onClick: () => {
334
+ setShowPassphrase(!showPassphrase);
335
+ },
336
+ children: EYE_GLYPH
337
+ }
338
+ )
339
+ ] })
340
+ ] }),
341
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__field", children: [
342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-cloud-sync-card__field-head", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "dsh-cloud-sync-card__label", htmlFor: "dsh-cloud-sync-device-name", children: t("field.deviceName") }) }),
343
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
344
+ "input",
345
+ {
346
+ id: "dsh-cloud-sync-device-name",
347
+ className: "dsh-cloud-sync-card__input",
348
+ type: "text",
349
+ value: state.deviceNameDraft,
350
+ disabled,
351
+ ...state.deviceNameInvalid ? { "aria-invalid": true } : {},
352
+ onChange: (event) => {
353
+ props.editDeviceName(event.target.value);
354
+ },
355
+ onBlur: () => {
356
+ props.commitDeviceName();
357
+ },
358
+ onKeyDown: (event) => {
359
+ if (event.key === "Enter") props.commitDeviceName();
360
+ }
361
+ }
362
+ ),
363
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
364
+ "p",
365
+ {
366
+ className: state.deviceNameInvalid ? "dsh-cloud-sync-card__error" : "dsh-cloud-sync-card__hint",
367
+ children: t(state.deviceNameInvalid ? "invalid.deviceName" : "field.deviceName.hint")
368
+ }
369
+ )
370
+ ] }),
371
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-cloud-sync-card__row-end", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
372
+ "button",
373
+ {
374
+ type: "button",
375
+ className: "dsh-cloud-sync-card__button",
376
+ disabled: disabled || state.testPending || !state.canTest,
377
+ onClick: () => {
378
+ props.testConnection();
379
+ },
380
+ children: t(state.testPending ? "action.testing" : "action.testConnection")
381
+ }
382
+ ) })
383
+ ] }),
384
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-cloud-sync-card__section", children: [
385
+ line.kind === "unconfigured" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "dsh-cloud-sync-card__status dsh-cloud-sync-card__status--muted", children: [
386
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__dot" }),
387
+ t("status.unconfigured")
388
+ ] }) : null,
389
+ line.kind === "failed" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
390
+ "p",
391
+ {
392
+ className: "dsh-cloud-sync-card__status dsh-cloud-sync-card__status--error",
393
+ title: line.detail === "" ? void 0 : line.detail,
394
+ children: [
395
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__dot", "data-state": "error" }),
396
+ t("status.disconnected", { reason: t(errorKeyOf(line.error)) })
397
+ ]
398
+ }
399
+ ) : null,
400
+ line.kind === "connected" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
401
+ "p",
402
+ {
403
+ className: "dsh-cloud-sync-card__status",
404
+ title: line.warn ? line.error === "deviceConflict" ? t("error.deviceConflict") : line.detail === "" ? void 0 : line.detail : void 0,
405
+ children: [
406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__dot", "data-state": line.warn ? "warning" : "done" }),
407
+ line.warn ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__warn", children: t("status.warnMark") }) : null,
408
+ t("status.connected", { count: line.count }),
409
+ line.lastSync !== null ? ` \xB7 ${t("status.lastSync", { time: relativeText(t, line.lastSync) })}` : null
410
+ ]
411
+ }
412
+ ) : null
413
+ ] }),
414
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-cloud-sync-card__section", children: [
415
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "dsh-cloud-sync-card__section-title", children: t("section.sync") }),
416
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__switch-row", children: [
417
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__switch-label", id: "dsh-cloud-sync-auto-upload-label", children: t("field.autoUpload") }),
418
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
419
+ "button",
420
+ {
421
+ type: "button",
422
+ role: "switch",
423
+ "aria-checked": state.autoUpload,
424
+ "aria-labelledby": "dsh-cloud-sync-auto-upload-label",
425
+ className: "dsh-cloud-sync-card__switch",
426
+ "data-on": state.autoUpload,
427
+ disabled,
428
+ onClick: () => {
429
+ props.toggleAutoUpload();
430
+ },
431
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__switch-knob" })
432
+ }
433
+ ),
434
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__hint", children: t("field.autoUpload.hint") })
435
+ ] }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__switch-row", children: [
437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__switch-label", id: "dsh-cloud-sync-check-on-startup-label", children: t("field.checkOnStartup") }),
438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
439
+ "button",
440
+ {
441
+ type: "button",
442
+ role: "switch",
443
+ "aria-checked": state.checkOnStartup,
444
+ "aria-labelledby": "dsh-cloud-sync-check-on-startup-label",
445
+ className: "dsh-cloud-sync-card__switch",
446
+ "data-on": state.checkOnStartup,
447
+ disabled,
448
+ onClick: () => {
449
+ props.toggleCheckOnStartup();
450
+ },
451
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__switch-knob" })
452
+ }
453
+ ),
454
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__hint", children: t("field.checkOnStartup.hint") })
455
+ ] })
456
+ ] }),
457
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-cloud-sync-card__section", children: [
458
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "dsh-cloud-sync-card__section-title", children: t("section.actions") }),
459
+ state.syncResult !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "dsh-cloud-sync-card__sync-result", role: "status", children: [
460
+ t("sync.result", { ok: state.syncResult.ok, failed: state.syncResult.failed }),
461
+ state.syncResult.failed > 0 && state.syncResult.error !== "" ? ` \u2014 ${state.syncResult.error}` : null
462
+ ] }) : null,
463
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-cloud-sync-card__row-end", children: [
464
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
465
+ "button",
466
+ {
467
+ type: "button",
468
+ className: "dsh-cloud-sync-card__button",
469
+ disabled: disabled || state.catalogPending,
470
+ onClick: () => {
471
+ props.openRestore();
472
+ },
473
+ children: t("action.restore")
474
+ }
475
+ ),
476
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
477
+ "button",
478
+ {
479
+ type: "button",
480
+ className: "dsh-cloud-sync-card__button dsh-cloud-sync-card__button--primary",
481
+ disabled: disabled || state.syncPending || !state.canSync,
482
+ onClick: () => {
483
+ props.syncAll();
484
+ },
485
+ children: t(state.syncPending ? "action.syncing" : "action.syncAll")
486
+ }
487
+ )
488
+ ] })
489
+ ] }),
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-cloud-sync-card__section", children: [
491
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "dsh-cloud-sync-card__section-title", children: t("section.mappings") }),
492
+ state.mappings.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-cloud-sync-card__hint", children: t("mapping.empty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dsh-cloud-sync-card__mappings", children: state.mappings.map((pair) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: "dsh-cloud-sync-card__mapping", children: [
493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsh-cloud-sync-card__mapping-paths", title: `${pair.from} \u2192 ${pair.to}`, children: [
494
+ pair.from,
495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-cloud-sync-card__mapping-arrow", children: "\u2192" }),
496
+ pair.to
497
+ ] }),
498
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
499
+ "button",
500
+ {
501
+ type: "button",
502
+ className: "dsh-cloud-sync-card__icon-button",
503
+ "aria-label": t("mapping.delete"),
504
+ disabled,
505
+ onClick: () => {
506
+ props.deleteMapping(pair.from);
507
+ },
508
+ children: TRASH_GLYPH
509
+ }
510
+ )
511
+ ] }, pair.from)) })
512
+ ] })
513
+ ] })
514
+ ] });
515
+ }
516
+
517
+ // src/client/CloudSyncOverlay.tsx
518
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
519
+
520
+ // src/client/RestoreDialog.tsx
521
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
522
+ var import_jsx_runtime2 = require("react/jsx-runtime");
523
+ function TargetCell(props) {
524
+ const { t, entry, rowKey, state, actions } = props;
525
+ if (entry.existsLocal) {
526
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-cloud-sync-restore__badge dsh-cloud-sync-restore__badge--muted", children: t("restore.existsLocal") });
527
+ }
528
+ if (entry.versionIncompatible) {
529
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
530
+ "span",
531
+ {
532
+ className: "dsh-cloud-sync-restore__badge dsh-cloud-sync-restore__badge--muted",
533
+ title: `format v${String(entry.formatVersion)}`,
534
+ children: t("restore.versionIncompatible")
535
+ }
536
+ );
537
+ }
538
+ const override = state.overrides[rowKey];
539
+ const target = override ?? entry.resolvedCwd;
540
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsh-cloud-sync-restore__target", children: [
541
+ target !== null && target !== void 0 && target !== "" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsh-cloud-sync-restore__path", title: target, children: [
542
+ target,
543
+ override === void 0 && entry.resolution === "suggested" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-cloud-sync-restore__badge", children: t("restore.pathSuggested") }) : null,
544
+ override === void 0 && entry.resolution === "mapping" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-cloud-sync-restore__badge", children: t("restore.pathMapped") }) : null
545
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-cloud-sync-restore__unmatched", children: t("restore.unmatchedPath") }),
546
+ state.pickerAvailable ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
547
+ "button",
548
+ {
549
+ type: "button",
550
+ className: "dsh-cloud-sync-restore__pick",
551
+ onClick: () => {
552
+ actions.pickTarget(rowKey);
553
+ },
554
+ children: t("restore.pickDirectory")
555
+ }
556
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
557
+ "input",
558
+ {
559
+ className: "dsh-cloud-sync-restore__manual",
560
+ type: "text",
561
+ value: override ?? "",
562
+ placeholder: t("restore.manualPath.placeholder"),
563
+ onChange: (event) => {
564
+ actions.overrideTarget(rowKey, event.target.value);
565
+ }
566
+ }
567
+ )
568
+ ] });
569
+ }
570
+ function RestoreDialog({ t, state, actions }) {
571
+ const catalog = state.catalog;
572
+ const entries = catalog === null ? [] : filterEntries(catalog.sessions, state.deviceFilter);
573
+ const selectedCount = state.selectedKeys.length;
574
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
575
+ import_dsh_client_ui_primitives.Modal,
576
+ {
577
+ open: true,
578
+ onClose: () => {
579
+ actions.closeRestore();
580
+ },
581
+ title: t("restore.title"),
582
+ closeLabel: t("restore.close"),
583
+ className: "dsh-cloud-sync-restore",
584
+ contentClassName: "dsh-cloud-sync-restore__content",
585
+ footer: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
586
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-cloud-sync-restore__selected", children: t("restore.selected", { count: selectedCount }) }),
587
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
588
+ "button",
589
+ {
590
+ type: "button",
591
+ className: "dsh-cloud-sync-card__button",
592
+ onClick: () => {
593
+ actions.closeRestore();
594
+ },
595
+ children: t("restore.cancel")
596
+ }
597
+ ),
598
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
599
+ "button",
600
+ {
601
+ type: "button",
602
+ className: "dsh-cloud-sync-card__button dsh-cloud-sync-card__button--primary",
603
+ disabled: selectedCount === 0 || state.restorePending,
604
+ onClick: () => {
605
+ actions.restoreSelected();
606
+ },
607
+ children: t(state.restorePending ? "restore.restoring" : "restore.action")
608
+ }
609
+ )
610
+ ] }),
611
+ children: [
612
+ catalog !== null && catalog.error !== "" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-cloud-sync-card__error", children: t(catalogErrorKeyOf(catalog.error)) }) : null,
613
+ catalog === null && state.catalogPending ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-cloud-sync-card__hint", children: t("restore.loading") }) : null,
614
+ catalog !== null && catalog.error === "" && entries.length === 0 && !state.catalogPending ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-cloud-sync-card__hint", children: t("restore.empty") }) : null,
615
+ catalog !== null && catalog.sessions.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
616
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-cloud-sync-restore__filter", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
617
+ "select",
618
+ {
619
+ className: "dsh-cloud-sync-restore__select",
620
+ value: state.deviceFilter,
621
+ onChange: (event) => {
622
+ actions.setDeviceFilter(event.target.value);
623
+ },
624
+ children: [
625
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: t("restore.filter.all") }),
626
+ deviceOptions(catalog).map((device) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: device, children: device }, device))
627
+ ]
628
+ }
629
+ ) }),
630
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-cloud-sync-restore__head", role: "row", children: [
631
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", {}),
632
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("restore.column.title") }),
633
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("restore.column.updated") }),
634
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("restore.column.events") }),
635
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("restore.column.target") })
636
+ ] }),
637
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "dsh-cloud-sync-restore__list", children: entries.map((entry) => {
638
+ const rowKey = rowKeyOf(entry);
639
+ const restorable = isRowRestorable(entry);
640
+ const updated = relativeTime(entry.updatedAt, Date.now());
641
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: "dsh-cloud-sync-restore__row", "data-disabled": !restorable, children: [
642
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
643
+ "input",
644
+ {
645
+ type: "checkbox",
646
+ checked: state.selectedKeys.includes(rowKey),
647
+ disabled: !restorable || state.restorePending,
648
+ onChange: () => {
649
+ actions.toggleRow(rowKey);
650
+ }
651
+ }
652
+ ),
653
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsh-cloud-sync-restore__title", title: entry.title, children: [
654
+ entry.title === "" ? entry.sessionId : entry.title,
655
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-cloud-sync-restore__device", children: entry.device })
656
+ ] }),
657
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-cloud-sync-restore__updated", children: updated === null ? "" : relativeText(t, updated) }),
658
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-cloud-sync-restore__events", children: entry.eventCount }),
659
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TargetCell, { t, entry, rowKey, state, actions })
660
+ ] }, rowKey);
661
+ }) })
662
+ ] }) : null
663
+ ]
664
+ }
665
+ );
666
+ }
667
+
668
+ // src/client/CloudSyncOverlay.tsx
669
+ var import_jsx_runtime3 = require("react/jsx-runtime");
670
+ function CloudSyncOverlay(props) {
671
+ const { t } = props;
672
+ const state = props.useCloudSyncCard((snapshot) => snapshot);
673
+ if (!state.available) return null;
674
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
675
+ state.restoreOpen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RestoreDialog, { t, state, actions: props }) : null,
676
+ state.notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-cloud-sync-notice", role: "alert", children: [
677
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-cloud-sync-notice__text", children: t("toast.newCloudSessions") }),
678
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
679
+ "button",
680
+ {
681
+ type: "button",
682
+ className: "dsh-cloud-sync-notice__action",
683
+ onClick: () => {
684
+ props.viewNotice();
685
+ },
686
+ children: t("toast.view")
687
+ }
688
+ ),
689
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
690
+ "button",
691
+ {
692
+ type: "button",
693
+ className: "dsh-cloud-sync-notice__dismiss",
694
+ "aria-label": t("toast.dismiss"),
695
+ onClick: () => {
696
+ props.dismissNotice();
697
+ },
698
+ children: "\xD7"
699
+ }
700
+ )
701
+ ] }) : null,
702
+ state.restoreOutcome !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
703
+ import_dsh_client_ui_primitives2.Toast,
704
+ {
705
+ text: t("restore.result", { ok: state.restoreOutcome.ok, failed: state.restoreOutcome.failed }),
706
+ onDone: () => {
707
+ props.clearRestoreOutcome();
708
+ }
709
+ }
710
+ ) : null
711
+ ] });
712
+ }
713
+
714
+ // src/client/cloud-sync-card-controller.ts
715
+ var import_dsh_client_store = require("@deepseek-ai/dsh-client-store");
716
+ var CLOUD_SYNC_NS = "cloud-sync";
717
+ var CLOUD_SYNC_STATUS_NS = "cloud-sync-status";
718
+ var SYNC_RESULT_VISIBLE_MS = 8e3;
719
+ var CLOCK_TICK_MS = 3e4;
720
+ var NOTICE_SEEN_KEY = "dsh-cloud-sync:notice-seen-at";
721
+ var CloudSyncCardController = class {
722
+ /**
723
+ * @param configScope - 绑定 'cloud-sync' 的 scope(配置读写)。
724
+ * @param statusScope - 绑定 'cloud-sync-status' 的 scope(状态读 + 触发器写)。
725
+ * @param directoryPicker - 目录选择器能力(可选;缺失时恢复对话框降级为手动输入)。
726
+ */
727
+ constructor(configScope, statusScope, directoryPicker) {
728
+ this.configScope = configScope;
729
+ this.statusScope = statusScope;
730
+ this.directoryPicker = directoryPicker;
731
+ this.store = (0, import_dsh_client_store.createSnapshotStore)(this.projection());
732
+ this.disposers.push(this.configScope.subscribe(() => {
733
+ this.onConfigChange();
734
+ }));
735
+ this.disposers.push(this.statusScope.subscribe(() => {
736
+ this.onStatusChange();
737
+ }));
738
+ const clock = setInterval(() => {
739
+ this.now = Date.now();
740
+ this.reproject();
741
+ }, CLOCK_TICK_MS);
742
+ this.disposers.push(() => {
743
+ clearInterval(clock);
744
+ });
745
+ try {
746
+ this.noticeSeenAt = Number(globalThis.sessionStorage?.getItem(NOTICE_SEEN_KEY) ?? 0) || 0;
747
+ } catch {
748
+ }
749
+ this.onConfigChange();
750
+ this.onStatusChange();
751
+ }
752
+ configScope;
753
+ statusScope;
754
+ directoryPicker;
755
+ store;
756
+ /** 本地草稿 + 各草稿对应的 scope 已同步值(用于识别外部变更该否覆盖草稿)。 */
757
+ serverUrlDraft = "";
758
+ serverUrlSynced = "";
759
+ usernameDraft = "";
760
+ usernameSynced = "";
761
+ deviceNameDraft = "";
762
+ deviceNameSynced = "";
763
+ tokenDraft = "";
764
+ passphraseDraft = "";
765
+ /** 本次会话内 secret 提交过的乐观标记(Host 的 tokenConfigured 回写前的即时反馈) */
766
+ tokenCommitted = false;
767
+ passphraseCommitted = false;
768
+ now = Date.now();
769
+ pendingTest = null;
770
+ pendingSync = null;
771
+ syncResult = null;
772
+ syncResultTimer = null;
773
+ /** 触发器进行期间的 1s 驱动节拍(无 pending 时停掉,不白转)。 */
774
+ triggerWatch = null;
775
+ disposers = [];
776
+ // ---- M4 内部状态 ----
777
+ restoreOpen = false;
778
+ catalog = null;
779
+ pendingRestoreList = null;
780
+ pendingRestore = null;
781
+ restoreOutcome = null;
782
+ deviceFilter = "";
783
+ selectedKeys = /* @__PURE__ */ new Set();
784
+ overrides = /* @__PURE__ */ new Map();
785
+ /** 映射删除的进行中请求(回显 = pathMappingsJson 里不再有该 from)。 */
786
+ pendingMappingDelete = null;
787
+ /** 启动通知的已读标记(同 at 不重复弹;sessionStorage 记忆,跨页面刷新不重现)。 */
788
+ noticeSeenAt = 0;
789
+ /** 释放订阅与计时器(插件卸载时由 ctx.effect 的清理函数调用)。 */
790
+ dispose() {
791
+ for (const dispose of this.disposers.splice(0)) dispose();
792
+ if (this.triggerWatch !== null) clearInterval(this.triggerWatch);
793
+ if (this.syncResultTimer !== null) clearTimeout(this.syncResultTimer);
794
+ }
795
+ /** 构建注册侧 face。 */
796
+ inject() {
797
+ return {
798
+ hooks: { cloudSyncCard: this.store },
799
+ editServerUrl: (text) => {
800
+ this.serverUrlDraft = text;
801
+ this.reproject();
802
+ },
803
+ commitServerUrl: () => {
804
+ this.commitServerUrl();
805
+ },
806
+ editUsername: (text) => {
807
+ this.usernameDraft = text;
808
+ this.reproject();
809
+ },
810
+ commitUsername: () => {
811
+ this.commitUsername();
812
+ },
813
+ editToken: (text) => {
814
+ this.tokenDraft = text;
815
+ this.reproject();
816
+ },
817
+ commitToken: () => {
818
+ this.commitSecret("token");
819
+ },
820
+ editPassphrase: (text) => {
821
+ this.passphraseDraft = text;
822
+ this.reproject();
823
+ },
824
+ commitPassphrase: () => {
825
+ this.commitSecret("passphrase");
826
+ },
827
+ editDeviceName: (text) => {
828
+ this.deviceNameDraft = text;
829
+ this.reproject();
830
+ },
831
+ commitDeviceName: () => {
832
+ this.commitDeviceName();
833
+ },
834
+ toggleAutoUpload: () => {
835
+ this.toggleBoolean("autoUpload");
836
+ },
837
+ toggleCheckOnStartup: () => {
838
+ this.toggleBoolean("checkOnStartup");
839
+ },
840
+ testConnection: () => {
841
+ this.startTrigger("test");
842
+ },
843
+ syncAll: () => {
844
+ this.startTrigger("sync");
845
+ },
846
+ openRestore: () => {
847
+ this.openRestore();
848
+ },
849
+ closeRestore: () => {
850
+ this.restoreOpen = false;
851
+ this.reproject();
852
+ },
853
+ setDeviceFilter: (filter) => {
854
+ this.deviceFilter = filter;
855
+ this.reproject();
856
+ },
857
+ toggleRow: (key) => {
858
+ if (this.selectedKeys.has(key)) this.selectedKeys.delete(key);
859
+ else this.selectedKeys.add(key);
860
+ this.reproject();
861
+ },
862
+ pickTarget: (key) => {
863
+ void this.pickTarget(key);
864
+ },
865
+ overrideTarget: (key, text) => {
866
+ const trimmed = text.trim();
867
+ if (trimmed === "") this.overrides.delete(key);
868
+ else this.overrides.set(key, trimmed);
869
+ this.reproject();
870
+ },
871
+ restoreSelected: () => {
872
+ this.startRestore();
873
+ },
874
+ deleteMapping: (from) => {
875
+ this.startMappingDelete(from);
876
+ },
877
+ dismissNotice: () => {
878
+ this.markNoticeSeen();
879
+ },
880
+ viewNotice: () => {
881
+ this.markNoticeSeen();
882
+ this.openRestore();
883
+ },
884
+ clearRestoreOutcome: () => {
885
+ this.restoreOutcome = null;
886
+ this.reproject();
887
+ }
888
+ };
889
+ }
890
+ /** 配置 scope 变更:同步未修改的草稿(正在编辑的草稿不被外部变更覆盖)。 */
891
+ onConfigChange() {
892
+ const value = this.configScope.getSnapshot().value;
893
+ const serverUrl = value?.serverUrl ?? "";
894
+ if (this.serverUrlDraft === this.serverUrlSynced) this.serverUrlDraft = serverUrl;
895
+ this.serverUrlSynced = serverUrl;
896
+ const username = value?.username ?? "";
897
+ if (this.usernameDraft === this.usernameSynced) this.usernameDraft = username;
898
+ this.usernameSynced = username;
899
+ const deviceName = value?.deviceName ?? "";
900
+ if (this.deviceNameDraft === this.deviceNameSynced) this.deviceNameDraft = deviceName;
901
+ this.deviceNameSynced = deviceName;
902
+ this.reproject();
903
+ }
904
+ /** 状态 scope 变更:驱动触发器的完成/重试判定,再重投影。 */
905
+ onStatusChange() {
906
+ if (this.catalog === null) {
907
+ this.catalog = parseCatalog(this.statusScope.getSnapshot().value?.restoreCatalogJson ?? "");
908
+ }
909
+ this.driveTriggers();
910
+ this.reproject();
911
+ }
912
+ /** 发起一次触发器写(测试连接 / 立即全部同步),进行态由 driveTriggers 维护。 */
913
+ startTrigger(kind) {
914
+ if (kind === "test" ? this.pendingTest !== null : this.pendingSync !== null) return;
915
+ const issuedAt = Date.now();
916
+ const op = { issuedAt, lastSentAt: 0 };
917
+ if (kind === "test") this.pendingTest = op;
918
+ else this.pendingSync = op;
919
+ this.sendTrigger(op, kind === "test" ? "testRequestedAt" : "syncAllRequestedAt");
920
+ this.ensureTriggerWatch();
921
+ this.reproject();
922
+ }
923
+ /** 写一次触发字段;失败不重抛——重试节拍会补发,总超时兜底退出。 */
924
+ sendTrigger(op, field) {
925
+ op.lastSentAt = Date.now();
926
+ void this.statusScope.set(field, op.issuedAt).catch(() => {
927
+ });
928
+ }
929
+ /** 触发器驱动:对每个 pending 判定 done/timeout/resend/wait 并执行。 */
930
+ driveTriggers() {
931
+ if (this.pendingTest === null && this.pendingSync === null && this.pendingRestoreList === null && this.pendingRestore === null && this.pendingMappingDelete === null) {
932
+ this.maybeStopTriggerWatch();
933
+ return;
934
+ }
935
+ const now = Date.now();
936
+ this.now = now;
937
+ const value = this.statusScope.getSnapshot().value;
938
+ if (this.pendingTest !== null) {
939
+ const action = triggerAction(value?.statusCheckedAt ?? 0, this.pendingTest.issuedAt, this.pendingTest.lastSentAt, now);
940
+ if (action === "done" || action === "timeout") this.pendingTest = null;
941
+ else if (action === "resend") this.sendTrigger(this.pendingTest, "testRequestedAt");
942
+ }
943
+ if (this.pendingSync !== null) {
944
+ const op = this.pendingSync;
945
+ const action = triggerAction(value?.statusSyncAllAt ?? 0, op.issuedAt, op.lastSentAt, now);
946
+ if (action === "done") {
947
+ this.pendingSync = null;
948
+ this.showSyncResult({
949
+ ok: value?.statusSyncAllOk ?? 0,
950
+ failed: value?.statusSyncAllFailed ?? 0,
951
+ error: value?.statusSyncAllError ?? ""
952
+ });
953
+ } else if (action === "timeout") {
954
+ this.pendingSync = null;
955
+ } else if (action === "resend") {
956
+ this.sendTrigger(op, "syncAllRequestedAt");
957
+ }
958
+ }
959
+ if (this.pendingRestoreList !== null) {
960
+ const op = this.pendingRestoreList;
961
+ const catalog = parseCatalog(value?.restoreCatalogJson ?? "");
962
+ const action = triggerAction(catalog?.at ?? 0, op.issuedAt, op.lastSentAt, now);
963
+ if (action === "done") {
964
+ this.pendingRestoreList = null;
965
+ this.catalog = catalog;
966
+ if (catalog !== null) {
967
+ for (const key of defaultSelectedKeys(catalog)) this.selectedKeys.add(key);
968
+ const live = new Set(catalog.sessions.map(rowKeyOf));
969
+ for (const key of [...this.selectedKeys]) if (!live.has(key)) this.selectedKeys.delete(key);
970
+ for (const key of [...this.overrides.keys()]) if (!live.has(key)) this.overrides.delete(key);
971
+ }
972
+ } else if (action === "timeout") {
973
+ this.pendingRestoreList = null;
974
+ } else if (action === "resend") {
975
+ op.lastSentAt = now;
976
+ void this.statusScope.set("restoreListRequestedAt", op.issuedAt).catch(() => {
977
+ });
978
+ }
979
+ }
980
+ if (this.pendingRestore !== null) {
981
+ const op = this.pendingRestore;
982
+ const result = parseRestoreResult(value?.restoreResultJson ?? "");
983
+ const action = triggerAction(result?.at ?? 0, op.issuedAt, op.lastSentAt, now);
984
+ if (action === "done" || action === "timeout") {
985
+ this.pendingRestore = null;
986
+ if (action === "done" && result !== null) {
987
+ this.restoreOutcome = result;
988
+ this.restoreOpen = false;
989
+ this.selectedKeys.clear();
990
+ this.overrides.clear();
991
+ }
992
+ } else if (action === "resend") {
993
+ op.lastSentAt = now;
994
+ void this.statusScope.set("restoreRequestJson", encodeRestoreRequest(op.issuedAt, this.restoreItems())).catch(() => {
995
+ });
996
+ }
997
+ }
998
+ if (this.pendingMappingDelete !== null) {
999
+ const op = this.pendingMappingDelete;
1000
+ const gone = !parseMappings(value?.pathMappingsJson ?? "[]").some((pair) => pair.from === op.from);
1001
+ const action = gone ? "done" : triggerAction(0, op.issuedAt, op.lastSentAt, now);
1002
+ if (action === "done" || action === "timeout") this.pendingMappingDelete = null;
1003
+ else if (action === "resend") {
1004
+ op.lastSentAt = now;
1005
+ void this.statusScope.set("mappingDeleteJson", encodeMappingDelete(op.issuedAt, op.from)).catch(() => {
1006
+ });
1007
+ }
1008
+ }
1009
+ this.maybeStopTriggerWatch();
1010
+ this.reproject();
1011
+ }
1012
+ /** pending 出现时起 1s 驱动节拍(写被静默丢弃时没有任何事件可等,必须主动轮询)。 */
1013
+ ensureTriggerWatch() {
1014
+ if (this.triggerWatch !== null) return;
1015
+ this.triggerWatch = setInterval(() => {
1016
+ this.driveTriggers();
1017
+ }, TRIGGER_RETRY_MS);
1018
+ }
1019
+ maybeStopTriggerWatch() {
1020
+ if (this.pendingTest !== null || this.pendingSync !== null || this.pendingRestoreList !== null || this.pendingRestore !== null || this.pendingMappingDelete !== null) return;
1021
+ if (this.triggerWatch === null) return;
1022
+ clearInterval(this.triggerWatch);
1023
+ this.triggerWatch = null;
1024
+ }
1025
+ // ---- M4 动作 ----
1026
+ /** 打开恢复对话框并发起目录拉取(每次打开都重新拉,保证 existsLocal 等标注新鲜)。 */
1027
+ openRestore() {
1028
+ this.restoreOpen = true;
1029
+ if (this.pendingRestoreList === null) {
1030
+ const op = { issuedAt: Date.now(), lastSentAt: 0 };
1031
+ this.pendingRestoreList = op;
1032
+ op.lastSentAt = op.issuedAt;
1033
+ void this.statusScope.set("restoreListRequestedAt", op.issuedAt).catch(() => {
1034
+ });
1035
+ this.ensureTriggerWatch();
1036
+ }
1037
+ this.reproject();
1038
+ }
1039
+ /** 目录选择器改选一行落位路径。 */
1040
+ async pickTarget(key) {
1041
+ if (!this.directoryPicker) return;
1042
+ try {
1043
+ const result = await this.directoryPicker.pick();
1044
+ if (result.ok && result.value !== null) {
1045
+ this.overrides.set(key, result.value);
1046
+ this.reproject();
1047
+ }
1048
+ } catch (error) {
1049
+ console.warn("cloud-sync: directory pick failed:", error);
1050
+ }
1051
+ }
1052
+ /** 组装恢复请求条目:改选优先,其次 Host 解析结果,都没有则 null(原样落位)。 */
1053
+ restoreItems() {
1054
+ const catalog = this.catalog;
1055
+ if (catalog === null) return [];
1056
+ const items = [];
1057
+ for (const entry of catalog.sessions) {
1058
+ const key = rowKeyOf(entry);
1059
+ if (!this.selectedKeys.has(key) || !isRowRestorable(entry)) continue;
1060
+ items.push({
1061
+ sessionId: entry.sessionId,
1062
+ device: entry.device,
1063
+ targetCwd: this.overrides.get(key) ?? entry.resolvedCwd
1064
+ });
1065
+ }
1066
+ return items;
1067
+ }
1068
+ /** 发起恢复执行触发器(重试保持同一 at 与条目集)。 */
1069
+ startRestore() {
1070
+ if (this.pendingRestore !== null) return;
1071
+ const items = this.restoreItems();
1072
+ if (items.length === 0) return;
1073
+ const op = { issuedAt: Date.now(), lastSentAt: 0 };
1074
+ this.pendingRestore = op;
1075
+ op.lastSentAt = op.issuedAt;
1076
+ void this.statusScope.set("restoreRequestJson", encodeRestoreRequest(op.issuedAt, items)).catch(() => {
1077
+ });
1078
+ this.ensureTriggerWatch();
1079
+ this.reproject();
1080
+ }
1081
+ /** 删除一条已学习映射(乐观移除 + 触发器重试到 Host 回显消失)。 */
1082
+ startMappingDelete(from) {
1083
+ if (this.pendingMappingDelete !== null) return;
1084
+ const op = { issuedAt: Date.now(), lastSentAt: 0, from };
1085
+ this.pendingMappingDelete = op;
1086
+ op.lastSentAt = op.issuedAt;
1087
+ void this.statusScope.set("mappingDeleteJson", encodeMappingDelete(op.issuedAt, from)).catch(() => {
1088
+ });
1089
+ this.ensureTriggerWatch();
1090
+ this.reproject();
1091
+ }
1092
+ /** 启动通知已读:本次 at 记入 sessionStorage,同 at 不再弹(01 §5.5 不重复弹出)。 */
1093
+ markNoticeSeen() {
1094
+ const notice = parseStartupNotice(this.statusScope.getSnapshot().value?.startupNoticeJson ?? "");
1095
+ if (notice !== null) {
1096
+ this.noticeSeenAt = notice.at;
1097
+ try {
1098
+ globalThis.sessionStorage?.setItem(NOTICE_SEEN_KEY, String(notice.at));
1099
+ } catch {
1100
+ }
1101
+ }
1102
+ this.reproject();
1103
+ }
1104
+ /** 展示同步结果摘要,短暂停留后自动消失。 */
1105
+ showSyncResult(result) {
1106
+ this.syncResult = result;
1107
+ if (this.syncResultTimer !== null) clearTimeout(this.syncResultTimer);
1108
+ this.syncResultTimer = setTimeout(() => {
1109
+ this.syncResult = null;
1110
+ this.syncResultTimer = null;
1111
+ this.reproject();
1112
+ }, SYNC_RESULT_VISIBLE_MS);
1113
+ }
1114
+ /** 服务器地址提交(blur/Enter):非法值不落盘,就地报错交给投影。 */
1115
+ commitServerUrl() {
1116
+ const text = this.serverUrlDraft.trim();
1117
+ this.serverUrlDraft = text;
1118
+ if (text !== "" && !isValidServerUrl(text)) {
1119
+ this.reproject();
1120
+ return;
1121
+ }
1122
+ if (text === this.serverUrlSynced) return;
1123
+ this.serverUrlSynced = text;
1124
+ void this.configScope.set("serverUrl", text).catch(() => {
1125
+ });
1126
+ }
1127
+ /** 用户名提交(blur/Enter):trim 后落盘,不做格式校验——合法性由服务端
1128
+ * 配对校验裁决(403 → userMismatch),插件无从感知服务端是单用户还是多用户。 */
1129
+ commitUsername() {
1130
+ const text = this.usernameDraft.trim();
1131
+ this.usernameDraft = text;
1132
+ if (text === this.usernameSynced) return;
1133
+ this.usernameSynced = text;
1134
+ void this.configScope.set("username", text).catch(() => {
1135
+ });
1136
+ }
1137
+ /** 设备名提交:合法或空(= 回退主机名)才落盘。 */
1138
+ commitDeviceName() {
1139
+ const text = this.deviceNameDraft.trim();
1140
+ this.deviceNameDraft = text;
1141
+ if (!isValidDeviceName(text)) {
1142
+ this.reproject();
1143
+ return;
1144
+ }
1145
+ if (text === this.deviceNameSynced) return;
1146
+ this.deviceNameSynced = text;
1147
+ void this.configScope.set("deviceName", text).catch(() => {
1148
+ });
1149
+ }
1150
+ /** secret 提交:空草稿不动既有配置;提交后立刻清空本地草稿。
1151
+ * 乐观置已配置标记(Host 纠偏回写前用户即可看到反馈),否则输入框清空
1152
+ * 又没有即时反馈,读起来像"值被吞了"。 */
1153
+ commitSecret(field) {
1154
+ const text = field === "token" ? this.tokenDraft : this.passphraseDraft;
1155
+ if (text === "") return;
1156
+ if (field === "token") {
1157
+ this.tokenDraft = "";
1158
+ this.tokenCommitted = true;
1159
+ } else {
1160
+ this.passphraseDraft = "";
1161
+ this.passphraseCommitted = true;
1162
+ }
1163
+ this.reproject();
1164
+ void this.configScope.set(field, text).catch(() => {
1165
+ });
1166
+ }
1167
+ /** 布尔开关:点击即写(配置即时保存,无"确定"按钮)。 */
1168
+ toggleBoolean(field) {
1169
+ const current = this.configScope.getSnapshot().value?.[field] ?? true;
1170
+ void this.configScope.set(field, !current).catch(() => {
1171
+ });
1172
+ }
1173
+ reproject() {
1174
+ this.store.set(this.projection());
1175
+ }
1176
+ projection() {
1177
+ const config = this.configScope.getSnapshot();
1178
+ const status = this.statusScope.getSnapshot().value;
1179
+ const tokenAvailable = status?.tokenConfigured === true || this.tokenDraft !== "";
1180
+ return {
1181
+ available: config.status !== "unavailable",
1182
+ writable: config.writable,
1183
+ serverUrlDraft: this.serverUrlDraft,
1184
+ serverUrlInvalid: this.serverUrlDraft !== "" && !isValidServerUrl(this.serverUrlDraft),
1185
+ usernameDraft: this.usernameDraft,
1186
+ deviceNameDraft: this.deviceNameDraft,
1187
+ deviceNameInvalid: !isValidDeviceName(this.deviceNameDraft),
1188
+ tokenDraft: this.tokenDraft,
1189
+ passphraseDraft: this.passphraseDraft,
1190
+ tokenConfigured: status?.tokenConfigured === true || this.tokenCommitted,
1191
+ passphraseConfigured: status?.passphraseConfigured === true || this.passphraseCommitted,
1192
+ autoUpload: config.value?.autoUpload ?? true,
1193
+ checkOnStartup: config.value?.checkOnStartup ?? true,
1194
+ canTest: canRunServerAction(this.serverUrlDraft, tokenAvailable),
1195
+ canSync: canRunServerAction(this.serverUrlDraft, tokenAvailable),
1196
+ testPending: this.pendingTest !== null,
1197
+ syncPending: this.pendingSync !== null,
1198
+ statusLine: projectStatusLine({
1199
+ phase: status?.statusPhase ?? "unconfigured",
1200
+ error: status?.statusError ?? "",
1201
+ errorDetail: status?.statusErrorDetail ?? "",
1202
+ cloudCount: status?.statusCloudCount ?? 0,
1203
+ lastSyncAt: status?.statusLastSyncAt ?? 0,
1204
+ now: this.now
1205
+ }),
1206
+ syncResult: this.syncResult,
1207
+ restoreOpen: this.restoreOpen,
1208
+ catalog: this.catalog,
1209
+ catalogPending: this.pendingRestoreList !== null,
1210
+ restorePending: this.pendingRestore !== null,
1211
+ restoreOutcome: this.restoreOutcome,
1212
+ deviceFilter: this.deviceFilter,
1213
+ selectedKeys: [...this.selectedKeys],
1214
+ overrides: Object.fromEntries(this.overrides),
1215
+ mappings: parseMappings(status?.pathMappingsJson ?? "[]"),
1216
+ notice: this.projectNotice(status?.startupNoticeJson ?? ""),
1217
+ pickerAvailable: this.directoryPicker !== void 0
1218
+ };
1219
+ }
1220
+ /** 启动通知投影:未读(at 晚于已读标记)才弹出。 */
1221
+ projectNotice(raw) {
1222
+ const notice = parseStartupNotice(raw);
1223
+ if (notice === null || notice.at <= this.noticeSeenAt) return null;
1224
+ return notice;
1225
+ }
1226
+ };
1227
+
1228
+ // src/client/locales.ts
1229
+ var zh = {
1230
+ title: "\u4E91\u7AEF\u540C\u6B65",
1231
+ description: "\u628A\u4F1A\u8BDD\u7AEF\u5230\u7AEF\u52A0\u5BC6\u540C\u6B65\u5230\u81EA\u5EFA\u670D\u52A1\u5668",
1232
+ "section.connection": "\u670D\u52A1\u5668\u8FDE\u63A5",
1233
+ "section.sync": "\u540C\u6B65\u65B9\u5F0F",
1234
+ "section.actions": "\u64CD\u4F5C",
1235
+ "field.serverUrl": "\u670D\u52A1\u5668\u5730\u5740",
1236
+ "field.serverUrl.placeholder": "http://host:8787",
1237
+ "field.username": "\u7528\u6237\u540D",
1238
+ "field.username.hint": "\u591A\u7528\u6237\u670D\u52A1\u7AEF\u586B\u7BA1\u7406\u5458\u5206\u914D\u7684\u7528\u6237\u540D\uFF1B\u5355\u7528\u6237\u670D\u52A1\u7AEF\u7559\u7A7A",
1239
+ "field.token": "\u8BBF\u95EE\u4EE4\u724C",
1240
+ "field.passphrase": "\u52A0\u5BC6\u53E3\u4EE4",
1241
+ "field.deviceName": "\u8BBE\u5907\u540D",
1242
+ "field.deviceName.hint": "\u7559\u7A7A\u4F7F\u7528\u672C\u673A\u4E3B\u673A\u540D",
1243
+ "field.autoUpload": "\u81EA\u52A8\u4E0A\u4F20",
1244
+ "field.autoUpload.hint": "\u4F1A\u8BDD\u6709\u65B0\u5185\u5BB9\u65F6\u540E\u53F0\u63A8\u9001",
1245
+ "field.checkOnStartup": "\u542F\u52A8\u65F6\u68C0\u67E5\u4E91\u7AEF",
1246
+ "field.checkOnStartup.hint": "\u53D1\u73B0\u5176\u4ED6\u8BBE\u5907\u7684\u4F1A\u8BDD\u65F6\u63D0\u793A",
1247
+ "field.configured": "\u5DF2\u914D\u7F6E",
1248
+ "field.unconfigured": "\u672A\u914D\u7F6E",
1249
+ "field.secret.show": "\u663E\u793A",
1250
+ "field.secret.hide": "\u9690\u85CF",
1251
+ "invalid.serverUrl": "\u8BF7\u8F93\u5165\u5408\u6CD5\u7684 http:// \u6216 https:// \u5730\u5740",
1252
+ "invalid.deviceName": "\u4EC5\u9650\u5B57\u6BCD\u3001\u6570\u5B57\u3001- \u548C _\uFF0C\u4E0D\u8D85\u8FC7 32 \u4E2A\u5B57\u7B26",
1253
+ "action.testConnection": "\u6D4B\u8BD5\u8FDE\u63A5",
1254
+ "action.testing": "\u6D4B\u8BD5\u4E2D\u2026",
1255
+ "action.syncAll": "\u7ACB\u5373\u5168\u90E8\u540C\u6B65",
1256
+ "action.syncing": "\u540C\u6B65\u4E2D\u2026",
1257
+ "status.unconfigured": "\u672A\u914D\u7F6E",
1258
+ "status.connected": "\u5DF2\u8FDE\u63A5 \xB7 \u4E91\u7AEF {count} \u4E2A\u4F1A\u8BDD",
1259
+ "status.lastSync": "\u4E0A\u6B21\u540C\u6B65 {time}",
1260
+ "status.disconnected": "\u8FDE\u63A5\u5931\u8D25\uFF1A{reason}",
1261
+ "status.warnMark": "\u26A0",
1262
+ "error.unreachable": "\u670D\u52A1\u5668\u5730\u5740\u4E0D\u53EF\u8FBE",
1263
+ "error.tokenInvalid": "\u8BBF\u95EE\u4EE4\u724C\u65E0\u6548",
1264
+ "error.userMismatch": "\u7528\u6237\u540D\u4E0E\u8BBF\u95EE\u4EE4\u724C\u4E0D\u5339\u914D",
1265
+ "error.passphraseMismatch": "\u52A0\u5BC6\u53E3\u4EE4\u4E0E\u4E91\u7AEF\u6570\u636E\u4E0D\u5339\u914D",
1266
+ "error.deviceConflict": "\u8BBE\u5907\u540D\u4E0E\u4E91\u7AEF\u5DF2\u6709\u6570\u636E\u51B2\u7A81\uFF0C\u8BF7\u66F4\u6362\u8BBE\u5907\u540D",
1267
+ "error.unknown": "\u540C\u6B65\u5931\u8D25",
1268
+ "time.justNow": "\u521A\u521A",
1269
+ "time.minutesAgo": "{count} \u5206\u949F\u524D",
1270
+ "time.hoursAgo": "{count} \u5C0F\u65F6\u524D",
1271
+ "time.daysAgo": "{count} \u5929\u524D",
1272
+ "sync.result": "\u6210\u529F {ok} \xB7 \u5931\u8D25 {failed}",
1273
+ "section.mappings": "\u8DEF\u5F84\u6620\u5C04",
1274
+ "mapping.empty": "\u6062\u590D\u4F1A\u8BDD\u65F6\u81EA\u52A8\u5B66\u4E60\uFF0C\u53EF\u5220\u9664\u540E\u91CD\u65B0\u5EFA\u7ACB",
1275
+ "mapping.delete": "\u5220\u9664",
1276
+ "action.restore": "\u4ECE\u4E91\u7AEF\u6062\u590D\u4F1A\u8BDD\u2026",
1277
+ "restore.title": "\u4ECE\u4E91\u7AEF\u6062\u590D\u4F1A\u8BDD",
1278
+ "restore.close": "\u5173\u95ED",
1279
+ "restore.cancel": "\u53D6\u6D88",
1280
+ "restore.loading": "\u6B63\u5728\u52A0\u8F7D\u4E91\u7AEF\u4F1A\u8BDD\u2026",
1281
+ "restore.empty": "\u4E91\u7AEF\u6CA1\u6709\u4F1A\u8BDD",
1282
+ "restore.filter.all": "\u5168\u90E8\u8BBE\u5907",
1283
+ "restore.column.title": "\u6807\u9898",
1284
+ "restore.column.updated": "\u6700\u540E\u66F4\u65B0",
1285
+ "restore.column.events": "\u4E8B\u4EF6\u6570",
1286
+ "restore.column.target": "\u843D\u4F4D\u8DEF\u5F84",
1287
+ "restore.pathSuggested": "\u5EFA\u8BAE",
1288
+ "restore.pathMapped": "\u5DF2\u6620\u5C04",
1289
+ "restore.unmatchedPath": "\u6062\u590D\u540E\u672A\u5206\u7EC4",
1290
+ "restore.pickDirectory": "\u9009\u62E9\u76EE\u5F55",
1291
+ "restore.manualPath.placeholder": "\u8F93\u5165\u672C\u673A\u76EE\u5F55\u8DEF\u5F84",
1292
+ "restore.existsLocal": "\u672C\u5730\u5DF2\u5B58\u5728",
1293
+ "restore.versionIncompatible": "\u7248\u672C\u4E0D\u517C\u5BB9",
1294
+ "restore.action": "\u6062\u590D\u9009\u4E2D\u4F1A\u8BDD",
1295
+ "restore.restoring": "\u6062\u590D\u4E2D\u2026",
1296
+ "restore.selected": "\u5DF2\u9009 {count} \u4E2A",
1297
+ "restore.result": "\u6062\u590D\u5B8C\u6210\uFF1A\u6210\u529F {ok} \xB7 \u5931\u8D25 {failed}",
1298
+ "toast.newCloudSessions": "\u4E91\u7AEF\u6709\u6765\u81EA\u5176\u4ED6\u8BBE\u5907\u7684\u4F1A\u8BDD\uFF0C\u70B9\u51FB\u67E5\u770B",
1299
+ "toast.view": "\u67E5\u770B",
1300
+ "toast.dismiss": "\u5173\u95ED"
1301
+ };
1302
+ var en = {
1303
+ title: "Cloud Sync",
1304
+ description: "End-to-end encrypted sync of sessions to your own server",
1305
+ "section.connection": "Server connection",
1306
+ "section.sync": "Sync",
1307
+ "section.actions": "Actions",
1308
+ "field.serverUrl": "Server address",
1309
+ "field.serverUrl.placeholder": "http://host:8787",
1310
+ "field.username": "Username",
1311
+ "field.username.hint": "Only for multi-user servers (assigned by the admin); leave empty otherwise",
1312
+ "field.token": "Access token",
1313
+ "field.passphrase": "Encryption passphrase",
1314
+ "field.deviceName": "Device name",
1315
+ "field.deviceName.hint": "Leave blank to use the hostname",
1316
+ "field.autoUpload": "Automatic upload",
1317
+ "field.autoUpload.hint": "Push in the background when sessions change",
1318
+ "field.checkOnStartup": "Check cloud on startup",
1319
+ "field.checkOnStartup.hint": "Prompt when other devices have sessions",
1320
+ "field.configured": "Configured",
1321
+ "field.unconfigured": "Not configured",
1322
+ "field.secret.show": "Show",
1323
+ "field.secret.hide": "Hide",
1324
+ "invalid.serverUrl": "Enter a valid http:// or https:// URL",
1325
+ "invalid.deviceName": "Letters, digits, - and _ only, up to 32 characters",
1326
+ "action.testConnection": "Test connection",
1327
+ "action.testing": "Testing\u2026",
1328
+ "action.syncAll": "Sync all now",
1329
+ "action.syncing": "Syncing\u2026",
1330
+ "status.unconfigured": "Not configured",
1331
+ "status.connected": "Connected \xB7 {count} sessions in cloud",
1332
+ "status.lastSync": "Last synced {time}",
1333
+ "status.disconnected": "Connection failed: {reason}",
1334
+ "status.warnMark": "\u26A0",
1335
+ "error.unreachable": "Server unreachable",
1336
+ "error.tokenInvalid": "Invalid access token",
1337
+ "error.userMismatch": "Username does not match the access token",
1338
+ "error.passphraseMismatch": "Passphrase does not match cloud data",
1339
+ "error.deviceConflict": "Device name conflicts with cloud data; rename this device",
1340
+ "error.unknown": "Sync failed",
1341
+ "time.justNow": "just now",
1342
+ "time.minutesAgo": "{count} min ago",
1343
+ "time.hoursAgo": "{count} h ago",
1344
+ "time.daysAgo": "{count} d ago",
1345
+ "sync.result": "Succeeded {ok} \xB7 failed {failed}",
1346
+ "section.mappings": "Path mappings",
1347
+ "mapping.empty": "Learned automatically when restoring sessions; delete to re-establish",
1348
+ "mapping.delete": "Delete",
1349
+ "action.restore": "Restore from cloud\u2026",
1350
+ "restore.title": "Restore sessions from cloud",
1351
+ "restore.close": "Close",
1352
+ "restore.cancel": "Cancel",
1353
+ "restore.loading": "Loading cloud sessions\u2026",
1354
+ "restore.empty": "No sessions in cloud",
1355
+ "restore.filter.all": "All devices",
1356
+ "restore.column.title": "Title",
1357
+ "restore.column.updated": "Last updated",
1358
+ "restore.column.events": "Events",
1359
+ "restore.column.target": "Target path",
1360
+ "restore.pathSuggested": "Suggested",
1361
+ "restore.pathMapped": "Mapped",
1362
+ "restore.unmatchedPath": "Ungrouped after restore",
1363
+ "restore.pickDirectory": "Choose directory",
1364
+ "restore.manualPath.placeholder": "Enter a local directory path",
1365
+ "restore.existsLocal": "Exists locally",
1366
+ "restore.versionIncompatible": "Version incompatible",
1367
+ "restore.action": "Restore selected",
1368
+ "restore.restoring": "Restoring\u2026",
1369
+ "restore.selected": "{count} selected",
1370
+ "restore.result": "Restore finished: {ok} succeeded \xB7 {failed} failed",
1371
+ "toast.newCloudSessions": "Sessions from other devices found in cloud \u2014 click to view",
1372
+ "toast.view": "View",
1373
+ "toast.dismiss": "Dismiss"
1374
+ };
1375
+
1376
+ // src/client/styles.ts
1377
+ var CARD_CSS = `
1378
+ .dsh-cloud-sync-card {
1379
+ list-style: none;
1380
+ border: 1px solid var(--dsw-alias-border-l2);
1381
+ border-radius: 12px;
1382
+ background: var(--dsw-alias-bg-layer-3);
1383
+ }
1384
+ .dsh-cloud-sync-card__header {
1385
+ display: flex;
1386
+ flex-direction: column;
1387
+ gap: 4px;
1388
+ padding: 14px 16px;
1389
+ }
1390
+ .dsh-cloud-sync-card__name {
1391
+ font-size: 15px;
1392
+ font-weight: 600;
1393
+ line-height: 1.4;
1394
+ color: var(--dsw-alias-label-primary);
1395
+ }
1396
+ .dsh-cloud-sync-card__description {
1397
+ font-size: 13px;
1398
+ line-height: 1.5;
1399
+ color: var(--dsw-alias-label-tertiary);
1400
+ }
1401
+ .dsh-cloud-sync-card__body {
1402
+ border-top: 1px solid var(--dsw-alias-border-l2);
1403
+ margin: 0 16px;
1404
+ padding: 4px 0 12px;
1405
+ }
1406
+ .dsh-cloud-sync-card__section {
1407
+ padding: 12px 0 0;
1408
+ }
1409
+ .dsh-cloud-sync-card__section + .dsh-cloud-sync-card__section {
1410
+ border-top: 1px solid var(--dsw-alias-border-l2);
1411
+ margin-top: 12px;
1412
+ }
1413
+ .dsh-cloud-sync-card__section-title {
1414
+ margin: 0 0 4px;
1415
+ font-size: 12px;
1416
+ font-weight: 500;
1417
+ line-height: 1.5;
1418
+ color: var(--dsw-alias-label-tertiary);
1419
+ }
1420
+ .dsh-cloud-sync-card__field {
1421
+ display: flex;
1422
+ flex-direction: column;
1423
+ gap: 6px;
1424
+ padding: 8px 0;
1425
+ }
1426
+ .dsh-cloud-sync-card__field-head {
1427
+ display: flex;
1428
+ align-items: center;
1429
+ gap: 8px;
1430
+ }
1431
+ .dsh-cloud-sync-card__label {
1432
+ flex: 1;
1433
+ min-width: 0;
1434
+ font-size: 13px;
1435
+ font-weight: 500;
1436
+ line-height: 1.5;
1437
+ color: var(--dsw-alias-label-primary);
1438
+ }
1439
+ .dsh-cloud-sync-card__badge {
1440
+ border-radius: 999px;
1441
+ padding: 1px 8px;
1442
+ font-size: 11px;
1443
+ line-height: 17px;
1444
+ white-space: nowrap;
1445
+ font-weight: 500;
1446
+ background: var(--dsw-alias-bg-module-platform);
1447
+ color: var(--dsw-alias-label-secondary);
1448
+ }
1449
+ .dsh-cloud-sync-card__badge--muted {
1450
+ background: none;
1451
+ color: var(--dsw-alias-label-tertiary);
1452
+ }
1453
+ .dsh-cloud-sync-card__input {
1454
+ display: block;
1455
+ width: 100%;
1456
+ box-sizing: border-box;
1457
+ height: 34px;
1458
+ padding: 0 12px;
1459
+ border: 1px solid var(--dsw-alias-border-l2);
1460
+ border-radius: 8px;
1461
+ background: var(--dsw-alias-bg-layer-3);
1462
+ font: inherit;
1463
+ font-size: 13px;
1464
+ line-height: 1.5;
1465
+ color: var(--dsw-alias-label-primary);
1466
+ }
1467
+ .dsh-cloud-sync-card__input::placeholder {
1468
+ color: var(--dsw-alias-label-dimmed);
1469
+ }
1470
+ .dsh-cloud-sync-card__input:focus-visible {
1471
+ outline: none;
1472
+ border-color: var(--dsw-alias-brand-primary);
1473
+ }
1474
+ .dsh-cloud-sync-card__input:disabled {
1475
+ color: var(--dsw-alias-label-tertiary);
1476
+ cursor: default;
1477
+ }
1478
+ .dsh-cloud-sync-card__input[aria-invalid='true'] {
1479
+ border-color: var(--dsw-alias-label-error);
1480
+ }
1481
+ /* secret \u884C\uFF1A\u5916\u6846\u662F\u89C6\u89C9\u63A7\u4EF6\uFF08\u7126\u70B9\u73AF\u843D\u5728\u884C\u4E0A\uFF09\uFF0C\u8F93\u5165\u6846\u88F8\u5D4C\u3001\u{1F441} \u6536\u8FDB\u6846\u5185\u3002
1482
+ \u6CE8\u610F\u8F93\u5165\u6846\u81EA\u8EAB\u4E0D\u80FD\u518D\u5E26 flex: 1 \u2014\u2014 \u5728\u7EB5\u5411 flex \u5BB9\u5668\u91CC flex-basis: 0%
1483
+ \u4F1A\u628A\u9AD8\u5EA6\u538B\u6CA1\uFF08M3 \u9A8C\u6536\u5B9E\u6D4B text \u8F93\u5165\u584C\u5230 15px\uFF09\uFF0C\u53EA\u80FD\u653E\u5728\u6A2A\u5411\u884C\u91CC\u7528\u3002 */
1484
+ .dsh-cloud-sync-card__input-row {
1485
+ display: flex;
1486
+ align-items: center;
1487
+ gap: 4px;
1488
+ height: 34px;
1489
+ box-sizing: border-box;
1490
+ padding: 0 6px 0 12px;
1491
+ border: 1px solid var(--dsw-alias-border-l2);
1492
+ border-radius: 8px;
1493
+ background: var(--dsw-alias-bg-layer-3);
1494
+ }
1495
+ .dsh-cloud-sync-card__input-row:focus-within {
1496
+ border-color: var(--dsw-alias-brand-primary);
1497
+ }
1498
+ .dsh-cloud-sync-card__input-row .dsh-cloud-sync-card__input {
1499
+ flex: 1;
1500
+ min-width: 0;
1501
+ height: auto;
1502
+ padding: 0;
1503
+ border: none;
1504
+ background: transparent;
1505
+ }
1506
+ .dsh-cloud-sync-card__input-row .dsh-cloud-sync-card__input:focus-visible {
1507
+ border: none;
1508
+ }
1509
+ .dsh-cloud-sync-card__hint {
1510
+ margin: 0;
1511
+ font-size: 12px;
1512
+ line-height: 1.5;
1513
+ color: var(--dsw-alias-label-tertiary);
1514
+ }
1515
+ .dsh-cloud-sync-card__error {
1516
+ margin: 0;
1517
+ font-size: 12px;
1518
+ line-height: 1.5;
1519
+ color: var(--dsw-alias-label-error);
1520
+ }
1521
+ .dsh-cloud-sync-card__icon-button {
1522
+ flex: none;
1523
+ appearance: none;
1524
+ border: none;
1525
+ background: none;
1526
+ padding: 4px;
1527
+ font: inherit;
1528
+ line-height: 1;
1529
+ color: var(--dsw-alias-label-tertiary);
1530
+ cursor: pointer;
1531
+ border-radius: 6px;
1532
+ }
1533
+ .dsh-cloud-sync-card__icon-button:hover:not(:disabled) {
1534
+ color: var(--dsw-alias-label-primary);
1535
+ }
1536
+ .dsh-cloud-sync-card__icon-button:disabled {
1537
+ cursor: default;
1538
+ opacity: 0.4;
1539
+ }
1540
+ .dsh-cloud-sync-card__row-end {
1541
+ display: flex;
1542
+ justify-content: flex-end;
1543
+ gap: 8px;
1544
+ padding: 4px 0;
1545
+ }
1546
+ .dsh-cloud-sync-card__button {
1547
+ appearance: none;
1548
+ border: 1px solid var(--dsw-alias-border-l2);
1549
+ border-radius: 8px;
1550
+ padding: 5px 14px;
1551
+ background: none;
1552
+ font: inherit;
1553
+ font-size: 13px;
1554
+ line-height: 1.5;
1555
+ color: var(--dsw-alias-label-secondary);
1556
+ cursor: pointer;
1557
+ }
1558
+ .dsh-cloud-sync-card__button:hover:not(:disabled) {
1559
+ color: var(--dsw-alias-label-primary);
1560
+ border-color: var(--dsw-alias-label-dimmed);
1561
+ }
1562
+ .dsh-cloud-sync-card__button:disabled {
1563
+ opacity: 0.4;
1564
+ cursor: default;
1565
+ }
1566
+ .dsh-cloud-sync-card__button:focus-visible {
1567
+ outline: 2px solid var(--dsw-alias-brand-primary);
1568
+ outline-offset: 1px;
1569
+ }
1570
+ .dsh-cloud-sync-card__button--primary {
1571
+ background: var(--dsw-alias-label-primary);
1572
+ border-color: transparent;
1573
+ color: var(--dsw-alias-bg-layer-3);
1574
+ }
1575
+ .dsh-cloud-sync-card__button--primary:hover:not(:disabled) {
1576
+ color: var(--dsw-alias-bg-layer-3);
1577
+ border-color: transparent;
1578
+ }
1579
+ .dsh-cloud-sync-card__status {
1580
+ display: flex;
1581
+ align-items: baseline;
1582
+ gap: 6px;
1583
+ margin: 0;
1584
+ font-size: 13px;
1585
+ line-height: 1.5;
1586
+ color: var(--dsw-alias-label-secondary);
1587
+ }
1588
+ .dsh-cloud-sync-card__status--muted {
1589
+ color: var(--dsw-alias-label-tertiary);
1590
+ }
1591
+ .dsh-cloud-sync-card__status--error {
1592
+ color: var(--dsw-alias-label-error);
1593
+ }
1594
+ .dsh-cloud-sync-card__dot {
1595
+ position: relative;
1596
+ top: -1px;
1597
+ align-self: center;
1598
+ flex: none;
1599
+ width: 8px;
1600
+ height: 8px;
1601
+ border-radius: 50%;
1602
+ background: var(--dsw-alias-label-tertiary);
1603
+ }
1604
+ .dsh-cloud-sync-card__dot[data-state='done'] {
1605
+ background: var(--dsw-alias-state-success-primary);
1606
+ }
1607
+ .dsh-cloud-sync-card__dot[data-state='warning'] {
1608
+ background: var(--dsw-alias-state-warn-primary);
1609
+ }
1610
+ .dsh-cloud-sync-card__dot[data-state='error'] {
1611
+ background: var(--dsw-alias-state-error-primary);
1612
+ }
1613
+ .dsh-cloud-sync-card__warn {
1614
+ color: var(--dsw-alias-state-warn-primary);
1615
+ }
1616
+ .dsh-cloud-sync-card__switch-row {
1617
+ display: flex;
1618
+ align-items: center;
1619
+ gap: 12px;
1620
+ padding: 8px 0;
1621
+ }
1622
+ .dsh-cloud-sync-card__switch-label {
1623
+ width: 120px;
1624
+ flex: none;
1625
+ font-size: 13px;
1626
+ font-weight: 500;
1627
+ line-height: 1.5;
1628
+ color: var(--dsw-alias-label-primary);
1629
+ }
1630
+ .dsh-cloud-sync-card__switch {
1631
+ position: relative;
1632
+ flex: none;
1633
+ width: 36px;
1634
+ height: 20px;
1635
+ border: none;
1636
+ border-radius: 999px;
1637
+ padding: 0;
1638
+ background: var(--dsw-alias-bg-module-platform);
1639
+ cursor: pointer;
1640
+ transition: background .16s;
1641
+ }
1642
+ .dsh-cloud-sync-card__switch[data-on='true'] {
1643
+ background: var(--dsw-alias-brand-primary);
1644
+ }
1645
+ .dsh-cloud-sync-card__switch:disabled {
1646
+ opacity: 0.4;
1647
+ cursor: default;
1648
+ }
1649
+ .dsh-cloud-sync-card__switch:focus-visible {
1650
+ outline: 2px solid var(--dsw-alias-brand-primary);
1651
+ outline-offset: 1px;
1652
+ }
1653
+ .dsh-cloud-sync-card__switch-knob {
1654
+ position: absolute;
1655
+ top: 2px;
1656
+ left: 2px;
1657
+ width: 16px;
1658
+ height: 16px;
1659
+ border-radius: 50%;
1660
+ background: var(--dsw-alias-bg-layer-3);
1661
+ transition: transform .16s;
1662
+ }
1663
+ .dsh-cloud-sync-card__switch[data-on='true'] .dsh-cloud-sync-card__switch-knob {
1664
+ transform: translateX(16px);
1665
+ }
1666
+ .dsh-cloud-sync-card__sync-result {
1667
+ margin: 0 0 4px;
1668
+ font-size: 12px;
1669
+ line-height: 1.5;
1670
+ color: var(--dsw-alias-label-secondary);
1671
+ }
1672
+ /* ---- M4\uFF1A\u8DEF\u5F84\u6620\u5C04\u533A ---- */
1673
+ .dsh-cloud-sync-card__mappings {
1674
+ list-style: none;
1675
+ margin: 4px 0 0;
1676
+ padding: 0;
1677
+ display: flex;
1678
+ flex-direction: column;
1679
+ gap: 2px;
1680
+ }
1681
+ .dsh-cloud-sync-card__mapping {
1682
+ display: flex;
1683
+ align-items: center;
1684
+ gap: 8px;
1685
+ padding: 4px 0;
1686
+ }
1687
+ .dsh-cloud-sync-card__mapping-paths {
1688
+ flex: 1;
1689
+ min-width: 0;
1690
+ overflow: hidden;
1691
+ text-overflow: ellipsis;
1692
+ white-space: nowrap;
1693
+ font-size: 12px;
1694
+ line-height: 1.5;
1695
+ color: var(--dsw-alias-label-secondary);
1696
+ }
1697
+ .dsh-cloud-sync-card__mapping-arrow {
1698
+ margin: 0 6px;
1699
+ color: var(--dsw-alias-label-dimmed);
1700
+ }
1701
+ /* ---- M4\uFF1A\u6062\u590D\u5BF9\u8BDD\u6846 ---- */
1702
+ .dsh-cloud-sync-restore {
1703
+ width: 720px;
1704
+ max-width: calc(100vw - 64px);
1705
+ }
1706
+ .dsh-cloud-sync-restore__content {
1707
+ max-height: 60vh;
1708
+ overflow-y: auto;
1709
+ }
1710
+ .dsh-cloud-sync-restore__filter {
1711
+ display: flex;
1712
+ justify-content: flex-end;
1713
+ margin-bottom: 8px;
1714
+ }
1715
+ .dsh-cloud-sync-restore__select {
1716
+ appearance: none;
1717
+ border: 1px solid var(--dsw-alias-border-l2);
1718
+ border-radius: 8px;
1719
+ padding: 4px 10px;
1720
+ background: var(--dsw-alias-bg-layer-3);
1721
+ font: inherit;
1722
+ font-size: 12px;
1723
+ color: var(--dsw-alias-label-secondary);
1724
+ }
1725
+ .dsh-cloud-sync-restore__head,
1726
+ .dsh-cloud-sync-restore__row {
1727
+ display: grid;
1728
+ grid-template-columns: 24px minmax(0, 2fr) 88px 56px minmax(0, 2.2fr);
1729
+ align-items: center;
1730
+ gap: 10px;
1731
+ }
1732
+ .dsh-cloud-sync-restore__head {
1733
+ padding: 4px 0;
1734
+ font-size: 11px;
1735
+ color: var(--dsw-alias-label-dimmed);
1736
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
1737
+ }
1738
+ .dsh-cloud-sync-restore__list {
1739
+ list-style: none;
1740
+ margin: 0;
1741
+ padding: 0;
1742
+ }
1743
+ .dsh-cloud-sync-restore__row {
1744
+ padding: 8px 0;
1745
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
1746
+ }
1747
+ .dsh-cloud-sync-restore__row[data-disabled='true'] {
1748
+ opacity: 0.55;
1749
+ }
1750
+ .dsh-cloud-sync-restore__title {
1751
+ min-width: 0;
1752
+ overflow: hidden;
1753
+ text-overflow: ellipsis;
1754
+ white-space: nowrap;
1755
+ font-size: 13px;
1756
+ color: var(--dsw-alias-label-primary);
1757
+ }
1758
+ .dsh-cloud-sync-restore__device {
1759
+ margin-left: 6px;
1760
+ font-size: 11px;
1761
+ color: var(--dsw-alias-label-dimmed);
1762
+ }
1763
+ .dsh-cloud-sync-restore__updated,
1764
+ .dsh-cloud-sync-restore__events {
1765
+ font-size: 12px;
1766
+ color: var(--dsw-alias-label-tertiary);
1767
+ }
1768
+ .dsh-cloud-sync-restore__target {
1769
+ display: flex;
1770
+ align-items: center;
1771
+ gap: 6px;
1772
+ min-width: 0;
1773
+ }
1774
+ .dsh-cloud-sync-restore__path {
1775
+ min-width: 0;
1776
+ overflow: hidden;
1777
+ text-overflow: ellipsis;
1778
+ white-space: nowrap;
1779
+ font-size: 12px;
1780
+ color: var(--dsw-alias-label-secondary);
1781
+ }
1782
+ .dsh-cloud-sync-restore__badge {
1783
+ flex: none;
1784
+ margin-left: 6px;
1785
+ border-radius: 999px;
1786
+ padding: 0 6px;
1787
+ font-size: 10px;
1788
+ line-height: 16px;
1789
+ background: var(--dsw-alias-bg-module-platform);
1790
+ color: var(--dsw-alias-label-secondary);
1791
+ }
1792
+ .dsh-cloud-sync-restore__badge--muted {
1793
+ background: none;
1794
+ color: var(--dsw-alias-label-tertiary);
1795
+ }
1796
+ .dsh-cloud-sync-restore__unmatched {
1797
+ flex: none;
1798
+ font-size: 12px;
1799
+ color: var(--dsw-alias-state-warn-primary);
1800
+ }
1801
+ .dsh-cloud-sync-restore__pick {
1802
+ flex: none;
1803
+ appearance: none;
1804
+ border: 1px solid var(--dsw-alias-border-l2);
1805
+ border-radius: 6px;
1806
+ padding: 2px 8px;
1807
+ background: none;
1808
+ font: inherit;
1809
+ font-size: 11px;
1810
+ color: var(--dsw-alias-label-secondary);
1811
+ cursor: pointer;
1812
+ }
1813
+ .dsh-cloud-sync-restore__manual {
1814
+ flex: 1;
1815
+ min-width: 0;
1816
+ height: 26px;
1817
+ padding: 0 8px;
1818
+ border: 1px solid var(--dsw-alias-border-l2);
1819
+ border-radius: 6px;
1820
+ background: var(--dsw-alias-bg-layer-3);
1821
+ font: inherit;
1822
+ font-size: 12px;
1823
+ color: var(--dsw-alias-label-primary);
1824
+ }
1825
+ .dsh-cloud-sync-restore__selected {
1826
+ flex: 1;
1827
+ font-size: 12px;
1828
+ color: var(--dsw-alias-label-tertiary);
1829
+ }
1830
+ /* ---- M4\uFF1A\u542F\u52A8\u68C0\u67E5\u901A\u77E5\uFF0801 \xA75.5 \u53F3\u4E0B\u89D2\u8F7B\u63D0\u793A\uFF1Bshell.overlay \u5C42\u9ED8\u8BA4 click-through\uFF0C\u9700\u81EA\u884C\u6062\u590D\u6307\u9488\u4E8B\u4EF6\uFF09 ---- */
1831
+ .dsh-cloud-sync-notice {
1832
+ position: fixed;
1833
+ right: 20px;
1834
+ bottom: 20px;
1835
+ z-index: 60;
1836
+ display: flex;
1837
+ align-items: center;
1838
+ gap: 10px;
1839
+ padding: 10px 14px;
1840
+ border: 1px solid var(--dsw-alias-border-l2);
1841
+ border-radius: 10px;
1842
+ background: var(--dsw-alias-bg-layer-3);
1843
+ box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
1844
+ pointer-events: auto;
1845
+ }
1846
+ .dsh-cloud-sync-notice__text {
1847
+ font-size: 13px;
1848
+ color: var(--dsw-alias-label-primary);
1849
+ }
1850
+ .dsh-cloud-sync-notice__action {
1851
+ appearance: none;
1852
+ border: none;
1853
+ background: none;
1854
+ padding: 0;
1855
+ font: inherit;
1856
+ font-size: 13px;
1857
+ color: var(--dsw-alias-brand-primary);
1858
+ cursor: pointer;
1859
+ }
1860
+ .dsh-cloud-sync-notice__dismiss {
1861
+ appearance: none;
1862
+ border: none;
1863
+ background: none;
1864
+ padding: 2px 4px;
1865
+ font: inherit;
1866
+ line-height: 1;
1867
+ color: var(--dsw-alias-label-tertiary);
1868
+ cursor: pointer;
1869
+ }
1870
+ `;
1871
+ var STYLE_TAG_ID = "dsh-cloud-sync/card";
1872
+ function installCardStyles() {
1873
+ if (typeof document === "undefined") return;
1874
+ if (document.querySelector(`style[data-plugin-css="${STYLE_TAG_ID}"]`) !== null) return;
1875
+ const tag = document.createElement("style");
1876
+ tag.dataset.plugin = "dsh-cloud-sync";
1877
+ tag.dataset.pluginCss = STYLE_TAG_ID;
1878
+ tag.textContent = CARD_CSS;
1879
+ document.head.appendChild(tag);
1880
+ }
1881
+
1882
+ // src/client/index.ts
1883
+ var LOCALE_NS = "settings.cloudSync";
1884
+ var inject = ["slots", "locale", "settingsScope"];
1885
+ function apply(ctx) {
1886
+ ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "cloud-sync: dictionaries");
1887
+ const directoryPicker = ctx.get("remote")?.directoryPicker;
1888
+ const controller = new CloudSyncCardController(
1889
+ ctx.settingsScope.bind({ namespace: CLOUD_SYNC_NS }),
1890
+ ctx.settingsScope.bind({ namespace: CLOUD_SYNC_STATUS_NS }),
1891
+ directoryPicker
1892
+ );
1893
+ ctx.effect(() => () => {
1894
+ controller.dispose();
1895
+ }, "cloud-sync: card controller");
1896
+ installCardStyles();
1897
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1898
+ name: "settings.plugin.item",
1899
+ key: CLOUD_SYNC_NS,
1900
+ locale: LOCALE_NS,
1901
+ inject: () => controller.inject()
1902
+ }, CloudSyncCard));
1903
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1904
+ name: "shell.overlay",
1905
+ id: "cloud-sync-overlay",
1906
+ order: 100,
1907
+ locale: LOCALE_NS,
1908
+ inject: () => controller.inject()
1909
+ }, CloudSyncOverlay));
1910
+ }
1911
+ return module.exports; } });
1912
+ //# sourceMappingURL=client.js.map