dsh-webdav-sync 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -25
- package/lib/client.js +563 -65
- package/lib/dav.d.ts +27 -7
- package/lib/dav.js +134 -28
- package/lib/dirs.d.ts +114 -0
- package/lib/dirs.js +259 -0
- package/lib/engine.d.ts +5 -1
- package/lib/engine.js +207 -44
- package/lib/index.d.ts +7 -18
- package/lib/index.js +98 -40
- package/lib/relay.d.ts +119 -0
- package/lib/relay.js +683 -0
- package/lib/state.d.ts +14 -2
- package/lib/state.js +47 -2
- package/lib/store.d.ts +1 -1
- package/lib/store.js +8 -10
- package/lib/wire.d.ts +228 -17
- package/lib/wire.js +65 -10
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -49,9 +49,40 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
30,
|
|
50
50
|
60
|
|
51
51
|
];
|
|
52
|
+
/** Loopback port the forwarder listens on when nothing else was asked for. */
|
|
53
|
+
const RELAY_DEFAULT_PORT = 3426;
|
|
54
|
+
/**
|
|
55
|
+
* The address a client is given: loopback, the port, and the server's own path.
|
|
56
|
+
*
|
|
57
|
+
* The path is carried over so that the rest of the address means the same thing
|
|
58
|
+
* to the client as the real server's address did — a client that joins its own
|
|
59
|
+
* paths onto the host alone, and one that keeps the base path it was given, both
|
|
60
|
+
* end up asking for the same place. Pure form: the same port and target always
|
|
61
|
+
* give the same address.
|
|
62
|
+
*/
|
|
63
|
+
function relayUrl(port, basePath) {
|
|
64
|
+
return `http://127.0.0.1:${port}${basePath}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The path part of a WebDAV address, which the forwarded address carries over.
|
|
68
|
+
*
|
|
69
|
+
* `https://host/dav/` gives `/dav`; an address with no path, one that cannot be
|
|
70
|
+
* read at all, or one that is not http(s) gives ''. Both halves work out the
|
|
71
|
+
* forwarded address from this, so the line the card shows is the address the host
|
|
72
|
+
* would hand out for the same draft rather than a second opinion about it.
|
|
73
|
+
*/
|
|
74
|
+
function relayBasePath(url) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = new URL(url);
|
|
77
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "";
|
|
78
|
+
return parsed.pathname.replace(/\/+$/, "");
|
|
79
|
+
} catch {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
52
83
|
//#endregion
|
|
53
84
|
//#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";
|
|
85
|
+
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/* The two stamps share one line: the check on the left, the sync on the right.\n They wrap rather than overlap when the card is too narrow to hold both. */\n.times_318697645 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 4px 10px;\n flex-wrap: wrap;\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/*\n * The extra-headers field is the card's only multi-line input, and there is no\n * multi-line primitive to reuse. It takes the one-line input's frame instead —\n * same border, radius and background — so it reads as a field of the same\n * family, and a monospace face because what goes in it is header syntax, where\n * the alignment of a name against its value is the thing being checked. It\n * starts three lines tall and may be dragged taller.\n */\n.headersArea_318697645 {\n box-sizing: border-box;\n width: 100%;\n min-height: 66px;\n padding: 6px 8px;\n resize: vertical;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-1);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n\n.headersArea_318697645::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n.headersArea_318697645:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n/*\n * A field's label line when the field also carries an option of its own: the\n * label and the saved-password checkbox share the line, so the option reads as\n * part of the field it governs instead of as a setting of its own further down\n * the card. They sit at the two ends of that line (`space-between`): the label\n * keeps the field column's left edge, the checkbox lines up with its right edge\n * — packed together they read as one long word. The checkbox is brought down to\n * the field label's 12px and its colour for the same reason as the part rows\n * (see `.partRow_318697645 label`): a control inside a label line should not outrank the\n * line it sits in.\n */\n.labelRow_318697645 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n}\n\n.labelRow_318697645 label {\n gap: 6px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.labelRow_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/*\n * The extra directories, laid out as one more part row — checkbox, name,\n * one-line description, state — exactly like the four above, with the picker on\n * the title line right after the name and the chosen paths listed under the row.\n * Each path is a small card of its own: a bordered row that reads as something\n * carried, not as another line of prose.\n */\n\n/*\n * The picker shares the title line with the switch. It is a text button, so it\n * has to stop borrowing a control's height and padding to sit level with a 12px\n * label — and it reads quieter than the label until it is pointed at.\n */\n.titleRow_318697645 {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.titleRow_318697645 button {\n height: auto;\n min-height: 0;\n padding: 0 2px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.titleRow_318697645 button:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n\n.dirList_318697645 {\n padding: 0 0 4px;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n\n/* The group owns the separator; its heading row sits above the paths. */\n.dirList_318697645 .partRow_318697645 {\n border-bottom: none;\n align-items: center;\n}\n\n.dirRow_318697645 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0 0 4px 20px;\n padding: 4px 6px 4px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n}\n\n/* Switched off, the paths stay on screen but read as dormant. */\n.dirOff_318697645 {\n opacity: 0.6;\n}\n\n/* Mono, because a path is read character by character; a narrow box drops the\n * tail, and the whole path stays in the row's title attribute. */\n.dirPath_318697645 {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-primary);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* The row a path is typed into, when no dialog could be reached. */\n.dirRow_318697645 input {\n flex: 1;\n min-width: 0;\n}\n\n/* A configured directory this computer could not read, one line per directory. */\n.problem_318697645 {\n margin: 4px 0 0;\n font-size: 11px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\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 * With no frame to separate them, the spacing has to: each label carries 6px of\n * its own and the row adds 2px, so neighbouring labels sit 14px apart — roomy\n * enough to read as separate actions, tight enough not to break the row.\n */\n.footer_318697645 {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-wrap: wrap;\n padding-top: 4px;\n}\n\n/*\n * Every sync action is a text button: no frame, no fill, only a label. The\n * reason is the row itself — six framed buttons next to the settings above read\n * as a second form — and the risk a text button always carries: 「看不出是个\n * 按钮」. That is answered by colour and by the pointer, not by a box:\n *\n * - the label sits in the ordinary primary colour, so it reads as text that\n * happens to be clickable;\n * - the hover fill comes from `ghost` and appears under the pointer, which is\n * exactly when the question 「能点吗」 gets asked;\n * - the focus ring is drawn here because `ghost` has none of its own, and a\n * keyboard user has no pointer to discover the button with.\n *\n * `height: auto` drops the control's 28px box so six labels fit one line; the\n * 24px `min-height` keeps the hover fill from collapsing onto the glyphs. The\n * 500 weight is what makes the whole row read as one band of controls rather\n * than as a sentence that happens to end the section: at 12px, ordinary body\n * weight next to ordinary body text is the one thing a text button cannot\n * recover from with colour alone.\n */\n.footer_318697645 button.textAction_318697645 {\n height: auto;\n min-height: 24px;\n padding: 0 6px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.footer_318697645 button.textAction_318697645:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.footer_318697645 button.textAction_318697645:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n\n/*\n * The one action this part of the card is for. It is the row's only accented\n * label, which is what makes 「立即同步」 findable without a filled box; the\n * colour carries it in both themes, where a 12px label is small enough that\n * hue alone would need a second signal — here that signal is being the only\n * coloured one in an otherwise grey row, so the weight stays the row's own.\n */\n.footer_318697645 button.textActionPrimary_318697645 {\n color: var(--dsw-alias-state-business-primary);\n}\n\n.footer_318697645 button.textActionPrimary_318697645:hover:not(:disabled) {\n color: var(--dsw-alias-state-business-primary);\n}\n\n/*\n * The two actions that replace one whole side with the other: 「服务器覆盖本机」\n * and 「本机覆盖服务器」. They are the only pair in the row that can throw away\n * work on the side it writes, so they carry the warning colour while staying\n * text buttons. Not the error colour: both are reversible (a restore has\n * `undo`, an upload keeps the old file in `sync/`), and red would say otherwise.\n *\n * `warn-label`, not `warn-primary`: the design system splits these two — the\n * primary amber (#f59e0b) paints dots and tag fills, while warning *text* uses\n * the darker `label` step (#dd8629), which is what `Toast` and\n * `ConnectionIndicator` put on 12px labels exactly like this one. At 12px the\n * primary amber on a light card is barely 2:1 against the surface; the label\n * step is the token that was picked for reading, so it is the one used here.\n */\n.footer_318697645 button.textActionReplace_318697645 {\n color: var(--dsw-alias-state-warn-label);\n}\n\n.footer_318697645 button.textActionReplace_318697645:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-state-warn-label) 10%, transparent);\n color: var(--dsw-alias-state-warn-label);\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\n/*\n * The forwarder's switch and the two values it hands out.\n *\n * The switch is a checkbox like the ones that choose the parts, and it reads at\n * the size of the section titles rather than at the primitive's own 14px: one\n * line of body-sized text under a title is a control shouting over its heading.\n *\n * The address is what a reader copies, so it is plain text — monospace, because\n * a path is read character by character — with the copy icon right after it and\n * no frame: a field around a value nobody types only reads as something to fill\n * in. The account sits under it the same way: name, value, copy icon. All three\n * are named, because three copy buttons in a row are otherwise three buttons\n * that all say 「copy」.\n */\n.relaySwitch_318697645 {\n margin-bottom: 6px;\n}\n\n.relaySwitch_318697645 label {\n font-size: 12px;\n line-height: 18px;\n}\n\n.relayAddressRow_318697645,\n.relayAccountRow_318697645 {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n margin-bottom: 6px;\n}\n\n/* The two account values are two groups rather than four loose items: the wider\n * gap separates them, the narrow one binds each label to its value and icon. */\n.relayAccountRow_318697645 {\n gap: 6px 14px;\n}\n\n/* One value with its copy icon. It stays on one line, so a wrapped card can\n * never leave an icon under a value it does not copy. */\n.relayPair_318697645 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n/* Plain text rather than a field frame, but still the reader's to select: the\n * failure hint's fallback — select it and copy by hand — has to keep working.\n * It takes only the width it needs, so the copy icon sits at the end of the\n * address instead of being pushed out to the card's right edge: the icon\n * belongs to that string, and a gap would read as a separate action. */\n.relayAddress_318697645 {\n flex: 0 1 auto;\n min-width: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n overflow-wrap: anywhere;\n}\n\n/* An icon button: the primitive's frame drops away, and what is left is the\n * app's own pressable shape — the hover wash around a 16px glyph, at the 24px\n * the app's other icon actions take. */\nbutton.relayCopy_318697645 {\n flex: none;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--dsw-alias-label-secondary);\n}\n\nbutton.relayCopy_318697645:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n\n.relayLabel_318697645 {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* A value with nothing around it: the three things a reader takes away — the\n * address and the two account strings — are all set the same way, because none\n * of them is a field. A frame here would say 「fill this in」 about a string that\n * is generated and read-only. */\n.relayValue_318697645 {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n";
|
|
55
86
|
const tagId = "dsh-webdav-sync/WebdavSyncCard.module.css";
|
|
56
87
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
57
88
|
const tag = document.createElement("style");
|
|
@@ -66,20 +97,31 @@ window.__ModuleLoader__.load({
|
|
|
66
97
|
"page": "page_318697645",
|
|
67
98
|
"legend": "legend_318697645",
|
|
68
99
|
"hint": "hint_318697645",
|
|
100
|
+
"times": "times_318697645",
|
|
69
101
|
"field": "field_318697645",
|
|
70
102
|
"fieldLabel": "fieldLabel_318697645",
|
|
71
103
|
"fieldHint": "fieldHint_318697645",
|
|
72
104
|
"grid": "grid_318697645",
|
|
105
|
+
"headersArea": "headersArea_318697645",
|
|
73
106
|
"partRow": "partRow_318697645",
|
|
74
|
-
"
|
|
107
|
+
"labelRow": "labelRow_318697645",
|
|
75
108
|
"autoControl": "autoControl_318697645",
|
|
76
109
|
"partText": "partText_318697645",
|
|
77
110
|
"partHint": "partHint_318697645",
|
|
78
111
|
"partState": "partState_318697645",
|
|
79
112
|
"partStateBoth": "partStateBoth_318697645",
|
|
113
|
+
"titleRow": "titleRow_318697645",
|
|
114
|
+
"dirList": "dirList_318697645",
|
|
115
|
+
"dirRow": "dirRow_318697645",
|
|
116
|
+
"dirOff": "dirOff_318697645",
|
|
117
|
+
"dirPath": "dirPath_318697645",
|
|
118
|
+
"problem": "problem_318697645",
|
|
80
119
|
"notice": "notice_318697645",
|
|
81
120
|
"noticeTitle": "noticeTitle_318697645",
|
|
82
121
|
"footer": "footer_318697645",
|
|
122
|
+
"textAction": "textAction_318697645",
|
|
123
|
+
"textActionPrimary": "textActionPrimary_318697645",
|
|
124
|
+
"textActionReplace": "textActionReplace_318697645",
|
|
83
125
|
"spacer": "spacer_318697645",
|
|
84
126
|
"cardFooter": "cardFooter_318697645",
|
|
85
127
|
"failed": "failed_318697645",
|
|
@@ -89,9 +131,188 @@ window.__ModuleLoader__.load({
|
|
|
89
131
|
"previewRow": "previewRow_318697645",
|
|
90
132
|
"previewPart": "previewPart_318697645",
|
|
91
133
|
"output": "output_318697645",
|
|
92
|
-
"spin": "spin_318697645"
|
|
134
|
+
"spin": "spin_318697645",
|
|
135
|
+
"relaySwitch": "relaySwitch_318697645",
|
|
136
|
+
"relayAddressRow": "relayAddressRow_318697645",
|
|
137
|
+
"relayAccountRow": "relayAccountRow_318697645",
|
|
138
|
+
"relayPair": "relayPair_318697645",
|
|
139
|
+
"relayAddress": "relayAddress_318697645",
|
|
140
|
+
"relayCopy": "relayCopy_318697645",
|
|
141
|
+
"relayLabel": "relayLabel_318697645",
|
|
142
|
+
"relayValue": "relayValue_318697645"
|
|
93
143
|
};
|
|
94
144
|
//#endregion
|
|
145
|
+
//#region src/client/RelayPanel.tsx
|
|
146
|
+
/**
|
|
147
|
+
* The forwarder's section of the card.
|
|
148
|
+
*
|
|
149
|
+
* It is a section of the sync's own form, not a panel of its own: one switch, and
|
|
150
|
+
* — once it is on — the one address another program is pointed at, as plain text
|
|
151
|
+
* with a copy icon right after it, and the account that address asks for
|
|
152
|
+
* underneath it, each of its two values carrying the same copy icon. Nothing here
|
|
153
|
+
* is filled in and nothing here is saved separately: everything the forwarder uses
|
|
154
|
+
* is the configuration above it, and the account is generated.
|
|
155
|
+
*
|
|
156
|
+
* The address is worked out from the draft rather than from what the host last
|
|
157
|
+
* reported, so the line shows the address this configuration *will* hand out,
|
|
158
|
+
* following the address above as it is edited. What the host is actually running
|
|
159
|
+
* comes from the view: whether it is listening, why it is not, and the account it
|
|
160
|
+
* minted, which does not exist until the first save. The port is part of the
|
|
161
|
+
* address rather than a field of its own — a reader copies one string, and the
|
|
162
|
+
* one case where the port needs changing (some other program already holds it)
|
|
163
|
+
* is exactly the case where the host reports it.
|
|
164
|
+
*
|
|
165
|
+
* @module webdav-sync/client/RelayPanel
|
|
166
|
+
*/
|
|
167
|
+
/** How long a copy icon says it worked, matching the app's own copy buttons. */
|
|
168
|
+
const COPIED_MS = 1e3;
|
|
169
|
+
/**
|
|
170
|
+
* One copy icon: a ghost button whose glyph changes to a check for a moment.
|
|
171
|
+
*
|
|
172
|
+
* @param props - the label, whether it worked, and the write to run.
|
|
173
|
+
* @returns the button, with its tooltip.
|
|
174
|
+
*/
|
|
175
|
+
function CopyControl({ label, done, onCopy }) {
|
|
176
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
177
|
+
label,
|
|
178
|
+
side: "top",
|
|
179
|
+
portal: true,
|
|
180
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
181
|
+
variant: "ghost",
|
|
182
|
+
size: "sm",
|
|
183
|
+
className: WebdavSyncCard_module_default.relayCopy,
|
|
184
|
+
icon: done ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutlineRegular, {}),
|
|
185
|
+
"aria-label": label,
|
|
186
|
+
onClick: onCopy
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The forwarder section.
|
|
192
|
+
*
|
|
193
|
+
* @param props - the draft's switch and port, the host's view, and the dictionary.
|
|
194
|
+
* @returns the section.
|
|
195
|
+
*/
|
|
196
|
+
function RelayPanel(props) {
|
|
197
|
+
const { t, target, enabled, port, view, busy } = props;
|
|
198
|
+
const [copied, setCopied] = (0, react.useState)(void 0);
|
|
199
|
+
const revert = (0, react.useRef)(void 0);
|
|
200
|
+
const address = relayUrl(port, relayBasePath(target));
|
|
201
|
+
const account = view?.user ?? "";
|
|
202
|
+
const password = view?.password ?? "";
|
|
203
|
+
const listening = view?.listening === true && view.port === port && view.url === address;
|
|
204
|
+
const problem = view?.error ?? "";
|
|
205
|
+
(0, react.useEffect)(() => () => window.clearTimeout(revert.current), []);
|
|
206
|
+
const copy = (which, text) => {
|
|
207
|
+
(0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(text).then((ok) => {
|
|
208
|
+
setCopied({
|
|
209
|
+
target: which,
|
|
210
|
+
ok
|
|
211
|
+
});
|
|
212
|
+
window.clearTimeout(revert.current);
|
|
213
|
+
if (ok) revert.current = window.setTimeout(() => setCopied(void 0), COPIED_MS);
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
/** One value's copy icon, saying 「已复制」 for it and for nothing else. */
|
|
217
|
+
const control = (which, text, base) => {
|
|
218
|
+
const done = copied?.target === which && copied.ok;
|
|
219
|
+
const failed = copied?.target === which && !copied.ok;
|
|
220
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyControl, {
|
|
221
|
+
label: done ? t("relayCopied") : t(base),
|
|
222
|
+
done,
|
|
223
|
+
onCopy: () => copy(which, text)
|
|
224
|
+
}), failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
225
|
+
className: WebdavSyncCard_module_default.failed,
|
|
226
|
+
role: "alert",
|
|
227
|
+
children: t("relayCopyFailed")
|
|
228
|
+
})] });
|
|
229
|
+
};
|
|
230
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
|
|
231
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h4", {
|
|
232
|
+
className: WebdavSyncCard_module_default.legend,
|
|
233
|
+
children: [t("relayLegend"), enabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
234
|
+
tone: listening ? "success" : "quiet",
|
|
235
|
+
children: listening ? t("relayListening") : t("relayIdle")
|
|
236
|
+
})]
|
|
237
|
+
}),
|
|
238
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
239
|
+
className: WebdavSyncCard_module_default.relaySwitch,
|
|
240
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
241
|
+
checked: enabled,
|
|
242
|
+
label: t("relayEnable"),
|
|
243
|
+
disabled: busy,
|
|
244
|
+
onChange: (next) => props.onToggle(next)
|
|
245
|
+
})
|
|
246
|
+
}),
|
|
247
|
+
enabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
248
|
+
target.trim() === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
249
|
+
className: WebdavSyncCard_module_default.hint,
|
|
250
|
+
children: t("relayNoTarget")
|
|
251
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
252
|
+
className: WebdavSyncCard_module_default.relayAddressRow,
|
|
253
|
+
children: [
|
|
254
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
255
|
+
className: WebdavSyncCard_module_default.relayLabel,
|
|
256
|
+
children: t("relayAddressLabel")
|
|
257
|
+
}),
|
|
258
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
259
|
+
className: WebdavSyncCard_module_default.relayAddress,
|
|
260
|
+
children: address
|
|
261
|
+
}),
|
|
262
|
+
control("address", address, "relayCopyAddress")
|
|
263
|
+
]
|
|
264
|
+
}),
|
|
265
|
+
problem !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
266
|
+
className: WebdavSyncCard_module_default.relayAddressRow,
|
|
267
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
268
|
+
className: WebdavSyncCard_module_default.failed,
|
|
269
|
+
role: "alert",
|
|
270
|
+
children: problem
|
|
271
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
272
|
+
variant: "ghost",
|
|
273
|
+
size: "sm",
|
|
274
|
+
disabled: busy,
|
|
275
|
+
onClick: () => props.onNextPort(),
|
|
276
|
+
children: t("relayNextPort")
|
|
277
|
+
})]
|
|
278
|
+
}),
|
|
279
|
+
account === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
280
|
+
className: WebdavSyncCard_module_default.hint,
|
|
281
|
+
children: t("relayAccountLater")
|
|
282
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
283
|
+
className: WebdavSyncCard_module_default.relayAccountRow,
|
|
284
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
285
|
+
className: WebdavSyncCard_module_default.relayPair,
|
|
286
|
+
children: [
|
|
287
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
288
|
+
className: WebdavSyncCard_module_default.relayLabel,
|
|
289
|
+
children: t("relayUserLabel")
|
|
290
|
+
}),
|
|
291
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
292
|
+
className: WebdavSyncCard_module_default.relayValue,
|
|
293
|
+
children: account
|
|
294
|
+
}),
|
|
295
|
+
control("user", account, "relayCopyUser")
|
|
296
|
+
]
|
|
297
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
298
|
+
className: WebdavSyncCard_module_default.relayPair,
|
|
299
|
+
children: [
|
|
300
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
301
|
+
className: WebdavSyncCard_module_default.relayLabel,
|
|
302
|
+
children: t("relayPasswordLabel")
|
|
303
|
+
}),
|
|
304
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
305
|
+
className: WebdavSyncCard_module_default.relayValue,
|
|
306
|
+
children: password
|
|
307
|
+
}),
|
|
308
|
+
control("password", password, "relayCopyPassword")
|
|
309
|
+
]
|
|
310
|
+
})]
|
|
311
|
+
})
|
|
312
|
+
] })
|
|
313
|
+
] });
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
95
316
|
//#region src/client/WebdavSyncCard.tsx
|
|
96
317
|
/**
|
|
97
318
|
* The WebDAV sync card, rendered in two seats from one body:
|
|
@@ -113,17 +334,40 @@ window.__ModuleLoader__.load({
|
|
|
113
334
|
models: "partModels",
|
|
114
335
|
defaults: "partDefaults",
|
|
115
336
|
plugins: "partPlugins",
|
|
116
|
-
patch: "partPatch"
|
|
337
|
+
patch: "partPatch",
|
|
338
|
+
dirs: "partDirs"
|
|
117
339
|
};
|
|
118
340
|
/** Which dictionary key says, in a line, what a part carries. */
|
|
119
341
|
const PART_HINT = {
|
|
120
342
|
models: "partModelsHint",
|
|
121
343
|
defaults: "partDefaultsHint",
|
|
122
344
|
plugins: "partPluginsHint",
|
|
123
|
-
patch: "partPatchHint"
|
|
345
|
+
patch: "partPatchHint",
|
|
346
|
+
dirs: "partDirsHint"
|
|
124
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* The parts whose switch is a checkbox on their own row.
|
|
350
|
+
*
|
|
351
|
+
* The extra directories are a `part` too — they hash, merge and travel like the
|
|
352
|
+
* four profile parts — but there is no single boolean behind them: the list
|
|
353
|
+
* itself is the part, so it is turned off by removing every row, and its rows
|
|
354
|
+
* are edited one by one instead of behind one label. Filtering it out keeps the
|
|
355
|
+
* checkbox loop honest, and the predicate is what tells the compiler so.
|
|
356
|
+
*/
|
|
357
|
+
function isToggledPart(view) {
|
|
358
|
+
return view.part !== "dirs";
|
|
359
|
+
}
|
|
125
360
|
/** The value of the auto-sync control's first tab, which is not an interval. */
|
|
126
361
|
const AUTO_OFF = "off";
|
|
362
|
+
/**
|
|
363
|
+
* How often the card re-reads the status while it is open, in milliseconds.
|
|
364
|
+
*
|
|
365
|
+
* The host's own timer works in minutes, so anything in that range would do;
|
|
366
|
+
* this is small enough that a page left open shows a sync as soon as it happens
|
|
367
|
+
* and large enough that the host is not being asked constantly. Nothing here
|
|
368
|
+
* reaches the server: `status` reads the profile and the state file.
|
|
369
|
+
*/
|
|
370
|
+
const STATUS_EVERY_MS = 15e3;
|
|
127
371
|
/** Which dictionary key names the action in flight. */
|
|
128
372
|
const BUSY_TEXT = {
|
|
129
373
|
check: "check",
|
|
@@ -178,6 +422,11 @@ window.__ModuleLoader__.load({
|
|
|
178
422
|
(0, react.useEffect)(() => {
|
|
179
423
|
if (onPage) props.ensure();
|
|
180
424
|
}, [onPage]);
|
|
425
|
+
(0, react.useEffect)(() => {
|
|
426
|
+
if (!onPage) return void 0;
|
|
427
|
+
const timer = setInterval(() => props.poll(), STATUS_EVERY_MS);
|
|
428
|
+
return () => clearInterval(timer);
|
|
429
|
+
}, [onPage]);
|
|
181
430
|
if (state.phase === "loading") return onPage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
182
431
|
className: WebdavSyncCard_module_default.note,
|
|
183
432
|
role: "status",
|
|
@@ -203,8 +452,9 @@ window.__ModuleLoader__.load({
|
|
|
203
452
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
204
453
|
className: WebdavSyncCard_module_default.footer,
|
|
205
454
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
206
|
-
variant: "
|
|
455
|
+
variant: "ghost",
|
|
207
456
|
size: "sm",
|
|
457
|
+
className: `${WebdavSyncCard_module_default.textAction} ${WebdavSyncCard_module_default.textActionPrimary}`,
|
|
208
458
|
onClick: () => props.refresh(),
|
|
209
459
|
children: t("check")
|
|
210
460
|
})
|
|
@@ -212,14 +462,11 @@ window.__ModuleLoader__.load({
|
|
|
212
462
|
});
|
|
213
463
|
const busy = state.busy !== "";
|
|
214
464
|
const busyKey = BUSY_TEXT[state.busy];
|
|
465
|
+
const dirsPart = (state.view?.parts ?? []).find((part) => part.part === "dirs");
|
|
215
466
|
const confirmText = state.confirm === "restore" ? t("confirmRestore") : state.confirm === "upload" ? t("confirmUpload") : state.confirm === "off" ? t("confirmOff") : "";
|
|
216
467
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
217
468
|
className: WebdavSyncCard_module_default.page,
|
|
218
469
|
children: [
|
|
219
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
220
|
-
className: WebdavSyncCard_module_default.hint,
|
|
221
|
-
children: t("sectionSubtitle")
|
|
222
|
-
}),
|
|
223
470
|
state.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
224
471
|
className: WebdavSyncCard_module_default.failed,
|
|
225
472
|
role: "alert",
|
|
@@ -232,24 +479,17 @@ window.__ModuleLoader__.load({
|
|
|
232
479
|
}),
|
|
233
480
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
234
481
|
className: WebdavSyncCard_module_default.field,
|
|
235
|
-
children: [
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
]
|
|
482
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
483
|
+
className: WebdavSyncCard_module_default.fieldLabel,
|
|
484
|
+
htmlFor: "webdav-sync-url",
|
|
485
|
+
children: t("urlLabel")
|
|
486
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
487
|
+
id: "webdav-sync-url",
|
|
488
|
+
value: state.url,
|
|
489
|
+
spellCheck: false,
|
|
490
|
+
placeholder: "https://dav.jianguoyun.com/dav/",
|
|
491
|
+
onChange: (event) => props.edit("url", event.target.value)
|
|
492
|
+
})]
|
|
253
493
|
}),
|
|
254
494
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
255
495
|
className: WebdavSyncCard_module_default.grid,
|
|
@@ -268,10 +508,17 @@ window.__ModuleLoader__.load({
|
|
|
268
508
|
})]
|
|
269
509
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
270
510
|
className: WebdavSyncCard_module_default.field,
|
|
271
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
272
|
-
className: WebdavSyncCard_module_default.
|
|
273
|
-
|
|
274
|
-
|
|
511
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
512
|
+
className: WebdavSyncCard_module_default.labelRow,
|
|
513
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
514
|
+
className: WebdavSyncCard_module_default.fieldLabel,
|
|
515
|
+
htmlFor: "webdav-sync-password",
|
|
516
|
+
children: t("passwordLabel")
|
|
517
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
518
|
+
checked: state.rememberPassword,
|
|
519
|
+
label: t("rememberPassword"),
|
|
520
|
+
onChange: (next) => props.toggle("rememberPassword", next)
|
|
521
|
+
})]
|
|
275
522
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
276
523
|
id: "webdav-sync-password",
|
|
277
524
|
type: "password",
|
|
@@ -305,14 +552,20 @@ window.__ModuleLoader__.load({
|
|
|
305
552
|
]
|
|
306
553
|
}),
|
|
307
554
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
308
|
-
className: WebdavSyncCard_module_default.
|
|
309
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
314
|
-
|
|
315
|
-
|
|
555
|
+
className: WebdavSyncCard_module_default.field,
|
|
556
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
557
|
+
className: WebdavSyncCard_module_default.fieldLabel,
|
|
558
|
+
htmlFor: "webdav-sync-headers",
|
|
559
|
+
children: t("headersLabel")
|
|
560
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
561
|
+
id: "webdav-sync-headers",
|
|
562
|
+
className: WebdavSyncCard_module_default.headersArea,
|
|
563
|
+
rows: 3,
|
|
564
|
+
spellCheck: false,
|
|
565
|
+
autoComplete: "off",
|
|
566
|
+
placeholder: t("headersPlaceholder"),
|
|
567
|
+
value: state.headers,
|
|
568
|
+
onChange: (event) => props.edit("headers", event.target.value)
|
|
316
569
|
})]
|
|
317
570
|
})
|
|
318
571
|
] }),
|
|
@@ -336,11 +589,7 @@ window.__ModuleLoader__.load({
|
|
|
336
589
|
className: WebdavSyncCard_module_default.legend,
|
|
337
590
|
children: t("partsLegend")
|
|
338
591
|
}),
|
|
339
|
-
|
|
340
|
-
className: WebdavSyncCard_module_default.hint,
|
|
341
|
-
children: t("partsHint")
|
|
342
|
-
}),
|
|
343
|
-
(state.view?.parts ?? []).map((part) => {
|
|
592
|
+
(state.view?.parts ?? []).filter(isToggledPart).map((part) => {
|
|
344
593
|
const stateKey = part.synced ? partStateKey(part) : "partOff";
|
|
345
594
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
346
595
|
className: WebdavSyncCard_module_default.partRow,
|
|
@@ -361,51 +610,122 @@ window.__ModuleLoader__.load({
|
|
|
361
610
|
})]
|
|
362
611
|
}, part.part);
|
|
363
612
|
}),
|
|
364
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
365
|
-
className: WebdavSyncCard_module_default.
|
|
366
|
-
children:
|
|
613
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
614
|
+
className: WebdavSyncCard_module_default.dirList,
|
|
615
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
616
|
+
className: WebdavSyncCard_module_default.partRow,
|
|
617
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
618
|
+
className: WebdavSyncCard_module_default.partText,
|
|
619
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
620
|
+
className: WebdavSyncCard_module_default.titleRow,
|
|
621
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
622
|
+
checked: state.dirsEnabled,
|
|
623
|
+
label: t("partDirs"),
|
|
624
|
+
title: t("partOffHint"),
|
|
625
|
+
onChange: (next) => props.toggle("dirsEnabled", next)
|
|
626
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
627
|
+
variant: "ghost",
|
|
628
|
+
size: "sm",
|
|
629
|
+
disabled: busy || state.dirs.length >= 5,
|
|
630
|
+
onClick: () => props.pickDir(),
|
|
631
|
+
children: t("chooseDir")
|
|
632
|
+
})]
|
|
633
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
634
|
+
className: WebdavSyncCard_module_default.partHint,
|
|
635
|
+
children: t("partDirsHint")
|
|
636
|
+
})]
|
|
637
|
+
}), dirsPart !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
638
|
+
className: WebdavSyncCard_module_default.partState,
|
|
639
|
+
children: t(dirsPart.synced ? partStateKey(dirsPart) : "partOff")
|
|
640
|
+
})]
|
|
641
|
+
}), state.dirs.map((path, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
642
|
+
className: state.dirsEnabled ? WebdavSyncCard_module_default.dirRow : `${WebdavSyncCard_module_default.dirRow} ${WebdavSyncCard_module_default.dirOff}`,
|
|
643
|
+
children: [path === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
644
|
+
id: `webdav-sync-dir-${index}`,
|
|
645
|
+
value: path,
|
|
646
|
+
spellCheck: false,
|
|
647
|
+
autoComplete: "off",
|
|
648
|
+
placeholder: t("dirPlaceholder"),
|
|
649
|
+
disabled: busy,
|
|
650
|
+
onChange: (event) => props.editDir(index, event.target.value)
|
|
651
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
652
|
+
className: WebdavSyncCard_module_default.dirPath,
|
|
653
|
+
title: path,
|
|
654
|
+
children: path
|
|
655
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
656
|
+
variant: "outline",
|
|
657
|
+
size: "sm",
|
|
658
|
+
disabled: busy,
|
|
659
|
+
onClick: () => props.removeDir(index),
|
|
660
|
+
children: t("dirRemove")
|
|
661
|
+
})]
|
|
662
|
+
}, index))]
|
|
663
|
+
}),
|
|
664
|
+
(state.view?.dirProblems ?? []).map((problem) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
665
|
+
className: WebdavSyncCard_module_default.problem,
|
|
666
|
+
role: "status",
|
|
667
|
+
children: t("dirProblem", {
|
|
668
|
+
path: problem.path,
|
|
669
|
+
message: problem.message
|
|
670
|
+
})
|
|
671
|
+
}, problem.path)),
|
|
672
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
673
|
+
className: WebdavSyncCard_module_default.times,
|
|
674
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
675
|
+
className: WebdavSyncCard_module_default.hint,
|
|
676
|
+
children: state.view?.last === void 0 ? t("neverChecked") : t("lastCheck", { at: whenText(state.view.last) })
|
|
677
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
678
|
+
className: WebdavSyncCard_module_default.hint,
|
|
679
|
+
children: state.view?.moved === void 0 ? t("neverSynced") : t("lastSync", { at: whenText(state.view.moved) })
|
|
680
|
+
})]
|
|
367
681
|
}),
|
|
368
682
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
369
683
|
className: WebdavSyncCard_module_default.footer,
|
|
370
684
|
children: [
|
|
371
685
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
372
|
-
variant: "
|
|
686
|
+
variant: "ghost",
|
|
373
687
|
size: "sm",
|
|
688
|
+
className: WebdavSyncCard_module_default.textAction,
|
|
374
689
|
disabled: busy,
|
|
375
690
|
onClick: () => props.check(),
|
|
376
691
|
children: state.busy === "check" ? t("working") : t("check")
|
|
377
692
|
}),
|
|
378
693
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
379
|
-
variant: "
|
|
694
|
+
variant: "ghost",
|
|
380
695
|
size: "sm",
|
|
696
|
+
className: WebdavSyncCard_module_default.textAction,
|
|
381
697
|
disabled: busy,
|
|
382
698
|
onClick: () => props.preview(),
|
|
383
699
|
children: state.busy === "preview" ? t("working") : t("preview")
|
|
384
700
|
}),
|
|
385
701
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
386
|
-
variant: "
|
|
702
|
+
variant: "ghost",
|
|
387
703
|
size: "sm",
|
|
704
|
+
className: `${WebdavSyncCard_module_default.textAction} ${WebdavSyncCard_module_default.textActionPrimary}`,
|
|
388
705
|
disabled: busy,
|
|
389
706
|
onClick: () => props.sync(),
|
|
390
707
|
children: state.busy === "sync" ? t("working") : t("syncNow")
|
|
391
708
|
}),
|
|
392
709
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
393
|
-
variant: "
|
|
710
|
+
variant: "ghost",
|
|
394
711
|
size: "sm",
|
|
712
|
+
className: `${WebdavSyncCard_module_default.textAction} ${WebdavSyncCard_module_default.textActionReplace}`,
|
|
395
713
|
disabled: busy,
|
|
396
714
|
onClick: () => props.askRestore(),
|
|
397
715
|
children: t("restore")
|
|
398
716
|
}),
|
|
399
717
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
400
|
-
variant: "
|
|
718
|
+
variant: "ghost",
|
|
401
719
|
size: "sm",
|
|
720
|
+
className: WebdavSyncCard_module_default.textAction,
|
|
402
721
|
disabled: busy || !(state.view?.undoable ?? false),
|
|
403
722
|
onClick: () => props.undo(),
|
|
404
723
|
children: t("undo")
|
|
405
724
|
}),
|
|
406
725
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
407
|
-
variant: "
|
|
726
|
+
variant: "ghost",
|
|
408
727
|
size: "sm",
|
|
728
|
+
className: `${WebdavSyncCard_module_default.textAction} ${WebdavSyncCard_module_default.textActionReplace}`,
|
|
409
729
|
disabled: busy,
|
|
410
730
|
onClick: () => props.askUpload(),
|
|
411
731
|
children: t("upload")
|
|
@@ -511,6 +831,16 @@ window.__ModuleLoader__.load({
|
|
|
511
831
|
t("working")
|
|
512
832
|
]
|
|
513
833
|
}),
|
|
834
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RelayPanel, {
|
|
835
|
+
t,
|
|
836
|
+
target: state.url,
|
|
837
|
+
enabled: state.relayEnabled,
|
|
838
|
+
port: state.relayPort,
|
|
839
|
+
view: state.view?.relay,
|
|
840
|
+
busy,
|
|
841
|
+
onToggle: (next) => props.toggle("relayEnabled", next),
|
|
842
|
+
onNextPort: () => props.nextPort()
|
|
843
|
+
}),
|
|
514
844
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
515
845
|
className: WebdavSyncCard_module_default.cardFooter,
|
|
516
846
|
children: [
|
|
@@ -655,12 +985,17 @@ window.__ModuleLoader__.load({
|
|
|
655
985
|
user: "",
|
|
656
986
|
password: "",
|
|
657
987
|
passphrase: "",
|
|
988
|
+
headers: "",
|
|
989
|
+
dirs: [],
|
|
990
|
+
dirsEnabled: true,
|
|
658
991
|
rememberPassword: true,
|
|
659
992
|
autoEvery: -1,
|
|
660
993
|
models: true,
|
|
661
994
|
defaults: true,
|
|
662
995
|
plugins: true,
|
|
663
996
|
patch: true,
|
|
997
|
+
relayEnabled: false,
|
|
998
|
+
relayPort: RELAY_DEFAULT_PORT,
|
|
664
999
|
dirty: false,
|
|
665
1000
|
busy: "",
|
|
666
1001
|
message: "",
|
|
@@ -675,6 +1010,16 @@ window.__ModuleLoader__.load({
|
|
|
675
1010
|
state = EMPTY;
|
|
676
1011
|
listeners = /* @__PURE__ */ new Set();
|
|
677
1012
|
started = false;
|
|
1013
|
+
/** The directory dialog of this deployment, when it has one. */
|
|
1014
|
+
choose;
|
|
1015
|
+
/**
|
|
1016
|
+
* @param choose - how this deployment picks a directory. Left out, the card
|
|
1017
|
+
* still works: a pick then leaves a blank row the path is typed into, which
|
|
1018
|
+
* is also what a deployment with no reachable dialog gets.
|
|
1019
|
+
*/
|
|
1020
|
+
constructor(choose) {
|
|
1021
|
+
this.choose = choose;
|
|
1022
|
+
}
|
|
678
1023
|
getSnapshot() {
|
|
679
1024
|
return this.state;
|
|
680
1025
|
}
|
|
@@ -708,12 +1053,17 @@ window.__ModuleLoader__.load({
|
|
|
708
1053
|
user: view.config.user,
|
|
709
1054
|
password: "",
|
|
710
1055
|
passphrase: "",
|
|
1056
|
+
headers: view.config.headers,
|
|
1057
|
+
dirs: [...view.config.dirs ?? []],
|
|
1058
|
+
dirsEnabled: view.config.dirsEnabled ?? true,
|
|
711
1059
|
rememberPassword: view.config.rememberPassword,
|
|
712
1060
|
autoEvery: view.config.autoEvery,
|
|
713
1061
|
models: view.config.models,
|
|
714
1062
|
defaults: view.config.defaults,
|
|
715
1063
|
plugins: view.config.plugins,
|
|
716
1064
|
patch: view.config.patch,
|
|
1065
|
+
relayEnabled: view.config.relayEnabled ?? false,
|
|
1066
|
+
relayPort: view.config.relayPort ?? 3426,
|
|
717
1067
|
dirty: false
|
|
718
1068
|
});
|
|
719
1069
|
}
|
|
@@ -762,6 +1112,31 @@ window.__ModuleLoader__.load({
|
|
|
762
1112
|
refresh() {
|
|
763
1113
|
this.load();
|
|
764
1114
|
}
|
|
1115
|
+
/**
|
|
1116
|
+
* A read the person did not ask for, for a timer. A first read already in
|
|
1117
|
+
* flight is allowed to land rather than asked for twice, and nothing here ever
|
|
1118
|
+
* reports a failure: the buttons are the paths that say what went wrong, and a
|
|
1119
|
+
* read that quietly could not be answered leaves the card showing the last
|
|
1120
|
+
* thing the host did say.
|
|
1121
|
+
*/
|
|
1122
|
+
poll() {
|
|
1123
|
+
if (this.state.phase === "loading") return;
|
|
1124
|
+
this.reloadStatus();
|
|
1125
|
+
}
|
|
1126
|
+
/** Re-read the status and fold it in without disturbing what is being typed. */
|
|
1127
|
+
async reloadStatus() {
|
|
1128
|
+
const answer = await requestStatus();
|
|
1129
|
+
if (!answer.ok) return;
|
|
1130
|
+
if (this.state.dirty) {
|
|
1131
|
+
this.set({
|
|
1132
|
+
view: answer.data,
|
|
1133
|
+
error: ""
|
|
1134
|
+
});
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
this.set({ error: "" });
|
|
1138
|
+
this.adopt(answer.data);
|
|
1139
|
+
}
|
|
765
1140
|
edit(field, value) {
|
|
766
1141
|
this.set({
|
|
767
1142
|
[field]: value,
|
|
@@ -774,6 +1149,50 @@ window.__ModuleLoader__.load({
|
|
|
774
1149
|
dirty: true
|
|
775
1150
|
});
|
|
776
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Ask for a directory and stage it.
|
|
1154
|
+
*
|
|
1155
|
+
* A dialog that opened and was cancelled stages nothing: a cancel must not
|
|
1156
|
+
* leave a row behind. A dialog that cannot be reached at all leaves one blank
|
|
1157
|
+
* row instead, which is the one way left to name a path by hand.
|
|
1158
|
+
*/
|
|
1159
|
+
pickDir() {
|
|
1160
|
+
if (this.state.dirs.length >= 5) return;
|
|
1161
|
+
const choose = this.choose;
|
|
1162
|
+
if (choose === void 0) {
|
|
1163
|
+
this.appendDir("");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
choose().then((chosen) => {
|
|
1167
|
+
const path = (chosen ?? "").trim();
|
|
1168
|
+
if (path !== "") this.appendDir(path);
|
|
1169
|
+
}, () => this.appendDir(""));
|
|
1170
|
+
}
|
|
1171
|
+
/** Stage a row at the end of the list, unless that exact path is already on it. */
|
|
1172
|
+
appendDir(path) {
|
|
1173
|
+
if (this.state.dirs.length >= 5) return;
|
|
1174
|
+
if (path !== "" && this.state.dirs.includes(path)) return;
|
|
1175
|
+
this.set({
|
|
1176
|
+
dirs: [...this.state.dirs, path],
|
|
1177
|
+
dirty: true
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
editDir(index, path) {
|
|
1181
|
+
if (index < 0 || index >= this.state.dirs.length) return;
|
|
1182
|
+
const dirs = [...this.state.dirs];
|
|
1183
|
+
dirs[index] = path;
|
|
1184
|
+
this.set({
|
|
1185
|
+
dirs,
|
|
1186
|
+
dirty: true
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
removeDir(index) {
|
|
1190
|
+
if (index < 0 || index >= this.state.dirs.length) return;
|
|
1191
|
+
this.set({
|
|
1192
|
+
dirs: this.state.dirs.filter((_, at) => at !== index),
|
|
1193
|
+
dirty: true
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
777
1196
|
setAuto(minutes) {
|
|
778
1197
|
if (!AUTO_CHOICES.includes(minutes)) return;
|
|
779
1198
|
this.set({
|
|
@@ -781,6 +1200,13 @@ window.__ModuleLoader__.load({
|
|
|
781
1200
|
dirty: true
|
|
782
1201
|
});
|
|
783
1202
|
}
|
|
1203
|
+
nextPort() {
|
|
1204
|
+
if (this.state.relayPort >= 65535) return;
|
|
1205
|
+
this.set({
|
|
1206
|
+
relayPort: this.state.relayPort + 1,
|
|
1207
|
+
dirty: true
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
784
1210
|
reset() {
|
|
785
1211
|
const view = this.state.view;
|
|
786
1212
|
if (view === void 0) return;
|
|
@@ -801,12 +1227,17 @@ window.__ModuleLoader__.load({
|
|
|
801
1227
|
user: this.state.user,
|
|
802
1228
|
password: this.state.password,
|
|
803
1229
|
passphrase: this.state.passphrase,
|
|
1230
|
+
headers: this.state.headers,
|
|
804
1231
|
autoEvery: this.state.autoEvery,
|
|
805
1232
|
models: this.state.models,
|
|
806
1233
|
defaults: this.state.defaults,
|
|
807
1234
|
plugins: this.state.plugins,
|
|
808
1235
|
patch: this.state.patch,
|
|
1236
|
+
dirs: this.state.dirs,
|
|
1237
|
+
dirsEnabled: this.state.dirsEnabled,
|
|
809
1238
|
rememberPassword: this.state.rememberPassword,
|
|
1239
|
+
relayEnabled: this.state.relayEnabled,
|
|
1240
|
+
relayPort: this.state.relayPort,
|
|
810
1241
|
...extra
|
|
811
1242
|
};
|
|
812
1243
|
}
|
|
@@ -885,9 +1316,14 @@ window.__ModuleLoader__.load({
|
|
|
885
1316
|
return {
|
|
886
1317
|
ensure: () => this.ensure(),
|
|
887
1318
|
refresh: () => this.refresh(),
|
|
1319
|
+
poll: () => this.poll(),
|
|
888
1320
|
edit: (field, value) => this.edit(field, value),
|
|
889
1321
|
toggle: (field, value) => this.toggle(field, value),
|
|
1322
|
+
pickDir: () => this.pickDir(),
|
|
1323
|
+
editDir: (index, path) => this.editDir(index, path),
|
|
1324
|
+
removeDir: (index) => this.removeDir(index),
|
|
890
1325
|
setAuto: (minutes) => this.setAuto(minutes),
|
|
1326
|
+
nextPort: () => this.nextPort(),
|
|
891
1327
|
reset: () => this.reset(),
|
|
892
1328
|
save: () => this.save(),
|
|
893
1329
|
check: () => this.check(),
|
|
@@ -904,31 +1340,47 @@ window.__ModuleLoader__.load({
|
|
|
904
1340
|
}
|
|
905
1341
|
};
|
|
906
1342
|
//#endregion
|
|
1343
|
+
//#region src/client/picker.ts
|
|
1344
|
+
/**
|
|
1345
|
+
* Build the chooser for whatever this deployment has.
|
|
1346
|
+
*
|
|
1347
|
+
* @param ctx - the client context, used only to look the workspace service up.
|
|
1348
|
+
* @returns the chooser; it rejects when no dialog can be reached.
|
|
1349
|
+
*/
|
|
1350
|
+
function directoryChooser(ctx) {
|
|
1351
|
+
const lookup = ctx.get.bind(ctx);
|
|
1352
|
+
return async () => {
|
|
1353
|
+
const bridge = globalThis.__DSH_DIRECTORY_PICKER__;
|
|
1354
|
+
if (typeof bridge?.pick === "function") return await bridge.pick();
|
|
1355
|
+
const workspace = lookup("uiWorkspace");
|
|
1356
|
+
if (typeof workspace?.pickDirectory === "function") return await workspace.pickDirectory();
|
|
1357
|
+
throw new Error("no directory chooser is available here");
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
//#endregion
|
|
907
1361
|
//#region src/client/locales.ts
|
|
908
1362
|
const NS = "webdav-sync";
|
|
909
1363
|
/** Chinese copy — the source dictionary. */
|
|
910
1364
|
const zh = {
|
|
911
1365
|
sectionTitle: "WebDAV 同步",
|
|
912
|
-
sectionSubtitle: "把本机的模型配置与已装插件同步到 WebDAV,多台机器保持一致。",
|
|
913
1366
|
cardSummaryOff: "WebDAV 同步:未开启",
|
|
914
1367
|
cardSummaryOn: "WebDAV 同步:{url}",
|
|
915
1368
|
cardSummaryUnset: "WebDAV 同步:已开启,地址待填写",
|
|
916
1369
|
addressLegend: "服务器",
|
|
917
1370
|
urlLabel: "WebDAV 地址",
|
|
918
|
-
urlHint: "地址填到文件夹这一层,例如坚果云 https://dav.jianguoyun.com/dav/。",
|
|
919
1371
|
userLabel: "用户名",
|
|
920
1372
|
passwordLabel: "密码",
|
|
921
1373
|
passphraseLabel: "口令",
|
|
922
1374
|
passphraseHint: "口令在文件离开本机前用来加密,服务器看不到内容;不要与 WebDAV 密码用同一个。",
|
|
1375
|
+
headersLabel: "额外请求头",
|
|
1376
|
+
headersPlaceholder: "User-Agent: Zotero/7.0\nAuthorization: Bearer xxxxx",
|
|
923
1377
|
rememberPassword: "保存密码",
|
|
924
|
-
rememberPasswordHint: "「保存密码」关掉后,密码只在本次运行内使用,不写入本机。",
|
|
925
1378
|
autoLegend: "自动同步",
|
|
926
1379
|
autoManual: "手动同步",
|
|
927
1380
|
autoEvery: "{n} 分钟",
|
|
928
1381
|
autoHour: "{n} 小时",
|
|
929
1382
|
autoOff: "关闭同步",
|
|
930
1383
|
partsLegend: "同步内容",
|
|
931
|
-
partsHint: "按份合并:只在本机改动的推送出去,只在服务器改动的取回来,两边都改过的保留较新的一份,被替换的那份存到 sync 目录。",
|
|
932
1384
|
partModels: "模型供应商与模型",
|
|
933
1385
|
partModelsHint: "有哪些供应商、各自提供哪些模型、API key 从哪里读。",
|
|
934
1386
|
partDefaults: "默认模型",
|
|
@@ -937,10 +1389,14 @@ window.__ModuleLoader__.load({
|
|
|
937
1389
|
partPluginsHint: "装了哪些插件、按什么顺序加载。插件包本身不传,每台机器自己安装。",
|
|
938
1390
|
partPatch: "其余 profile 配置",
|
|
939
1391
|
partPatchHint: "MCP 服务器、权限预设、界面设置、insert 块等其余配置。",
|
|
1392
|
+
partDirs: "附加目录",
|
|
1393
|
+
partDirsHint: "profile 之外的目录,连里面的文件一起同步;路径跟着这份配置走,恢复时写回同一个目录。",
|
|
940
1394
|
partSynced: "已一致",
|
|
941
1395
|
partOnlyHere: "仅本机有改动",
|
|
942
1396
|
partOnlyThere: "仅服务器有改动",
|
|
943
1397
|
partBoth: "两边都有改动",
|
|
1398
|
+
lastCheck: "上次检查:{at}",
|
|
1399
|
+
neverChecked: "尚未检查过",
|
|
944
1400
|
lastSync: "上次同步:{at}",
|
|
945
1401
|
neverSynced: "尚未同步过",
|
|
946
1402
|
noticeTitle: "上次同步替换了内容",
|
|
@@ -979,31 +1435,48 @@ window.__ModuleLoader__.load({
|
|
|
979
1435
|
takeNewerRemote: "两边都改过,保留服务器较新的一份",
|
|
980
1436
|
unavailable: "暂时不可用",
|
|
981
1437
|
partOff: "不同步",
|
|
982
|
-
partOffHint: "关掉之后,本机不会再动这一份,也不会把它推给别的机器。"
|
|
1438
|
+
partOffHint: "关掉之后,本机不会再动这一份,也不会把它推给别的机器。",
|
|
1439
|
+
chooseDir: "选择目录",
|
|
1440
|
+
dirPlaceholder: "这个目录的绝对路径",
|
|
1441
|
+
dirRemove: "移除",
|
|
1442
|
+
dirProblem: "「{path}」这个目录没读成:{message}",
|
|
1443
|
+
relayLegend: "本机转发",
|
|
1444
|
+
relayEnable: "启用本机转发",
|
|
1445
|
+
relayListening: "正在转发",
|
|
1446
|
+
relayIdle: "没有在转发",
|
|
1447
|
+
relayNoTarget: "先在上面把 WebDAV 地址填好",
|
|
1448
|
+
relayNextPort: "换一个端口",
|
|
1449
|
+
relayAccountLater: "保存一次后生成给客户端用的账号密码。",
|
|
1450
|
+
relayAddressLabel: "地址",
|
|
1451
|
+
relayUserLabel: "用户名",
|
|
1452
|
+
relayPasswordLabel: "密码",
|
|
1453
|
+
relayCopyAddress: "复制地址",
|
|
1454
|
+
relayCopyUser: "复制用户名",
|
|
1455
|
+
relayCopyPassword: "复制密码",
|
|
1456
|
+
relayCopied: "已复制",
|
|
1457
|
+
relayCopyFailed: "复制没成功,请手动选中复制"
|
|
983
1458
|
};
|
|
984
1459
|
/** English copy, complete by construction. */
|
|
985
1460
|
const en = {
|
|
986
1461
|
sectionTitle: "WebDAV sync",
|
|
987
|
-
sectionSubtitle: "Keep the model setup and installed plugins the same on every computer through a WebDAV folder.",
|
|
988
1462
|
cardSummaryOff: "WebDAV sync: off",
|
|
989
1463
|
cardSummaryOn: "WebDAV sync: {url}",
|
|
990
1464
|
cardSummaryUnset: "WebDAV sync: on, no address yet",
|
|
991
1465
|
addressLegend: "Server",
|
|
992
1466
|
urlLabel: "WebDAV address",
|
|
993
|
-
urlHint: "The folder itself, e.g. Nutstore https://dav.jianguoyun.com/dav/.",
|
|
994
1467
|
userLabel: "User name",
|
|
995
1468
|
passwordLabel: "Password",
|
|
996
1469
|
passphraseLabel: "Passphrase",
|
|
997
1470
|
passphraseHint: "It seals the file before it leaves this computer, so the server cannot read it. Do not reuse the password above.",
|
|
1471
|
+
headersLabel: "Extra request headers",
|
|
1472
|
+
headersPlaceholder: "User-Agent: Zotero/7.0\nAuthorization: Bearer xxxxx",
|
|
998
1473
|
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
1474
|
autoLegend: "Sync by itself",
|
|
1001
1475
|
autoManual: "By hand",
|
|
1002
1476
|
autoEvery: "Every {n} min",
|
|
1003
1477
|
autoHour: "Every {n} h",
|
|
1004
1478
|
autoOff: "Off",
|
|
1005
1479
|
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
1480
|
partModels: "Model providers and models",
|
|
1008
1481
|
partModelsHint: "Which providers there are, what models each offers, and where their API keys come from.",
|
|
1009
1482
|
partDefaults: "Default model",
|
|
@@ -1012,10 +1485,14 @@ window.__ModuleLoader__.load({
|
|
|
1012
1485
|
partPluginsHint: "Which plugins are installed and in what order they load. The packages themselves stay behind: each computer installs them.",
|
|
1013
1486
|
partPatch: "Other profile configuration",
|
|
1014
1487
|
partPatchHint: "MCP servers, permission presets, interface settings, insert blocks, and the rest.",
|
|
1488
|
+
partDirs: "Extra directories",
|
|
1489
|
+
partDirsHint: "Directories outside the profile, carried with the files inside them; the paths travel with this configuration, and a restore writes back into the same directory.",
|
|
1015
1490
|
partSynced: "In step",
|
|
1016
1491
|
partOnlyHere: "Changed here only",
|
|
1017
1492
|
partOnlyThere: "Changed on the server only",
|
|
1018
1493
|
partBoth: "Changed on both",
|
|
1494
|
+
lastCheck: "Last check: {at}",
|
|
1495
|
+
neverChecked: "Never checked yet",
|
|
1019
1496
|
lastSync: "Last sync: {at}",
|
|
1020
1497
|
neverSynced: "Never synced yet",
|
|
1021
1498
|
noticeTitle: "The last sync replaced something",
|
|
@@ -1054,7 +1531,26 @@ window.__ModuleLoader__.load({
|
|
|
1054
1531
|
takeNewerRemote: "Both changed: keep the server’s as the newer one",
|
|
1055
1532
|
unavailable: "Not available",
|
|
1056
1533
|
partOff: "Not synced",
|
|
1057
|
-
partOffHint: "Off: this computer neither changes this part nor hands it to the others."
|
|
1534
|
+
partOffHint: "Off: this computer neither changes this part nor hands it to the others.",
|
|
1535
|
+
chooseDir: "Choose directory",
|
|
1536
|
+
dirPlaceholder: "Absolute path of the directory",
|
|
1537
|
+
dirRemove: "Remove",
|
|
1538
|
+
dirProblem: "Could not read the directory {path}: {message}",
|
|
1539
|
+
relayLegend: "Forwarding on this computer",
|
|
1540
|
+
relayEnable: "Forward for other programs on this computer",
|
|
1541
|
+
relayListening: "Forwarding",
|
|
1542
|
+
relayIdle: "Not forwarding",
|
|
1543
|
+
relayNoTarget: "Fill in the WebDAV address above first",
|
|
1544
|
+
relayNextPort: "Another port",
|
|
1545
|
+
relayAccountLater: "The account for the client is made on the first save.",
|
|
1546
|
+
relayAddressLabel: "Address",
|
|
1547
|
+
relayUserLabel: "User name",
|
|
1548
|
+
relayPasswordLabel: "Password",
|
|
1549
|
+
relayCopyAddress: "Copy address",
|
|
1550
|
+
relayCopyUser: "Copy user name",
|
|
1551
|
+
relayCopyPassword: "Copy password",
|
|
1552
|
+
relayCopied: "Copied",
|
|
1553
|
+
relayCopyFailed: "Could not copy — select it and copy by hand"
|
|
1058
1554
|
};
|
|
1059
1555
|
//#endregion
|
|
1060
1556
|
//#region src/client/index.ts
|
|
@@ -1090,12 +1586,14 @@ window.__ModuleLoader__.load({
|
|
|
1090
1586
|
zh,
|
|
1091
1587
|
en
|
|
1092
1588
|
}), "webdav-sync: dictionaries");
|
|
1093
|
-
const model = new WebdavSyncCard();
|
|
1589
|
+
const model = new WebdavSyncCard(directoryChooser(ctx));
|
|
1094
1590
|
const bound = model.actions();
|
|
1095
1591
|
const t = ctx.locale.bind(NS);
|
|
1096
1592
|
/**
|
|
1097
1593
|
* The registration-side face both seats inject. The store rides the reserved
|
|
1098
1594
|
* `hooks` compartment, so the renderer binds it as the `useWebdavSync` prop.
|
|
1595
|
+
* The forwarder is a switch inside this same store rather than a second one:
|
|
1596
|
+
* its state is part of the card's draft and goes out with the same Save.
|
|
1099
1597
|
*/
|
|
1100
1598
|
const face = () => ({
|
|
1101
1599
|
...bound,
|