dsh-webdav-sync 1.0.1 → 1.0.3
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 +120 -25
- package/lib/client.js +543 -18
- 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 +121 -0
- package/lib/relay.js +687 -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/*\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.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,10 +97,12 @@ 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",
|
|
@@ -77,9 +110,18 @@ window.__ModuleLoader__.load({
|
|
|
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,6 +462,7 @@ 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,
|
|
@@ -299,6 +550,23 @@ window.__ModuleLoader__.load({
|
|
|
299
550
|
onChange: (event) => props.edit("passphrase", event.target.value)
|
|
300
551
|
})
|
|
301
552
|
]
|
|
553
|
+
}),
|
|
554
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
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)
|
|
569
|
+
})]
|
|
302
570
|
})
|
|
303
571
|
] }),
|
|
304
572
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
@@ -321,7 +589,7 @@ window.__ModuleLoader__.load({
|
|
|
321
589
|
className: WebdavSyncCard_module_default.legend,
|
|
322
590
|
children: t("partsLegend")
|
|
323
591
|
}),
|
|
324
|
-
(state.view?.parts ?? []).map((part) => {
|
|
592
|
+
(state.view?.parts ?? []).filter(isToggledPart).map((part) => {
|
|
325
593
|
const stateKey = part.synced ? partStateKey(part) : "partOff";
|
|
326
594
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
327
595
|
className: WebdavSyncCard_module_default.partRow,
|
|
@@ -342,51 +610,122 @@ window.__ModuleLoader__.load({
|
|
|
342
610
|
})]
|
|
343
611
|
}, part.part);
|
|
344
612
|
}),
|
|
345
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
346
|
-
className: WebdavSyncCard_module_default.
|
|
347
|
-
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
|
+
})]
|
|
348
681
|
}),
|
|
349
682
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
350
683
|
className: WebdavSyncCard_module_default.footer,
|
|
351
684
|
children: [
|
|
352
685
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
353
|
-
variant: "
|
|
686
|
+
variant: "ghost",
|
|
354
687
|
size: "sm",
|
|
688
|
+
className: WebdavSyncCard_module_default.textAction,
|
|
355
689
|
disabled: busy,
|
|
356
690
|
onClick: () => props.check(),
|
|
357
691
|
children: state.busy === "check" ? t("working") : t("check")
|
|
358
692
|
}),
|
|
359
693
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
360
|
-
variant: "
|
|
694
|
+
variant: "ghost",
|
|
361
695
|
size: "sm",
|
|
696
|
+
className: WebdavSyncCard_module_default.textAction,
|
|
362
697
|
disabled: busy,
|
|
363
698
|
onClick: () => props.preview(),
|
|
364
699
|
children: state.busy === "preview" ? t("working") : t("preview")
|
|
365
700
|
}),
|
|
366
701
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
367
|
-
variant: "
|
|
702
|
+
variant: "ghost",
|
|
368
703
|
size: "sm",
|
|
704
|
+
className: `${WebdavSyncCard_module_default.textAction} ${WebdavSyncCard_module_default.textActionPrimary}`,
|
|
369
705
|
disabled: busy,
|
|
370
706
|
onClick: () => props.sync(),
|
|
371
707
|
children: state.busy === "sync" ? t("working") : t("syncNow")
|
|
372
708
|
}),
|
|
373
709
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
374
|
-
variant: "
|
|
710
|
+
variant: "ghost",
|
|
375
711
|
size: "sm",
|
|
712
|
+
className: `${WebdavSyncCard_module_default.textAction} ${WebdavSyncCard_module_default.textActionReplace}`,
|
|
376
713
|
disabled: busy,
|
|
377
714
|
onClick: () => props.askRestore(),
|
|
378
715
|
children: t("restore")
|
|
379
716
|
}),
|
|
380
717
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
381
|
-
variant: "
|
|
718
|
+
variant: "ghost",
|
|
382
719
|
size: "sm",
|
|
720
|
+
className: WebdavSyncCard_module_default.textAction,
|
|
383
721
|
disabled: busy || !(state.view?.undoable ?? false),
|
|
384
722
|
onClick: () => props.undo(),
|
|
385
723
|
children: t("undo")
|
|
386
724
|
}),
|
|
387
725
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
388
|
-
variant: "
|
|
726
|
+
variant: "ghost",
|
|
389
727
|
size: "sm",
|
|
728
|
+
className: `${WebdavSyncCard_module_default.textAction} ${WebdavSyncCard_module_default.textActionReplace}`,
|
|
390
729
|
disabled: busy,
|
|
391
730
|
onClick: () => props.askUpload(),
|
|
392
731
|
children: t("upload")
|
|
@@ -492,6 +831,16 @@ window.__ModuleLoader__.load({
|
|
|
492
831
|
t("working")
|
|
493
832
|
]
|
|
494
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
|
+
}),
|
|
495
844
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
496
845
|
className: WebdavSyncCard_module_default.cardFooter,
|
|
497
846
|
children: [
|
|
@@ -636,12 +985,17 @@ window.__ModuleLoader__.load({
|
|
|
636
985
|
user: "",
|
|
637
986
|
password: "",
|
|
638
987
|
passphrase: "",
|
|
988
|
+
headers: "",
|
|
989
|
+
dirs: [],
|
|
990
|
+
dirsEnabled: true,
|
|
639
991
|
rememberPassword: true,
|
|
640
992
|
autoEvery: -1,
|
|
641
993
|
models: true,
|
|
642
994
|
defaults: true,
|
|
643
995
|
plugins: true,
|
|
644
996
|
patch: true,
|
|
997
|
+
relayEnabled: false,
|
|
998
|
+
relayPort: RELAY_DEFAULT_PORT,
|
|
645
999
|
dirty: false,
|
|
646
1000
|
busy: "",
|
|
647
1001
|
message: "",
|
|
@@ -656,6 +1010,16 @@ window.__ModuleLoader__.load({
|
|
|
656
1010
|
state = EMPTY;
|
|
657
1011
|
listeners = /* @__PURE__ */ new Set();
|
|
658
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
|
+
}
|
|
659
1023
|
getSnapshot() {
|
|
660
1024
|
return this.state;
|
|
661
1025
|
}
|
|
@@ -689,12 +1053,17 @@ window.__ModuleLoader__.load({
|
|
|
689
1053
|
user: view.config.user,
|
|
690
1054
|
password: "",
|
|
691
1055
|
passphrase: "",
|
|
1056
|
+
headers: view.config.headers,
|
|
1057
|
+
dirs: [...view.config.dirs ?? []],
|
|
1058
|
+
dirsEnabled: view.config.dirsEnabled ?? true,
|
|
692
1059
|
rememberPassword: view.config.rememberPassword,
|
|
693
1060
|
autoEvery: view.config.autoEvery,
|
|
694
1061
|
models: view.config.models,
|
|
695
1062
|
defaults: view.config.defaults,
|
|
696
1063
|
plugins: view.config.plugins,
|
|
697
1064
|
patch: view.config.patch,
|
|
1065
|
+
relayEnabled: view.config.relayEnabled ?? false,
|
|
1066
|
+
relayPort: view.config.relayPort ?? 3426,
|
|
698
1067
|
dirty: false
|
|
699
1068
|
});
|
|
700
1069
|
}
|
|
@@ -743,6 +1112,31 @@ window.__ModuleLoader__.load({
|
|
|
743
1112
|
refresh() {
|
|
744
1113
|
this.load();
|
|
745
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
|
+
}
|
|
746
1140
|
edit(field, value) {
|
|
747
1141
|
this.set({
|
|
748
1142
|
[field]: value,
|
|
@@ -755,6 +1149,50 @@ window.__ModuleLoader__.load({
|
|
|
755
1149
|
dirty: true
|
|
756
1150
|
});
|
|
757
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
|
+
}
|
|
758
1196
|
setAuto(minutes) {
|
|
759
1197
|
if (!AUTO_CHOICES.includes(minutes)) return;
|
|
760
1198
|
this.set({
|
|
@@ -762,6 +1200,13 @@ window.__ModuleLoader__.load({
|
|
|
762
1200
|
dirty: true
|
|
763
1201
|
});
|
|
764
1202
|
}
|
|
1203
|
+
nextPort() {
|
|
1204
|
+
if (this.state.relayPort >= 65535) return;
|
|
1205
|
+
this.set({
|
|
1206
|
+
relayPort: this.state.relayPort + 1,
|
|
1207
|
+
dirty: true
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
765
1210
|
reset() {
|
|
766
1211
|
const view = this.state.view;
|
|
767
1212
|
if (view === void 0) return;
|
|
@@ -782,12 +1227,17 @@ window.__ModuleLoader__.load({
|
|
|
782
1227
|
user: this.state.user,
|
|
783
1228
|
password: this.state.password,
|
|
784
1229
|
passphrase: this.state.passphrase,
|
|
1230
|
+
headers: this.state.headers,
|
|
785
1231
|
autoEvery: this.state.autoEvery,
|
|
786
1232
|
models: this.state.models,
|
|
787
1233
|
defaults: this.state.defaults,
|
|
788
1234
|
plugins: this.state.plugins,
|
|
789
1235
|
patch: this.state.patch,
|
|
1236
|
+
dirs: this.state.dirs,
|
|
1237
|
+
dirsEnabled: this.state.dirsEnabled,
|
|
790
1238
|
rememberPassword: this.state.rememberPassword,
|
|
1239
|
+
relayEnabled: this.state.relayEnabled,
|
|
1240
|
+
relayPort: this.state.relayPort,
|
|
791
1241
|
...extra
|
|
792
1242
|
};
|
|
793
1243
|
}
|
|
@@ -866,9 +1316,14 @@ window.__ModuleLoader__.load({
|
|
|
866
1316
|
return {
|
|
867
1317
|
ensure: () => this.ensure(),
|
|
868
1318
|
refresh: () => this.refresh(),
|
|
1319
|
+
poll: () => this.poll(),
|
|
869
1320
|
edit: (field, value) => this.edit(field, value),
|
|
870
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),
|
|
871
1325
|
setAuto: (minutes) => this.setAuto(minutes),
|
|
1326
|
+
nextPort: () => this.nextPort(),
|
|
872
1327
|
reset: () => this.reset(),
|
|
873
1328
|
save: () => this.save(),
|
|
874
1329
|
check: () => this.check(),
|
|
@@ -885,6 +1340,24 @@ window.__ModuleLoader__.load({
|
|
|
885
1340
|
}
|
|
886
1341
|
};
|
|
887
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
|
|
888
1361
|
//#region src/client/locales.ts
|
|
889
1362
|
const NS = "webdav-sync";
|
|
890
1363
|
/** Chinese copy — the source dictionary. */
|
|
@@ -899,6 +1372,8 @@ window.__ModuleLoader__.load({
|
|
|
899
1372
|
passwordLabel: "密码",
|
|
900
1373
|
passphraseLabel: "口令",
|
|
901
1374
|
passphraseHint: "口令在文件离开本机前用来加密,服务器看不到内容;不要与 WebDAV 密码用同一个。",
|
|
1375
|
+
headersLabel: "额外请求头",
|
|
1376
|
+
headersPlaceholder: "User-Agent: Zotero/7.0\nAuthorization: Bearer xxxxx",
|
|
902
1377
|
rememberPassword: "保存密码",
|
|
903
1378
|
autoLegend: "自动同步",
|
|
904
1379
|
autoManual: "手动同步",
|
|
@@ -914,10 +1389,14 @@ window.__ModuleLoader__.load({
|
|
|
914
1389
|
partPluginsHint: "装了哪些插件、按什么顺序加载。插件包本身不传,每台机器自己安装。",
|
|
915
1390
|
partPatch: "其余 profile 配置",
|
|
916
1391
|
partPatchHint: "MCP 服务器、权限预设、界面设置、insert 块等其余配置。",
|
|
1392
|
+
partDirs: "附加目录",
|
|
1393
|
+
partDirsHint: "profile 之外的目录,连里面的文件一起同步;路径跟着这份配置走,恢复时写回同一个目录。",
|
|
917
1394
|
partSynced: "已一致",
|
|
918
1395
|
partOnlyHere: "仅本机有改动",
|
|
919
1396
|
partOnlyThere: "仅服务器有改动",
|
|
920
1397
|
partBoth: "两边都有改动",
|
|
1398
|
+
lastCheck: "上次检查:{at}",
|
|
1399
|
+
neverChecked: "尚未检查过",
|
|
921
1400
|
lastSync: "上次同步:{at}",
|
|
922
1401
|
neverSynced: "尚未同步过",
|
|
923
1402
|
noticeTitle: "上次同步替换了内容",
|
|
@@ -956,7 +1435,26 @@ window.__ModuleLoader__.load({
|
|
|
956
1435
|
takeNewerRemote: "两边都改过,保留服务器较新的一份",
|
|
957
1436
|
unavailable: "暂时不可用",
|
|
958
1437
|
partOff: "不同步",
|
|
959
|
-
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: "复制没成功,请手动选中复制"
|
|
960
1458
|
};
|
|
961
1459
|
/** English copy, complete by construction. */
|
|
962
1460
|
const en = {
|
|
@@ -970,6 +1468,8 @@ window.__ModuleLoader__.load({
|
|
|
970
1468
|
passwordLabel: "Password",
|
|
971
1469
|
passphraseLabel: "Passphrase",
|
|
972
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",
|
|
973
1473
|
rememberPassword: "Remember the password",
|
|
974
1474
|
autoLegend: "Sync by itself",
|
|
975
1475
|
autoManual: "By hand",
|
|
@@ -985,10 +1485,14 @@ window.__ModuleLoader__.load({
|
|
|
985
1485
|
partPluginsHint: "Which plugins are installed and in what order they load. The packages themselves stay behind: each computer installs them.",
|
|
986
1486
|
partPatch: "Other profile configuration",
|
|
987
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.",
|
|
988
1490
|
partSynced: "In step",
|
|
989
1491
|
partOnlyHere: "Changed here only",
|
|
990
1492
|
partOnlyThere: "Changed on the server only",
|
|
991
1493
|
partBoth: "Changed on both",
|
|
1494
|
+
lastCheck: "Last check: {at}",
|
|
1495
|
+
neverChecked: "Never checked yet",
|
|
992
1496
|
lastSync: "Last sync: {at}",
|
|
993
1497
|
neverSynced: "Never synced yet",
|
|
994
1498
|
noticeTitle: "The last sync replaced something",
|
|
@@ -1027,7 +1531,26 @@ window.__ModuleLoader__.load({
|
|
|
1027
1531
|
takeNewerRemote: "Both changed: keep the server’s as the newer one",
|
|
1028
1532
|
unavailable: "Not available",
|
|
1029
1533
|
partOff: "Not synced",
|
|
1030
|
-
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"
|
|
1031
1554
|
};
|
|
1032
1555
|
//#endregion
|
|
1033
1556
|
//#region src/client/index.ts
|
|
@@ -1063,12 +1586,14 @@ window.__ModuleLoader__.load({
|
|
|
1063
1586
|
zh,
|
|
1064
1587
|
en
|
|
1065
1588
|
}), "webdav-sync: dictionaries");
|
|
1066
|
-
const model = new WebdavSyncCard();
|
|
1589
|
+
const model = new WebdavSyncCard(directoryChooser(ctx));
|
|
1067
1590
|
const bound = model.actions();
|
|
1068
1591
|
const t = ctx.locale.bind(NS);
|
|
1069
1592
|
/**
|
|
1070
1593
|
* The registration-side face both seats inject. The store rides the reserved
|
|
1071
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.
|
|
1072
1597
|
*/
|
|
1073
1598
|
const face = () => ({
|
|
1074
1599
|
...bound,
|