dsh-webdav-sync 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +298 -2
- package/cordis.patch.yml +7 -0
- package/icon.svg +9 -0
- package/lib/client.js +1144 -0
- package/lib/crypto.d.ts +64 -0
- package/lib/crypto.js +185 -0
- package/lib/dav.d.ts +137 -0
- package/lib/dav.js +548 -0
- package/lib/engine.d.ts +100 -0
- package/lib/engine.js +1042 -0
- package/lib/index.d.ts +74 -0
- package/lib/index.js +329 -0
- package/lib/install.d.ts +28 -0
- package/lib/install.js +269 -0
- package/lib/paths.d.ts +57 -0
- package/lib/paths.js +79 -0
- package/lib/state.d.ts +93 -0
- package/lib/state.js +253 -0
- package/lib/store.d.ts +147 -0
- package/lib/store.js +560 -0
- package/lib/wire.d.ts +346 -0
- package/lib/wire.js +71 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +107 -3
package/lib/client.js
ADDED
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-webdav-sync",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/wire.ts
|
|
11
|
+
/**
|
|
12
|
+
* dsh-webdav-sync wire contract: the zero-runtime-dependency module both the
|
|
13
|
+
* Host half and the browser half import. Nothing here touches node:fs, node:crypto
|
|
14
|
+
* or React, so the browser bundle can pull it in without widening its surface.
|
|
15
|
+
*
|
|
16
|
+
* The shapes below are the plugin's own protocol: the sealed backup that travels
|
|
17
|
+
* to the WebDAV server, the local sync state that never leaves this computer, and
|
|
18
|
+
* the HTTP envelopes the settings card talks to.
|
|
19
|
+
*/
|
|
20
|
+
/** Every route this plugin owns lives under this prefix. */
|
|
21
|
+
const ROUTE_PREFIX = "/plugin/webdav-sync";
|
|
22
|
+
/** GET: configuration (without secrets), state, notice, and what installs pend. */
|
|
23
|
+
const ROUTE_STATUS = `${ROUTE_PREFIX}/status`;
|
|
24
|
+
/** POST: save the sync configuration. */
|
|
25
|
+
const ROUTE_CONFIG = `${ROUTE_PREFIX}/config`;
|
|
26
|
+
/** POST: reach the server with the stored configuration and report what happened. */
|
|
27
|
+
const ROUTE_CHECK = `${ROUTE_PREFIX}/check`;
|
|
28
|
+
/** POST: run one merge. `{ force }` runs it even when nothing looks changed. */
|
|
29
|
+
const ROUTE_SYNC = `${ROUTE_PREFIX}/sync`;
|
|
30
|
+
/** POST: make this computer's setup the server's. */
|
|
31
|
+
const ROUTE_RESTORE = `${ROUTE_PREFIX}/restore`;
|
|
32
|
+
/** POST: write over a file on the server that is not a backup of ours. */
|
|
33
|
+
const ROUTE_UPLOAD = `${ROUTE_PREFIX}/upload`;
|
|
34
|
+
/** POST: clear the sync notice. */
|
|
35
|
+
const ROUTE_DISMISS = `${ROUTE_PREFIX}/dismiss`;
|
|
36
|
+
/** POST: forget the configuration and stop syncing. */
|
|
37
|
+
const ROUTE_OFF = `${ROUTE_PREFIX}/off`;
|
|
38
|
+
/** POST: install the profile's declared plugins with the bundled pnpm. */
|
|
39
|
+
const ROUTE_INSTALL = `${ROUTE_PREFIX}/install`;
|
|
40
|
+
/** GET: what one sync would do, without writing anything anywhere. */
|
|
41
|
+
const ROUTE_PREVIEW = `${ROUTE_PREFIX}/preview`;
|
|
42
|
+
/** POST: put back the copies a restore replaced. */
|
|
43
|
+
const ROUTE_UNDO = `${ROUTE_PREFIX}/undo`;
|
|
44
|
+
/** The intervals the card offers, in minutes. */
|
|
45
|
+
const AUTO_CHOICES = [
|
|
46
|
+
-1,
|
|
47
|
+
3,
|
|
48
|
+
15,
|
|
49
|
+
30,
|
|
50
|
+
60
|
|
51
|
+
];
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region \0webdav-sync-css:/home/runner/work/dsh-webdav/dsh-webdav/src/client/WebdavSyncCard.module.css?inline
|
|
54
|
+
const css = ".summary_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.summaryText_318697645 {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.page_318697645 {\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 4px 0 8px;\n font-size: 13px;\n color: var(--dsw-alias-label-primary);\n}\n\n.legend_318697645 {\n display: flex;\n align-items: baseline;\n gap: 8px;\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.hint_318697645 {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.field_318697645 {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-bottom: 10px;\n}\n\n.fieldLabel_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/*\n * The line of explanation belonging to one field: it sits between that field's\n * label and its input, so a reader gets the rule before typing, and the gap\n * between two fields (10px) still reads as the bigger break.\n */\n.fieldHint_318697645 {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.grid_318697645 {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n gap: 10px 14px;\n}\n\n/* The saved-password checkbox with its own line of explanation, set off from\n * the fields above it. Its label is brought down to the fields' 12px for the\n * same reason as the part rows (see `.partRow_318697645 label`). */\n.toggleRow_318697645 {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n padding-top: 10px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n\n.toggleRow_318697645 label {\n gap: 6px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.toggleRow_318697645 label input {\n width: 14px;\n height: 14px;\n}\n\n/*\n * The auto-sync choice: every tab in one row, all the same width, so six\n * options read as one strip instead of a dropdown hiding five of them.\n */\n/* Doubled class: the control's own rule also sets a display, and this wins. */\n.autoControl_318697645.autoControl_318697645 {\n display: grid;\n width: 100%;\n box-sizing: border-box;\n}\n\n/* Six tabs have to share the row, so each keeps its padding modest. */\n.autoControl_318697645 button {\n padding: 0 6px;\n font-size: 12px;\n}\n\n.partRow_318697645 {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 7px 0;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n\n/* No trailing border when the rows stand alone (a host with no view yet). */\n.partRow_318697645:last-child {\n border-bottom: none;\n}\n\n/* The checkbox with its one-line description under it. */\n.partText_318697645 {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n/*\n * The checkbox component sets its own label at 14px, which is bigger than this\n * card's section headings (12px) — the rows read as if they outranked the\n * heading above them. Its class name is internal to the component, so the\n * element selector is the way in; the box shrinks with the text to match.\n */\n.partRow_318697645 label {\n gap: 6px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.partRow_318697645 label input {\n width: 14px;\n height: 14px;\n}\n\n.partHint_318697645 {\n font-size: 11px;\n line-height: 1.4;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.partState_318697645 {\n margin-left: auto;\n flex: none;\n padding-top: 2px;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.partStateBoth_318697645 {\n color: var(--dsw-alias-label-warning, var(--dsw-alias-label-secondary));\n}\n\n.notice_318697645 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n font-size: 12px;\n}\n\n.noticeTitle_318697645 {\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n/*\n * The sync actions. Six short labels have to sit on one row, so the buttons\n * keep their own padding down; wrapping stays on only as a narrow-card escape.\n */\n.footer_318697645 {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n padding-top: 4px;\n}\n\n.footer_318697645 button {\n padding: 0 8px;\n}\n\n.spacer_318697645 {\n flex: 1;\n}\n\n/* The card's own actions: WebDAV settings, kept apart from the sync ones. */\n.cardFooter_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n margin-top: 4px;\n padding-top: 12px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n\n.failed_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-error);\n}\n\n.note_318697645 {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.confirm_318697645 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l4);\n border-radius: 10px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n\n.previewList_318697645 {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 12px;\n}\n\n.previewRow_318697645 {\n display: flex;\n gap: 8px;\n}\n\n.previewPart_318697645 {\n min-width: 90px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.output_318697645 {\n max-height: 180px;\n margin: 0;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 1.5;\n white-space: pre-wrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n.spin_318697645 {\n animation: webdavSyncSpin 0.9s linear infinite;\n}\n\n@keyframes webdavSyncSpin {\n to {\n transform: rotate(360deg);\n }\n}\n";
|
|
55
|
+
const tagId = "dsh-webdav-sync/WebdavSyncCard.module.css";
|
|
56
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
57
|
+
const tag = document.createElement("style");
|
|
58
|
+
tag.dataset.plugin = "dsh-webdav-sync";
|
|
59
|
+
tag.dataset.pluginCss = tagId;
|
|
60
|
+
tag.textContent = css;
|
|
61
|
+
document.head.appendChild(tag);
|
|
62
|
+
}
|
|
63
|
+
var WebdavSyncCard_module_default = {
|
|
64
|
+
"summary": "summary_318697645",
|
|
65
|
+
"summaryText": "summaryText_318697645",
|
|
66
|
+
"page": "page_318697645",
|
|
67
|
+
"legend": "legend_318697645",
|
|
68
|
+
"hint": "hint_318697645",
|
|
69
|
+
"field": "field_318697645",
|
|
70
|
+
"fieldLabel": "fieldLabel_318697645",
|
|
71
|
+
"fieldHint": "fieldHint_318697645",
|
|
72
|
+
"grid": "grid_318697645",
|
|
73
|
+
"partRow": "partRow_318697645",
|
|
74
|
+
"toggleRow": "toggleRow_318697645",
|
|
75
|
+
"autoControl": "autoControl_318697645",
|
|
76
|
+
"partText": "partText_318697645",
|
|
77
|
+
"partHint": "partHint_318697645",
|
|
78
|
+
"partState": "partState_318697645",
|
|
79
|
+
"partStateBoth": "partStateBoth_318697645",
|
|
80
|
+
"notice": "notice_318697645",
|
|
81
|
+
"noticeTitle": "noticeTitle_318697645",
|
|
82
|
+
"footer": "footer_318697645",
|
|
83
|
+
"spacer": "spacer_318697645",
|
|
84
|
+
"cardFooter": "cardFooter_318697645",
|
|
85
|
+
"failed": "failed_318697645",
|
|
86
|
+
"note": "note_318697645",
|
|
87
|
+
"confirm": "confirm_318697645",
|
|
88
|
+
"previewList": "previewList_318697645",
|
|
89
|
+
"previewRow": "previewRow_318697645",
|
|
90
|
+
"previewPart": "previewPart_318697645",
|
|
91
|
+
"output": "output_318697645",
|
|
92
|
+
"spin": "spin_318697645"
|
|
93
|
+
};
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/client/WebdavSyncCard.tsx
|
|
96
|
+
/**
|
|
97
|
+
* The WebDAV sync card, rendered in two seats from one body:
|
|
98
|
+
*
|
|
99
|
+
* - `WebdavSyncBundleCard`, the keyed `plugins.bundle.config` entry for this
|
|
100
|
+
* package, so it shows on the bundle's row (`view: 'summary'`) and inside its
|
|
101
|
+
* page (`view: 'page'`) on the Plugins page;
|
|
102
|
+
* - `WebdavSyncSection`, a `settings.section` page, so it is also reachable from
|
|
103
|
+
* Settings directly.
|
|
104
|
+
*
|
|
105
|
+
* Everything the card shows comes from the host half's status route. The card
|
|
106
|
+
* never reads a profile file and never speaks WebDAV itself: the browser half is
|
|
107
|
+
* a view plus the routes in `api.ts`.
|
|
108
|
+
*
|
|
109
|
+
* @module webdav-sync/client/WebdavSyncCard
|
|
110
|
+
*/
|
|
111
|
+
/** Which dictionary key names each part. */
|
|
112
|
+
const PART_NAME = {
|
|
113
|
+
models: "partModels",
|
|
114
|
+
defaults: "partDefaults",
|
|
115
|
+
plugins: "partPlugins",
|
|
116
|
+
patch: "partPatch"
|
|
117
|
+
};
|
|
118
|
+
/** Which dictionary key says, in a line, what a part carries. */
|
|
119
|
+
const PART_HINT = {
|
|
120
|
+
models: "partModelsHint",
|
|
121
|
+
defaults: "partDefaultsHint",
|
|
122
|
+
plugins: "partPluginsHint",
|
|
123
|
+
patch: "partPatchHint"
|
|
124
|
+
};
|
|
125
|
+
/** The value of the auto-sync control's first tab, which is not an interval. */
|
|
126
|
+
const AUTO_OFF = "off";
|
|
127
|
+
/** Which dictionary key names the action in flight. */
|
|
128
|
+
const BUSY_TEXT = {
|
|
129
|
+
check: "check",
|
|
130
|
+
preview: "preview",
|
|
131
|
+
sync: "syncNow",
|
|
132
|
+
restore: "restore",
|
|
133
|
+
undo: "undo",
|
|
134
|
+
upload: "upload",
|
|
135
|
+
install: "install"
|
|
136
|
+
};
|
|
137
|
+
/** The one-line state of one part as the panel words it. */
|
|
138
|
+
function partStateKey(view) {
|
|
139
|
+
if (view.localChanged && view.remoteChanged) return "partBoth";
|
|
140
|
+
if (view.localChanged) return "partOnlyHere";
|
|
141
|
+
if (view.remoteChanged) return "partOnlyThere";
|
|
142
|
+
return "partSynced";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The last sync time as this computer's own clock reads it: the host stores an
|
|
146
|
+
* ISO string in UTC, which is not what a person recognises at a glance.
|
|
147
|
+
*/
|
|
148
|
+
function whenText(at) {
|
|
149
|
+
const when = new Date(at);
|
|
150
|
+
return Number.isNaN(when.getTime()) ? at : when.toLocaleString();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The auto-sync tabs, in the order the card shows them: off first, then by
|
|
154
|
+
* hand, then the intervals. Off is not an interval but a way out, so it is
|
|
155
|
+
* worded as its own tab instead of an extra choice at the end.
|
|
156
|
+
*/
|
|
157
|
+
function autoOptions(t) {
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
value: AUTO_OFF,
|
|
161
|
+
label: t("autoOff")
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
value: String(-1),
|
|
165
|
+
label: t("autoManual")
|
|
166
|
+
},
|
|
167
|
+
...AUTO_CHOICES.filter((minutes) => minutes !== -1).map((minutes) => ({
|
|
168
|
+
value: String(minutes),
|
|
169
|
+
label: minutes % 60 === 0 ? t("autoHour", { n: minutes / 60 }) : t("autoEvery", { n: minutes })
|
|
170
|
+
}))
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
/** The card body. One render path, so the two seats never drift apart. */
|
|
174
|
+
function Card(props) {
|
|
175
|
+
const { t } = props;
|
|
176
|
+
const onPage = props.view !== "summary";
|
|
177
|
+
const state = props.useWebdavSync((snapshot) => snapshot);
|
|
178
|
+
(0, react.useEffect)(() => {
|
|
179
|
+
if (onPage) props.ensure();
|
|
180
|
+
}, [onPage]);
|
|
181
|
+
if (state.phase === "loading") return onPage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
182
|
+
className: WebdavSyncCard_module_default.note,
|
|
183
|
+
role: "status",
|
|
184
|
+
children: t("working")
|
|
185
|
+
}) : null;
|
|
186
|
+
const config = state.view?.config;
|
|
187
|
+
if (!onPage) {
|
|
188
|
+
const summary = config === void 0 || !config.passphraseSet ? t("cardSummaryOff") : config.url === "" ? t("cardSummaryUnset") : t("cardSummaryOn", { url: config.url });
|
|
189
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
190
|
+
className: WebdavSyncCard_module_default.summary,
|
|
191
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
192
|
+
className: WebdavSyncCard_module_default.summaryText,
|
|
193
|
+
children: summary
|
|
194
|
+
})
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (config === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
198
|
+
className: WebdavSyncCard_module_default.page,
|
|
199
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
200
|
+
className: WebdavSyncCard_module_default.failed,
|
|
201
|
+
role: "alert",
|
|
202
|
+
children: state.error || t("unavailable")
|
|
203
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
204
|
+
className: WebdavSyncCard_module_default.footer,
|
|
205
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
206
|
+
variant: "outline",
|
|
207
|
+
size: "sm",
|
|
208
|
+
onClick: () => props.refresh(),
|
|
209
|
+
children: t("check")
|
|
210
|
+
})
|
|
211
|
+
})]
|
|
212
|
+
});
|
|
213
|
+
const busy = state.busy !== "";
|
|
214
|
+
const busyKey = BUSY_TEXT[state.busy];
|
|
215
|
+
const confirmText = state.confirm === "restore" ? t("confirmRestore") : state.confirm === "upload" ? t("confirmUpload") : state.confirm === "off" ? t("confirmOff") : "";
|
|
216
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
217
|
+
className: WebdavSyncCard_module_default.page,
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
220
|
+
className: WebdavSyncCard_module_default.hint,
|
|
221
|
+
children: t("sectionSubtitle")
|
|
222
|
+
}),
|
|
223
|
+
state.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
224
|
+
className: WebdavSyncCard_module_default.failed,
|
|
225
|
+
role: "alert",
|
|
226
|
+
children: state.error
|
|
227
|
+
}),
|
|
228
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
|
|
229
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
230
|
+
className: WebdavSyncCard_module_default.legend,
|
|
231
|
+
children: t("addressLegend")
|
|
232
|
+
}),
|
|
233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
234
|
+
className: WebdavSyncCard_module_default.field,
|
|
235
|
+
children: [
|
|
236
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
237
|
+
className: WebdavSyncCard_module_default.fieldLabel,
|
|
238
|
+
htmlFor: "webdav-sync-url",
|
|
239
|
+
children: t("urlLabel")
|
|
240
|
+
}),
|
|
241
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
242
|
+
className: WebdavSyncCard_module_default.fieldHint,
|
|
243
|
+
children: t("urlHint")
|
|
244
|
+
}),
|
|
245
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
246
|
+
id: "webdav-sync-url",
|
|
247
|
+
value: state.url,
|
|
248
|
+
spellCheck: false,
|
|
249
|
+
placeholder: "https://dav.jianguoyun.com/dav/",
|
|
250
|
+
onChange: (event) => props.edit("url", event.target.value)
|
|
251
|
+
})
|
|
252
|
+
]
|
|
253
|
+
}),
|
|
254
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
255
|
+
className: WebdavSyncCard_module_default.grid,
|
|
256
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
257
|
+
className: WebdavSyncCard_module_default.field,
|
|
258
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
259
|
+
className: WebdavSyncCard_module_default.fieldLabel,
|
|
260
|
+
htmlFor: "webdav-sync-user",
|
|
261
|
+
children: t("userLabel")
|
|
262
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
263
|
+
id: "webdav-sync-user",
|
|
264
|
+
value: state.user,
|
|
265
|
+
spellCheck: false,
|
|
266
|
+
autoComplete: "off",
|
|
267
|
+
onChange: (event) => props.edit("user", event.target.value)
|
|
268
|
+
})]
|
|
269
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
270
|
+
className: WebdavSyncCard_module_default.field,
|
|
271
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
272
|
+
className: WebdavSyncCard_module_default.fieldLabel,
|
|
273
|
+
htmlFor: "webdav-sync-password",
|
|
274
|
+
children: t("passwordLabel")
|
|
275
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
276
|
+
id: "webdav-sync-password",
|
|
277
|
+
type: "password",
|
|
278
|
+
value: state.password,
|
|
279
|
+
autoComplete: "new-password",
|
|
280
|
+
placeholder: config.passwordSet ? "••••••••" : "",
|
|
281
|
+
onChange: (event) => props.edit("password", event.target.value)
|
|
282
|
+
})]
|
|
283
|
+
})]
|
|
284
|
+
}),
|
|
285
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
286
|
+
className: WebdavSyncCard_module_default.field,
|
|
287
|
+
children: [
|
|
288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
289
|
+
className: WebdavSyncCard_module_default.fieldLabel,
|
|
290
|
+
htmlFor: "webdav-sync-passphrase",
|
|
291
|
+
children: t("passphraseLabel")
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
294
|
+
className: WebdavSyncCard_module_default.fieldHint,
|
|
295
|
+
children: t("passphraseHint")
|
|
296
|
+
}),
|
|
297
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
298
|
+
id: "webdav-sync-passphrase",
|
|
299
|
+
type: "password",
|
|
300
|
+
value: state.passphrase,
|
|
301
|
+
autoComplete: "new-password",
|
|
302
|
+
placeholder: config.passphraseSet ? "••••••••" : "",
|
|
303
|
+
onChange: (event) => props.edit("passphrase", event.target.value)
|
|
304
|
+
})
|
|
305
|
+
]
|
|
306
|
+
}),
|
|
307
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
308
|
+
className: WebdavSyncCard_module_default.toggleRow,
|
|
309
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
310
|
+
checked: state.rememberPassword,
|
|
311
|
+
label: t("rememberPassword"),
|
|
312
|
+
onChange: (next) => props.toggle("rememberPassword", next)
|
|
313
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
314
|
+
className: WebdavSyncCard_module_default.fieldHint,
|
|
315
|
+
children: t("rememberPasswordHint")
|
|
316
|
+
})]
|
|
317
|
+
})
|
|
318
|
+
] }),
|
|
319
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
320
|
+
className: WebdavSyncCard_module_default.legend,
|
|
321
|
+
children: t("autoLegend")
|
|
322
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedControl, {
|
|
323
|
+
id: "webdav-sync-auto",
|
|
324
|
+
className: WebdavSyncCard_module_default.autoControl,
|
|
325
|
+
label: t("autoLegend"),
|
|
326
|
+
value: state.url.trim() === "" ? AUTO_OFF : String(state.autoEvery),
|
|
327
|
+
disabled: busy,
|
|
328
|
+
options: autoOptions(t),
|
|
329
|
+
onChange: (next) => {
|
|
330
|
+
if (next === AUTO_OFF) props.askOff();
|
|
331
|
+
else props.setAuto(Number(next));
|
|
332
|
+
}
|
|
333
|
+
})] }),
|
|
334
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
|
|
335
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
336
|
+
className: WebdavSyncCard_module_default.legend,
|
|
337
|
+
children: t("partsLegend")
|
|
338
|
+
}),
|
|
339
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
340
|
+
className: WebdavSyncCard_module_default.hint,
|
|
341
|
+
children: t("partsHint")
|
|
342
|
+
}),
|
|
343
|
+
(state.view?.parts ?? []).map((part) => {
|
|
344
|
+
const stateKey = part.synced ? partStateKey(part) : "partOff";
|
|
345
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
346
|
+
className: WebdavSyncCard_module_default.partRow,
|
|
347
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
348
|
+
className: WebdavSyncCard_module_default.partText,
|
|
349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
350
|
+
checked: part.synced,
|
|
351
|
+
label: t(PART_NAME[part.part]),
|
|
352
|
+
title: t("partOffHint"),
|
|
353
|
+
onChange: (next) => props.toggle(part.part, next)
|
|
354
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
355
|
+
className: WebdavSyncCard_module_default.partHint,
|
|
356
|
+
children: t(PART_HINT[part.part])
|
|
357
|
+
})]
|
|
358
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
359
|
+
className: stateKey === "partBoth" ? `${WebdavSyncCard_module_default.partState} ${WebdavSyncCard_module_default.partStateBoth}` : WebdavSyncCard_module_default.partState,
|
|
360
|
+
children: t(stateKey)
|
|
361
|
+
})]
|
|
362
|
+
}, part.part);
|
|
363
|
+
}),
|
|
364
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
365
|
+
className: WebdavSyncCard_module_default.hint,
|
|
366
|
+
children: state.view?.last === void 0 ? t("neverSynced") : t("lastSync", { at: whenText(state.view.last) })
|
|
367
|
+
}),
|
|
368
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
369
|
+
className: WebdavSyncCard_module_default.footer,
|
|
370
|
+
children: [
|
|
371
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
372
|
+
variant: "outline",
|
|
373
|
+
size: "sm",
|
|
374
|
+
disabled: busy,
|
|
375
|
+
onClick: () => props.check(),
|
|
376
|
+
children: state.busy === "check" ? t("working") : t("check")
|
|
377
|
+
}),
|
|
378
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
379
|
+
variant: "outline",
|
|
380
|
+
size: "sm",
|
|
381
|
+
disabled: busy,
|
|
382
|
+
onClick: () => props.preview(),
|
|
383
|
+
children: state.busy === "preview" ? t("working") : t("preview")
|
|
384
|
+
}),
|
|
385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
386
|
+
variant: "primary",
|
|
387
|
+
size: "sm",
|
|
388
|
+
disabled: busy,
|
|
389
|
+
onClick: () => props.sync(),
|
|
390
|
+
children: state.busy === "sync" ? t("working") : t("syncNow")
|
|
391
|
+
}),
|
|
392
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
393
|
+
variant: "outline",
|
|
394
|
+
size: "sm",
|
|
395
|
+
disabled: busy,
|
|
396
|
+
onClick: () => props.askRestore(),
|
|
397
|
+
children: t("restore")
|
|
398
|
+
}),
|
|
399
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
400
|
+
variant: "outline",
|
|
401
|
+
size: "sm",
|
|
402
|
+
disabled: busy || !(state.view?.undoable ?? false),
|
|
403
|
+
onClick: () => props.undo(),
|
|
404
|
+
children: t("undo")
|
|
405
|
+
}),
|
|
406
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
407
|
+
variant: "outline",
|
|
408
|
+
size: "sm",
|
|
409
|
+
disabled: busy,
|
|
410
|
+
onClick: () => props.askUpload(),
|
|
411
|
+
children: t("upload")
|
|
412
|
+
})
|
|
413
|
+
]
|
|
414
|
+
})
|
|
415
|
+
] }),
|
|
416
|
+
state.view?.notice !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
417
|
+
className: WebdavSyncCard_module_default.notice,
|
|
418
|
+
children: [
|
|
419
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
420
|
+
className: WebdavSyncCard_module_default.noticeTitle,
|
|
421
|
+
children: t("noticeTitle")
|
|
422
|
+
}),
|
|
423
|
+
state.view.notice.restored === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("noticeRestored") }),
|
|
424
|
+
state.view.notice.here.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("noticeHere", { parts: state.view.notice.here.map((part) => t(PART_NAME[part])).join("、") }) }),
|
|
425
|
+
state.view.notice.there.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("noticeThere", { parts: state.view.notice.there.map((part) => t(PART_NAME[part])).join("、") }) }),
|
|
426
|
+
state.view.notice.saved !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("noticeSaved", { dir: state.view.notice.saved }) }),
|
|
427
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
428
|
+
variant: "outline",
|
|
429
|
+
size: "sm",
|
|
430
|
+
onClick: () => props.dismiss(),
|
|
431
|
+
children: t("noticeDismiss")
|
|
432
|
+
}) })
|
|
433
|
+
]
|
|
434
|
+
}),
|
|
435
|
+
state.reloadNeeded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
436
|
+
className: WebdavSyncCard_module_default.note,
|
|
437
|
+
role: "status",
|
|
438
|
+
children: t("reloadHint")
|
|
439
|
+
}),
|
|
440
|
+
state.view?.pendingInstall === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
441
|
+
className: WebdavSyncCard_module_default.note,
|
|
442
|
+
role: "status",
|
|
443
|
+
children: [
|
|
444
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pendingInstall", { names: (state.view.pendingNames ?? []).join("、") }) }),
|
|
445
|
+
" ",
|
|
446
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
447
|
+
variant: "outline",
|
|
448
|
+
size: "sm",
|
|
449
|
+
disabled: busy,
|
|
450
|
+
onClick: () => props.install(),
|
|
451
|
+
children: t("pendingInstallAction")
|
|
452
|
+
})
|
|
453
|
+
]
|
|
454
|
+
}),
|
|
455
|
+
state.installOutput !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
456
|
+
className: state.messageFailed ? WebdavSyncCard_module_default.failed : WebdavSyncCard_module_default.note,
|
|
457
|
+
children: state.messageFailed ? t("installFailed") : t("installOk")
|
|
458
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
459
|
+
className: WebdavSyncCard_module_default.output,
|
|
460
|
+
children: state.installOutput
|
|
461
|
+
})] }),
|
|
462
|
+
state.preview !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
463
|
+
className: WebdavSyncCard_module_default.legend,
|
|
464
|
+
children: t("previewTitle")
|
|
465
|
+
}), state.preview.changes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
466
|
+
className: WebdavSyncCard_module_default.hint,
|
|
467
|
+
children: t("previewEmpty")
|
|
468
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
469
|
+
className: WebdavSyncCard_module_default.previewList,
|
|
470
|
+
children: state.preview.changes.map((change) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
471
|
+
className: WebdavSyncCard_module_default.previewRow,
|
|
472
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
473
|
+
className: WebdavSyncCard_module_default.previewPart,
|
|
474
|
+
children: t(PART_NAME[change.part])
|
|
475
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: change.take === "local" ? t("takeLocal") : change.take === "remote" ? t("takeRemote") : change.take === "newer-local" ? t("takeNewerLocal") : t("takeNewerRemote") })]
|
|
476
|
+
}, change.part))
|
|
477
|
+
})] }),
|
|
478
|
+
state.message !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
479
|
+
className: state.messageFailed ? WebdavSyncCard_module_default.failed : WebdavSyncCard_module_default.note,
|
|
480
|
+
role: state.messageFailed ? "alert" : "status",
|
|
481
|
+
children: state.message
|
|
482
|
+
}),
|
|
483
|
+
confirmText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
484
|
+
className: WebdavSyncCard_module_default.confirm,
|
|
485
|
+
children: [
|
|
486
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: confirmText }),
|
|
487
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
488
|
+
variant: "primary",
|
|
489
|
+
size: "sm",
|
|
490
|
+
disabled: busy,
|
|
491
|
+
onClick: () => props.answer(true),
|
|
492
|
+
children: t("yes")
|
|
493
|
+
}),
|
|
494
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
495
|
+
variant: "outline",
|
|
496
|
+
size: "sm",
|
|
497
|
+
onClick: () => props.answer(false),
|
|
498
|
+
children: t("no")
|
|
499
|
+
})
|
|
500
|
+
]
|
|
501
|
+
}),
|
|
502
|
+
busy && busyKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
503
|
+
className: WebdavSyncCard_module_default.note,
|
|
504
|
+
role: "status",
|
|
505
|
+
children: [
|
|
506
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
507
|
+
tone: "quiet",
|
|
508
|
+
children: t(busyKey)
|
|
509
|
+
}),
|
|
510
|
+
" ",
|
|
511
|
+
t("working")
|
|
512
|
+
]
|
|
513
|
+
}),
|
|
514
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
515
|
+
className: WebdavSyncCard_module_default.cardFooter,
|
|
516
|
+
children: [
|
|
517
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
518
|
+
className: WebdavSyncCard_module_default.hint,
|
|
519
|
+
children: t("saveLegend")
|
|
520
|
+
}),
|
|
521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: WebdavSyncCard_module_default.spacer }),
|
|
522
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
523
|
+
variant: "outline",
|
|
524
|
+
size: "sm",
|
|
525
|
+
disabled: busy || !state.dirty,
|
|
526
|
+
onClick: () => props.reset(),
|
|
527
|
+
children: t("discard")
|
|
528
|
+
}),
|
|
529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
530
|
+
variant: "primary",
|
|
531
|
+
size: "sm",
|
|
532
|
+
disabled: busy || !state.dirty,
|
|
533
|
+
onClick: () => props.save(),
|
|
534
|
+
children: state.busy === "save" ? t("saving") : t("save")
|
|
535
|
+
})
|
|
536
|
+
]
|
|
537
|
+
})
|
|
538
|
+
]
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* The card on the Plugins page: one line on the bundle's row, the form on its page.
|
|
543
|
+
*
|
|
544
|
+
* @param props - the card snapshot, its actions, and the page's `view`.
|
|
545
|
+
* @returns the summary line or the form.
|
|
546
|
+
*/
|
|
547
|
+
function WebdavSyncBundleCard(props) {
|
|
548
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
|
|
549
|
+
...props,
|
|
550
|
+
view: props.view
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The card as a Settings page. There is no summary seat here, so the form always renders.
|
|
555
|
+
*
|
|
556
|
+
* @param props - the card snapshot and its actions.
|
|
557
|
+
* @returns the form.
|
|
558
|
+
*/
|
|
559
|
+
function WebdavSyncSection(props) {
|
|
560
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
|
|
561
|
+
...props,
|
|
562
|
+
view: "page"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/client/api.ts
|
|
567
|
+
/** Send one JSON request and fold every failure into a message. */
|
|
568
|
+
async function send(method, url, body) {
|
|
569
|
+
let response;
|
|
570
|
+
try {
|
|
571
|
+
response = await fetch(url, {
|
|
572
|
+
method,
|
|
573
|
+
cache: "no-store",
|
|
574
|
+
headers: { "content-type": "application/json" },
|
|
575
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
576
|
+
});
|
|
577
|
+
} catch (cause) {
|
|
578
|
+
return {
|
|
579
|
+
ok: false,
|
|
580
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
let payload;
|
|
584
|
+
try {
|
|
585
|
+
payload = await response.json();
|
|
586
|
+
} catch {
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
error: `服务器返回了 HTTP ${response.status},但响应不是 JSON`
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (!response.ok) {
|
|
593
|
+
const text = payload?.error;
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
error: typeof text === "string" && text !== "" ? text : `服务器返回了 HTTP ${response.status}`
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
ok: true,
|
|
601
|
+
data: payload
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/** Everything the card draws from, including which parts disagree. */
|
|
605
|
+
function requestStatus() {
|
|
606
|
+
return send("GET", ROUTE_STATUS);
|
|
607
|
+
}
|
|
608
|
+
/** Store the address, account, passphrase and part switches. */
|
|
609
|
+
function requestConfigure(body) {
|
|
610
|
+
return send("POST", ROUTE_CONFIG, body);
|
|
611
|
+
}
|
|
612
|
+
/** Reach the server once and say what is there, writing nothing anywhere. */
|
|
613
|
+
function requestCheck() {
|
|
614
|
+
return send("POST", ROUTE_CHECK, {});
|
|
615
|
+
}
|
|
616
|
+
/** Say what a sync would do, part by part, without touching anything. */
|
|
617
|
+
function requestPreview() {
|
|
618
|
+
return send("POST", ROUTE_PREVIEW, {});
|
|
619
|
+
}
|
|
620
|
+
/** Merge with the file on the server. */
|
|
621
|
+
function requestSync(body = {}) {
|
|
622
|
+
return send("POST", ROUTE_SYNC, body);
|
|
623
|
+
}
|
|
624
|
+
/** Make this computer match the server, whatever changed here. */
|
|
625
|
+
function requestRestore() {
|
|
626
|
+
return send("POST", ROUTE_RESTORE, {});
|
|
627
|
+
}
|
|
628
|
+
/** Put this computer's setup back where a restore replaced it. */
|
|
629
|
+
function requestUndo() {
|
|
630
|
+
return send("POST", ROUTE_UNDO, {});
|
|
631
|
+
}
|
|
632
|
+
/** Write this computer's setup over a file that is not one of ours. */
|
|
633
|
+
function requestUpload() {
|
|
634
|
+
return send("POST", ROUTE_UPLOAD, {});
|
|
635
|
+
}
|
|
636
|
+
/** Fetch the plugins the profile now declares. */
|
|
637
|
+
function requestInstall() {
|
|
638
|
+
return send("POST", ROUTE_INSTALL, {});
|
|
639
|
+
}
|
|
640
|
+
/** Put the last sync's notice away. */
|
|
641
|
+
function requestDismiss() {
|
|
642
|
+
return send("POST", ROUTE_DISMISS, {});
|
|
643
|
+
}
|
|
644
|
+
/** Stop syncing; the file on the server stays. */
|
|
645
|
+
function requestOff() {
|
|
646
|
+
return send("POST", ROUTE_OFF, {});
|
|
647
|
+
}
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/client/card-store.ts
|
|
650
|
+
const EMPTY = {
|
|
651
|
+
phase: "loading",
|
|
652
|
+
error: "",
|
|
653
|
+
view: void 0,
|
|
654
|
+
url: "",
|
|
655
|
+
user: "",
|
|
656
|
+
password: "",
|
|
657
|
+
passphrase: "",
|
|
658
|
+
rememberPassword: true,
|
|
659
|
+
autoEvery: -1,
|
|
660
|
+
models: true,
|
|
661
|
+
defaults: true,
|
|
662
|
+
plugins: true,
|
|
663
|
+
patch: true,
|
|
664
|
+
dirty: false,
|
|
665
|
+
busy: "",
|
|
666
|
+
message: "",
|
|
667
|
+
messageFailed: false,
|
|
668
|
+
preview: void 0,
|
|
669
|
+
installOutput: "",
|
|
670
|
+
reloadNeeded: false,
|
|
671
|
+
confirm: ""
|
|
672
|
+
};
|
|
673
|
+
/** The card: an observable snapshot, the staged draft, and every write. */
|
|
674
|
+
var WebdavSyncCard = class {
|
|
675
|
+
state = EMPTY;
|
|
676
|
+
listeners = /* @__PURE__ */ new Set();
|
|
677
|
+
started = false;
|
|
678
|
+
getSnapshot() {
|
|
679
|
+
return this.state;
|
|
680
|
+
}
|
|
681
|
+
subscribe(listener) {
|
|
682
|
+
this.listeners.add(listener);
|
|
683
|
+
return () => {
|
|
684
|
+
this.listeners.delete(listener);
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/** Replace the state only on a real change, so React skips no-op renders. */
|
|
688
|
+
set(patch) {
|
|
689
|
+
const next = {
|
|
690
|
+
...this.state,
|
|
691
|
+
...patch
|
|
692
|
+
};
|
|
693
|
+
let moved = false;
|
|
694
|
+
for (const key of Object.keys(next)) if (next[key] !== this.state[key]) {
|
|
695
|
+
moved = true;
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
if (!moved) return;
|
|
699
|
+
this.state = next;
|
|
700
|
+
for (const listener of [...this.listeners]) listener();
|
|
701
|
+
}
|
|
702
|
+
/** Seed the draft from a status the host just gave. */
|
|
703
|
+
adopt(view) {
|
|
704
|
+
this.set({
|
|
705
|
+
phase: "ready",
|
|
706
|
+
view,
|
|
707
|
+
url: view.config.url,
|
|
708
|
+
user: view.config.user,
|
|
709
|
+
password: "",
|
|
710
|
+
passphrase: "",
|
|
711
|
+
rememberPassword: view.config.rememberPassword,
|
|
712
|
+
autoEvery: view.config.autoEvery,
|
|
713
|
+
models: view.config.models,
|
|
714
|
+
defaults: view.config.defaults,
|
|
715
|
+
plugins: view.config.plugins,
|
|
716
|
+
patch: view.config.patch,
|
|
717
|
+
dirty: false
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
/** Run one call and fold its outcome into the state. */
|
|
721
|
+
async run(kind, call, describe) {
|
|
722
|
+
this.set({
|
|
723
|
+
busy: kind,
|
|
724
|
+
message: "",
|
|
725
|
+
messageFailed: false
|
|
726
|
+
});
|
|
727
|
+
const answer = await call();
|
|
728
|
+
if (!answer.ok) {
|
|
729
|
+
this.set({
|
|
730
|
+
busy: "",
|
|
731
|
+
message: answer.error,
|
|
732
|
+
messageFailed: true
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
this.set({
|
|
737
|
+
busy: "",
|
|
738
|
+
...describe(answer.data)
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
ensure() {
|
|
742
|
+
if (this.started) return;
|
|
743
|
+
this.started = true;
|
|
744
|
+
this.load();
|
|
745
|
+
}
|
|
746
|
+
async load() {
|
|
747
|
+
this.set({
|
|
748
|
+
phase: this.state.view === void 0 ? "loading" : this.state.phase,
|
|
749
|
+
error: ""
|
|
750
|
+
});
|
|
751
|
+
const answer = await requestStatus();
|
|
752
|
+
if (!answer.ok) {
|
|
753
|
+
this.set({
|
|
754
|
+
phase: "ready",
|
|
755
|
+
error: answer.error,
|
|
756
|
+
view: void 0
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
this.adopt(answer.data);
|
|
761
|
+
}
|
|
762
|
+
refresh() {
|
|
763
|
+
this.load();
|
|
764
|
+
}
|
|
765
|
+
edit(field, value) {
|
|
766
|
+
this.set({
|
|
767
|
+
[field]: value,
|
|
768
|
+
dirty: true
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
toggle(field, value) {
|
|
772
|
+
this.set({
|
|
773
|
+
[field]: value,
|
|
774
|
+
dirty: true
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
setAuto(minutes) {
|
|
778
|
+
if (!AUTO_CHOICES.includes(minutes)) return;
|
|
779
|
+
this.set({
|
|
780
|
+
autoEvery: minutes,
|
|
781
|
+
dirty: true
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
reset() {
|
|
785
|
+
const view = this.state.view;
|
|
786
|
+
if (view === void 0) return;
|
|
787
|
+
this.set({
|
|
788
|
+
preview: void 0,
|
|
789
|
+
message: "",
|
|
790
|
+
messageFailed: false,
|
|
791
|
+
installOutput: "",
|
|
792
|
+
confirm: "",
|
|
793
|
+
reloadNeeded: false
|
|
794
|
+
});
|
|
795
|
+
this.adopt(view);
|
|
796
|
+
}
|
|
797
|
+
/** The draft as the host's configure body. */
|
|
798
|
+
body(extra = {}) {
|
|
799
|
+
return {
|
|
800
|
+
url: this.state.url,
|
|
801
|
+
user: this.state.user,
|
|
802
|
+
password: this.state.password,
|
|
803
|
+
passphrase: this.state.passphrase,
|
|
804
|
+
autoEvery: this.state.autoEvery,
|
|
805
|
+
models: this.state.models,
|
|
806
|
+
defaults: this.state.defaults,
|
|
807
|
+
plugins: this.state.plugins,
|
|
808
|
+
patch: this.state.patch,
|
|
809
|
+
rememberPassword: this.state.rememberPassword,
|
|
810
|
+
...extra
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
save() {
|
|
814
|
+
this.run("save", () => requestConfigure(this.body()), () => ({
|
|
815
|
+
dirty: false,
|
|
816
|
+
message: "",
|
|
817
|
+
messageFailed: false
|
|
818
|
+
})).then(() => this.load());
|
|
819
|
+
}
|
|
820
|
+
check() {
|
|
821
|
+
this.run("check", () => requestCheck(), (result) => ({
|
|
822
|
+
message: String(result.message ?? ""),
|
|
823
|
+
messageFailed: !result.ok
|
|
824
|
+
}));
|
|
825
|
+
}
|
|
826
|
+
preview() {
|
|
827
|
+
this.run("preview", () => requestPreview(), (result) => ({
|
|
828
|
+
preview: result,
|
|
829
|
+
reloadNeeded: false
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
sync() {
|
|
833
|
+
this.run("sync", () => requestSync({ force: true }), (result) => ({
|
|
834
|
+
preview: void 0,
|
|
835
|
+
reloadNeeded: result.reloadNeeded === true
|
|
836
|
+
})).then(() => this.load());
|
|
837
|
+
}
|
|
838
|
+
askRestore() {
|
|
839
|
+
this.set({ confirm: "restore" });
|
|
840
|
+
}
|
|
841
|
+
askUpload() {
|
|
842
|
+
this.set({ confirm: "upload" });
|
|
843
|
+
}
|
|
844
|
+
askOff() {
|
|
845
|
+
this.set({ confirm: "off" });
|
|
846
|
+
}
|
|
847
|
+
undo() {
|
|
848
|
+
this.run("undo", () => requestUndo(), (result) => ({
|
|
849
|
+
reloadNeeded: result.reloadNeeded === true,
|
|
850
|
+
preview: void 0
|
|
851
|
+
})).then(() => this.load());
|
|
852
|
+
}
|
|
853
|
+
install() {
|
|
854
|
+
this.run("install", () => requestInstall(), (result) => ({
|
|
855
|
+
installOutput: String(result.output ?? ""),
|
|
856
|
+
messageFailed: result.ok !== true
|
|
857
|
+
})).then(() => this.load());
|
|
858
|
+
}
|
|
859
|
+
dismiss() {
|
|
860
|
+
this.run("dismiss", () => requestDismiss(), () => ({})).then(() => this.load());
|
|
861
|
+
}
|
|
862
|
+
answer(yes) {
|
|
863
|
+
const kind = this.state.confirm;
|
|
864
|
+
this.set({ confirm: "" });
|
|
865
|
+
if (!yes) return;
|
|
866
|
+
if (kind === "restore") {
|
|
867
|
+
this.run("restore", () => requestRestore(), (result) => ({
|
|
868
|
+
reloadNeeded: result.reloadNeeded === true,
|
|
869
|
+
preview: void 0
|
|
870
|
+
})).then(() => this.load());
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (kind === "upload") {
|
|
874
|
+
this.run("upload", () => requestUpload(), (result) => ({
|
|
875
|
+
preview: void 0,
|
|
876
|
+
messageFailed: false,
|
|
877
|
+
reloadNeeded: result.reloadNeeded === true
|
|
878
|
+
})).then(() => this.load());
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (kind === "off") this.run("off", () => requestOff(), () => ({ preview: void 0 })).then(() => this.load());
|
|
882
|
+
}
|
|
883
|
+
/** Every action as plain methods, for the inject face and for tests. */
|
|
884
|
+
actions() {
|
|
885
|
+
return {
|
|
886
|
+
ensure: () => this.ensure(),
|
|
887
|
+
refresh: () => this.refresh(),
|
|
888
|
+
edit: (field, value) => this.edit(field, value),
|
|
889
|
+
toggle: (field, value) => this.toggle(field, value),
|
|
890
|
+
setAuto: (minutes) => this.setAuto(minutes),
|
|
891
|
+
reset: () => this.reset(),
|
|
892
|
+
save: () => this.save(),
|
|
893
|
+
check: () => this.check(),
|
|
894
|
+
preview: () => this.preview(),
|
|
895
|
+
sync: () => this.sync(),
|
|
896
|
+
askRestore: () => this.askRestore(),
|
|
897
|
+
undo: () => this.undo(),
|
|
898
|
+
askUpload: () => this.askUpload(),
|
|
899
|
+
askOff: () => this.askOff(),
|
|
900
|
+
answer: (yes) => this.answer(yes),
|
|
901
|
+
install: () => this.install(),
|
|
902
|
+
dismiss: () => this.dismiss()
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/client/locales.ts
|
|
908
|
+
const NS = "webdav-sync";
|
|
909
|
+
/** Chinese copy — the source dictionary. */
|
|
910
|
+
const zh = {
|
|
911
|
+
sectionTitle: "WebDAV 同步",
|
|
912
|
+
sectionSubtitle: "把本机的模型配置与已装插件同步到 WebDAV,多台机器保持一致。",
|
|
913
|
+
cardSummaryOff: "WebDAV 同步:未开启",
|
|
914
|
+
cardSummaryOn: "WebDAV 同步:{url}",
|
|
915
|
+
cardSummaryUnset: "WebDAV 同步:已开启,地址待填写",
|
|
916
|
+
addressLegend: "服务器",
|
|
917
|
+
urlLabel: "WebDAV 地址",
|
|
918
|
+
urlHint: "地址填到文件夹这一层,例如坚果云 https://dav.jianguoyun.com/dav/。",
|
|
919
|
+
userLabel: "用户名",
|
|
920
|
+
passwordLabel: "密码",
|
|
921
|
+
passphraseLabel: "口令",
|
|
922
|
+
passphraseHint: "口令在文件离开本机前用来加密,服务器看不到内容;不要与 WebDAV 密码用同一个。",
|
|
923
|
+
rememberPassword: "保存密码",
|
|
924
|
+
rememberPasswordHint: "「保存密码」关掉后,密码只在本次运行内使用,不写入本机。",
|
|
925
|
+
autoLegend: "自动同步",
|
|
926
|
+
autoManual: "手动同步",
|
|
927
|
+
autoEvery: "{n} 分钟",
|
|
928
|
+
autoHour: "{n} 小时",
|
|
929
|
+
autoOff: "关闭同步",
|
|
930
|
+
partsLegend: "同步内容",
|
|
931
|
+
partsHint: "按份合并:只在本机改动的推送出去,只在服务器改动的取回来,两边都改过的保留较新的一份,被替换的那份存到 sync 目录。",
|
|
932
|
+
partModels: "模型供应商与模型",
|
|
933
|
+
partModelsHint: "有哪些供应商、各自提供哪些模型、API key 从哪里读。",
|
|
934
|
+
partDefaults: "默认模型",
|
|
935
|
+
partDefaultsHint: "新开会话时默认用哪个供应商、哪个模型。",
|
|
936
|
+
partPlugins: "已安装插件",
|
|
937
|
+
partPluginsHint: "装了哪些插件、按什么顺序加载。插件包本身不传,每台机器自己安装。",
|
|
938
|
+
partPatch: "其余 profile 配置",
|
|
939
|
+
partPatchHint: "MCP 服务器、权限预设、界面设置、insert 块等其余配置。",
|
|
940
|
+
partSynced: "已一致",
|
|
941
|
+
partOnlyHere: "仅本机有改动",
|
|
942
|
+
partOnlyThere: "仅服务器有改动",
|
|
943
|
+
partBoth: "两边都有改动",
|
|
944
|
+
lastSync: "上次同步:{at}",
|
|
945
|
+
neverSynced: "尚未同步过",
|
|
946
|
+
noticeTitle: "上次同步替换了内容",
|
|
947
|
+
noticeHere: "服务器覆盖了本机:{parts}",
|
|
948
|
+
noticeThere: "本机覆盖了服务器:{parts}",
|
|
949
|
+
noticeSaved: "被替换的副本保存在:{dir}",
|
|
950
|
+
noticeRestored: "这次是把服务器恢复到本机。",
|
|
951
|
+
noticeDismiss: "知道了",
|
|
952
|
+
save: "保存",
|
|
953
|
+
saving: "保存中…",
|
|
954
|
+
saveLegend: "保存以上设置(服务器、自动同步、同步内容)",
|
|
955
|
+
discard: "放弃改动",
|
|
956
|
+
check: "测试连接",
|
|
957
|
+
preview: "预览",
|
|
958
|
+
syncNow: "立即同步",
|
|
959
|
+
restore: "服务器覆盖本机",
|
|
960
|
+
undo: "撤销恢复",
|
|
961
|
+
upload: "本机覆盖服务器",
|
|
962
|
+
install: "安装缺少的插件",
|
|
963
|
+
working: "处理中…",
|
|
964
|
+
reloadHint: "配置文件有改动,重新加载界面后生效。",
|
|
965
|
+
pendingInstall: "有插件尚未安装:{names}",
|
|
966
|
+
pendingInstallAction: "安装",
|
|
967
|
+
installOk: "安装完成。",
|
|
968
|
+
installFailed: "安装未成功,请看下面的输出。",
|
|
969
|
+
confirmRestore: "用服务器上的配置覆盖本机?本机现在的配置会先另存一份,之后可以撤销。",
|
|
970
|
+
confirmUpload: "用本机配置覆盖服务器上的文件?服务器上现有的文件会先另存一份。",
|
|
971
|
+
confirmOff: "关闭同步?服务器上的文件不会被删除。",
|
|
972
|
+
yes: "确定",
|
|
973
|
+
no: "取消",
|
|
974
|
+
previewEmpty: "两边一致,同步不会有任何改动。",
|
|
975
|
+
previewTitle: "这次同步会这样处理",
|
|
976
|
+
takeLocal: "推送本机的一份",
|
|
977
|
+
takeRemote: "取回服务器的一份",
|
|
978
|
+
takeNewerLocal: "两边都改过,保留本机较新的一份",
|
|
979
|
+
takeNewerRemote: "两边都改过,保留服务器较新的一份",
|
|
980
|
+
unavailable: "暂时不可用",
|
|
981
|
+
partOff: "不同步",
|
|
982
|
+
partOffHint: "关掉之后,本机不会再动这一份,也不会把它推给别的机器。"
|
|
983
|
+
};
|
|
984
|
+
/** English copy, complete by construction. */
|
|
985
|
+
const en = {
|
|
986
|
+
sectionTitle: "WebDAV sync",
|
|
987
|
+
sectionSubtitle: "Keep the model setup and installed plugins the same on every computer through a WebDAV folder.",
|
|
988
|
+
cardSummaryOff: "WebDAV sync: off",
|
|
989
|
+
cardSummaryOn: "WebDAV sync: {url}",
|
|
990
|
+
cardSummaryUnset: "WebDAV sync: on, no address yet",
|
|
991
|
+
addressLegend: "Server",
|
|
992
|
+
urlLabel: "WebDAV address",
|
|
993
|
+
urlHint: "The folder itself, e.g. Nutstore https://dav.jianguoyun.com/dav/.",
|
|
994
|
+
userLabel: "User name",
|
|
995
|
+
passwordLabel: "Password",
|
|
996
|
+
passphraseLabel: "Passphrase",
|
|
997
|
+
passphraseHint: "It seals the file before it leaves this computer, so the server cannot read it. Do not reuse the password above.",
|
|
998
|
+
rememberPassword: "Remember the password",
|
|
999
|
+
rememberPasswordHint: "With “Remember the password” off, the password is used for this run only and is not written here.",
|
|
1000
|
+
autoLegend: "Sync by itself",
|
|
1001
|
+
autoManual: "By hand",
|
|
1002
|
+
autoEvery: "Every {n} min",
|
|
1003
|
+
autoHour: "Every {n} h",
|
|
1004
|
+
autoOff: "Off",
|
|
1005
|
+
partsLegend: "What is synced",
|
|
1006
|
+
partsHint: "Merged a part at a time: a part changed only here is pushed, one changed only on the server is brought in, one changed on both keeps the newer, and the replaced copy is kept in the sync folder.",
|
|
1007
|
+
partModels: "Model providers and models",
|
|
1008
|
+
partModelsHint: "Which providers there are, what models each offers, and where their API keys come from.",
|
|
1009
|
+
partDefaults: "Default model",
|
|
1010
|
+
partDefaultsHint: "Which provider and model a new session starts on.",
|
|
1011
|
+
partPlugins: "Installed plugins",
|
|
1012
|
+
partPluginsHint: "Which plugins are installed and in what order they load. The packages themselves stay behind: each computer installs them.",
|
|
1013
|
+
partPatch: "Other profile configuration",
|
|
1014
|
+
partPatchHint: "MCP servers, permission presets, interface settings, insert blocks, and the rest.",
|
|
1015
|
+
partSynced: "In step",
|
|
1016
|
+
partOnlyHere: "Changed here only",
|
|
1017
|
+
partOnlyThere: "Changed on the server only",
|
|
1018
|
+
partBoth: "Changed on both",
|
|
1019
|
+
lastSync: "Last sync: {at}",
|
|
1020
|
+
neverSynced: "Never synced yet",
|
|
1021
|
+
noticeTitle: "The last sync replaced something",
|
|
1022
|
+
noticeHere: "The server replaced this computer: {parts}",
|
|
1023
|
+
noticeThere: "This computer replaced the server: {parts}",
|
|
1024
|
+
noticeSaved: "The replaced copies are kept in {dir}",
|
|
1025
|
+
noticeRestored: "This one took the server’s setup over this computer’s.",
|
|
1026
|
+
noticeDismiss: "Got it",
|
|
1027
|
+
save: "Save",
|
|
1028
|
+
saving: "Saving…",
|
|
1029
|
+
saveLegend: "Save the settings above (server, auto sync, what is synced)",
|
|
1030
|
+
discard: "Discard",
|
|
1031
|
+
check: "Test the connection",
|
|
1032
|
+
preview: "Preview",
|
|
1033
|
+
syncNow: "Sync now",
|
|
1034
|
+
restore: "Server onto here",
|
|
1035
|
+
undo: "Undo restore",
|
|
1036
|
+
upload: "Here onto server",
|
|
1037
|
+
install: "Install the missing plugins",
|
|
1038
|
+
working: "Working…",
|
|
1039
|
+
reloadHint: "Configuration files changed: reload the interface to pick them up.",
|
|
1040
|
+
pendingInstall: "Plugins declared but not installed: {names}",
|
|
1041
|
+
pendingInstallAction: "Install",
|
|
1042
|
+
installOk: "Installed.",
|
|
1043
|
+
installFailed: "The install did not finish; the output is below.",
|
|
1044
|
+
confirmRestore: "Take the server’s setup over this computer’s? What is here now is kept aside first, so it can be undone.",
|
|
1045
|
+
confirmUpload: "Write this computer’s setup over the file on the server? The file there is kept aside first.",
|
|
1046
|
+
confirmOff: "Stop syncing? The file on the server is not deleted.",
|
|
1047
|
+
yes: "Yes",
|
|
1048
|
+
no: "Cancel",
|
|
1049
|
+
previewEmpty: "Both sides agree: a sync would change nothing.",
|
|
1050
|
+
previewTitle: "A sync would do this",
|
|
1051
|
+
takeLocal: "Push this computer’s copy",
|
|
1052
|
+
takeRemote: "Bring in the server’s copy",
|
|
1053
|
+
takeNewerLocal: "Both changed: keep this computer’s as the newer one",
|
|
1054
|
+
takeNewerRemote: "Both changed: keep the server’s as the newer one",
|
|
1055
|
+
unavailable: "Not available",
|
|
1056
|
+
partOff: "Not synced",
|
|
1057
|
+
partOffHint: "Off: this computer neither changes this part nor hands it to the others."
|
|
1058
|
+
};
|
|
1059
|
+
//#endregion
|
|
1060
|
+
//#region src/client/index.ts
|
|
1061
|
+
/**
|
|
1062
|
+
* Host composition entry id from `cordis.patch.yml`.
|
|
1063
|
+
*
|
|
1064
|
+
* Configuration forms are addressed by it; the locale and route namespaces are
|
|
1065
|
+
* independent. Kept client-local because the browser bundle must not import the
|
|
1066
|
+
* host half.
|
|
1067
|
+
*/
|
|
1068
|
+
const PLUGIN_ENTRY_ID = "dsh-webdav-sync";
|
|
1069
|
+
/**
|
|
1070
|
+
* This package's own name — the key `plugins.bundle.config` dispatches by. It
|
|
1071
|
+
* matches the `cordis.patch.yml` entry's `name` verbatim.
|
|
1072
|
+
*/
|
|
1073
|
+
const PLUGIN_PACKAGE = "dsh-webdav-sync";
|
|
1074
|
+
/** The nav place of the Settings page. Deliberately near the top: the settings
|
|
1075
|
+
* nav column has no scrollbar and a fixed height, so a late order is clipped
|
|
1076
|
+
* out of reach in a short window. */
|
|
1077
|
+
const SECTION_ORDER = 22;
|
|
1078
|
+
const inject = [
|
|
1079
|
+
"slots",
|
|
1080
|
+
"locale",
|
|
1081
|
+
"configForms"
|
|
1082
|
+
];
|
|
1083
|
+
/**
|
|
1084
|
+
* Mount the card in both seats.
|
|
1085
|
+
*
|
|
1086
|
+
* @param ctx - the client context, with slots, locale and configForms injected.
|
|
1087
|
+
*/
|
|
1088
|
+
function apply(ctx) {
|
|
1089
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1090
|
+
zh,
|
|
1091
|
+
en
|
|
1092
|
+
}), "webdav-sync: dictionaries");
|
|
1093
|
+
const model = new WebdavSyncCard();
|
|
1094
|
+
const bound = model.actions();
|
|
1095
|
+
const t = ctx.locale.bind(NS);
|
|
1096
|
+
/**
|
|
1097
|
+
* The registration-side face both seats inject. The store rides the reserved
|
|
1098
|
+
* `hooks` compartment, so the renderer binds it as the `useWebdavSync` prop.
|
|
1099
|
+
*/
|
|
1100
|
+
const face = () => ({
|
|
1101
|
+
...bound,
|
|
1102
|
+
hooks: { webdavSync: model }
|
|
1103
|
+
});
|
|
1104
|
+
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1105
|
+
name: "settings.section",
|
|
1106
|
+
id: "webdav-sync",
|
|
1107
|
+
order: SECTION_ORDER,
|
|
1108
|
+
label: () => t("sectionTitle"),
|
|
1109
|
+
locale: NS,
|
|
1110
|
+
inject: face
|
|
1111
|
+
}, WebdavSyncSection)), "webdav-sync: settings page");
|
|
1112
|
+
let bundleCard;
|
|
1113
|
+
const seatBundleCard = () => {
|
|
1114
|
+
const snapshot = describe.getSnapshot();
|
|
1115
|
+
const served = snapshot.status === "ready" && (snapshot.view?.namespaces.some((entry) => entry.ns === PLUGIN_ENTRY_ID) ?? false);
|
|
1116
|
+
if (served && bundleCard === void 0) {
|
|
1117
|
+
bundleCard = ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
1118
|
+
name: "plugins.bundle.config",
|
|
1119
|
+
key: PLUGIN_PACKAGE,
|
|
1120
|
+
locale: NS,
|
|
1121
|
+
inject: face
|
|
1122
|
+
}, WebdavSyncBundleCard));
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (!served && bundleCard !== void 0) {
|
|
1126
|
+
bundleCard();
|
|
1127
|
+
bundleCard = void 0;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
const describe = ctx.configForms.describe();
|
|
1131
|
+
const unsubscribe = describe.subscribe(seatBundleCard);
|
|
1132
|
+
describe.ensure();
|
|
1133
|
+
seatBundleCard();
|
|
1134
|
+
ctx.effect(() => () => {
|
|
1135
|
+
unsubscribe();
|
|
1136
|
+
if (bundleCard !== void 0) bundleCard();
|
|
1137
|
+
}, "webdav-sync: bundle card seat");
|
|
1138
|
+
}
|
|
1139
|
+
//#endregion
|
|
1140
|
+
exports.apply = apply;
|
|
1141
|
+
exports.inject = inject;
|
|
1142
|
+
return module.exports;
|
|
1143
|
+
}
|
|
1144
|
+
});
|