dsh-webdav-sync 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -19
- package/lib/client.js +109 -31
- package/lib/dirs.d.ts +1 -1
- package/lib/dirs.js +1 -1
- package/lib/engine.d.ts +22 -5
- package/lib/engine.js +136 -34
- package/lib/index.js +30 -10
- package/lib/keys.d.ts +125 -0
- package/lib/keys.js +168 -0
- package/lib/state.js +19 -6
- package/lib/store.d.ts +18 -8
- package/lib/store.js +23 -33
- package/lib/wire.d.ts +90 -16
- package/lib/wire.js +12 -1
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -82,7 +82,7 @@ window.__ModuleLoader__.load({
|
|
|
82
82
|
}
|
|
83
83
|
//#endregion
|
|
84
84
|
//#region \0webdav-sync-css:/home/runner/work/dsh-webdav/dsh-webdav/src/client/WebdavSyncCard.module.css?inline
|
|
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";
|
|
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/*\n * The install report: how it went, the log, and the way out.\n *\n * The button sits on the sentence's row rather than under the log, so closing it\n * is where the eye already is — and one button takes the whole report away, since\n * the log is there to be read once and then is only something to scroll past.\n */\n.outputBlock_318697645 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.outputHead_318697645 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\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";
|
|
86
86
|
const tagId = "dsh-webdav-sync/WebdavSyncCard.module.css";
|
|
87
87
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
88
88
|
const tag = document.createElement("style");
|
|
@@ -131,6 +131,8 @@ window.__ModuleLoader__.load({
|
|
|
131
131
|
"previewRow": "previewRow_318697645",
|
|
132
132
|
"previewPart": "previewPart_318697645",
|
|
133
133
|
"output": "output_318697645",
|
|
134
|
+
"outputBlock": "outputBlock_318697645",
|
|
135
|
+
"outputHead": "outputHead_318697645",
|
|
134
136
|
"spin": "spin_318697645",
|
|
135
137
|
"relaySwitch": "relaySwitch_318697645",
|
|
136
138
|
"relayAddressRow": "relayAddressRow_318697645",
|
|
@@ -332,24 +334,24 @@ window.__ModuleLoader__.load({
|
|
|
332
334
|
/** Which dictionary key names each part. */
|
|
333
335
|
const PART_NAME = {
|
|
334
336
|
models: "partModels",
|
|
335
|
-
defaults: "partDefaults",
|
|
336
337
|
plugins: "partPlugins",
|
|
337
338
|
patch: "partPatch",
|
|
338
|
-
dirs: "partDirs"
|
|
339
|
+
dirs: "partDirs",
|
|
340
|
+
keys: "partKeys"
|
|
339
341
|
};
|
|
340
342
|
/** Which dictionary key says, in a line, what a part carries. */
|
|
341
343
|
const PART_HINT = {
|
|
342
344
|
models: "partModelsHint",
|
|
343
|
-
defaults: "partDefaultsHint",
|
|
344
345
|
plugins: "partPluginsHint",
|
|
345
346
|
patch: "partPatchHint",
|
|
346
|
-
dirs: "partDirsHint"
|
|
347
|
+
dirs: "partDirsHint",
|
|
348
|
+
keys: "partKeysHint"
|
|
347
349
|
};
|
|
348
350
|
/**
|
|
349
351
|
* The parts whose switch is a checkbox on their own row.
|
|
350
352
|
*
|
|
351
353
|
* The extra directories are a `part` too — they hash, merge and travel like the
|
|
352
|
-
*
|
|
354
|
+
* profile parts — but there is no single boolean behind them: the list
|
|
353
355
|
* itself is the part, so it is turned off by removing every row, and its rows
|
|
354
356
|
* are edited one by one instead of behind one label. Filtering it out keeps the
|
|
355
357
|
* checkbox loop honest, and the predicate is what tells the compiler so.
|
|
@@ -386,6 +388,20 @@ window.__ModuleLoader__.load({
|
|
|
386
388
|
return "partSynced";
|
|
387
389
|
}
|
|
388
390
|
/**
|
|
391
|
+
* The state word of one part's row, asked with the draft the row is bound to.
|
|
392
|
+
*
|
|
393
|
+
* The box and this word sit in the same row and have to answer for the same
|
|
394
|
+
* thing, so both read the draft. The host's `synced` says what the configuration
|
|
395
|
+
* on disk carries, and `localChanged` / `remoteChanged` describe that same saved
|
|
396
|
+
* configuration; while the draft disagrees with it, no word of the four is true —
|
|
397
|
+
* "in step" and "off" both speak about a save that has not happened yet. The row
|
|
398
|
+
* then says the one thing that is the case: the change is waiting for the save.
|
|
399
|
+
*/
|
|
400
|
+
function rowStateKey(draft, view) {
|
|
401
|
+
if (draft !== view.synced) return "partUnsaved";
|
|
402
|
+
return draft ? partStateKey(view) : "partOff";
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
389
405
|
* The last sync time as this computer's own clock reads it: the host stores an
|
|
390
406
|
* ISO string in UTC, which is not what a person recognises at a glance.
|
|
391
407
|
*/
|
|
@@ -590,13 +606,14 @@ window.__ModuleLoader__.load({
|
|
|
590
606
|
children: t("partsLegend")
|
|
591
607
|
}),
|
|
592
608
|
(state.view?.parts ?? []).filter(isToggledPart).map((part) => {
|
|
593
|
-
const
|
|
609
|
+
const on = state[part.part];
|
|
610
|
+
const stateKey = rowStateKey(on, part);
|
|
594
611
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
595
612
|
className: WebdavSyncCard_module_default.partRow,
|
|
596
613
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
597
614
|
className: WebdavSyncCard_module_default.partText,
|
|
598
615
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
599
|
-
checked:
|
|
616
|
+
checked: on,
|
|
600
617
|
label: t(PART_NAME[part.part]),
|
|
601
618
|
title: t("partOffHint"),
|
|
602
619
|
onChange: (next) => props.toggle(part.part, next)
|
|
@@ -636,7 +653,7 @@ window.__ModuleLoader__.load({
|
|
|
636
653
|
})]
|
|
637
654
|
}), dirsPart !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
638
655
|
className: WebdavSyncCard_module_default.partState,
|
|
639
|
-
children: t(
|
|
656
|
+
children: t(rowStateKey(state.dirsEnabled, dirsPart))
|
|
640
657
|
})]
|
|
641
658
|
}), state.dirs.map((path, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
642
659
|
className: state.dirsEnabled ? WebdavSyncCard_module_default.dirRow : `${WebdavSyncCard_module_default.dirRow} ${WebdavSyncCard_module_default.dirOff}`,
|
|
@@ -669,6 +686,22 @@ window.__ModuleLoader__.load({
|
|
|
669
686
|
message: problem.message
|
|
670
687
|
})
|
|
671
688
|
}, problem.path)),
|
|
689
|
+
(state.view?.keysProblems ?? []).map((problem) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
690
|
+
className: WebdavSyncCard_module_default.problem,
|
|
691
|
+
role: "status",
|
|
692
|
+
children: problem.ref === "" ? t("keyStoreProblem", { message: problem.message }) : t("keyProblem", {
|
|
693
|
+
ref: problem.ref,
|
|
694
|
+
message: problem.message
|
|
695
|
+
})
|
|
696
|
+
}, problem.ref)),
|
|
697
|
+
state.keysProblems.map((problem) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
698
|
+
className: WebdavSyncCard_module_default.problem,
|
|
699
|
+
role: "status",
|
|
700
|
+
children: problem.ref === "" ? t("keyStoreProblem", { message: problem.message }) : t("keyProblem", {
|
|
701
|
+
ref: problem.ref,
|
|
702
|
+
message: problem.message
|
|
703
|
+
})
|
|
704
|
+
}, `last-${problem.ref}`)),
|
|
672
705
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
673
706
|
className: WebdavSyncCard_module_default.times,
|
|
674
707
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -772,13 +805,24 @@ window.__ModuleLoader__.load({
|
|
|
772
805
|
})
|
|
773
806
|
]
|
|
774
807
|
}),
|
|
775
|
-
state.installOutput !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
776
|
-
className:
|
|
777
|
-
children:
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
808
|
+
state.installOutput !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
809
|
+
className: WebdavSyncCard_module_default.outputBlock,
|
|
810
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
811
|
+
className: WebdavSyncCard_module_default.outputHead,
|
|
812
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
813
|
+
className: state.installFailed ? WebdavSyncCard_module_default.failed : WebdavSyncCard_module_default.note,
|
|
814
|
+
children: state.installFailed ? t("installFailed") : t("installOk")
|
|
815
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
816
|
+
variant: "outline",
|
|
817
|
+
size: "sm",
|
|
818
|
+
onClick: () => props.closeOutput(),
|
|
819
|
+
children: t("outputClose")
|
|
820
|
+
})]
|
|
821
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
822
|
+
className: WebdavSyncCard_module_default.output,
|
|
823
|
+
children: state.installOutput
|
|
824
|
+
})]
|
|
825
|
+
}),
|
|
782
826
|
state.preview !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
783
827
|
className: WebdavSyncCard_module_default.legend,
|
|
784
828
|
children: t("previewTitle")
|
|
@@ -977,6 +1021,17 @@ window.__ModuleLoader__.load({
|
|
|
977
1021
|
}
|
|
978
1022
|
//#endregion
|
|
979
1023
|
//#region src/client/card-store.ts
|
|
1024
|
+
/**
|
|
1025
|
+
* The keys an action could not write, as the host reported them.
|
|
1026
|
+
*
|
|
1027
|
+
* Every action that applies a merge answers with the same field, and an absent one
|
|
1028
|
+
* means every key went in — so this reads both shapes as "no problems" rather than
|
|
1029
|
+
* making each caller remember which.
|
|
1030
|
+
*/
|
|
1031
|
+
function keyProblemsOf(result) {
|
|
1032
|
+
const held = result.keysProblems;
|
|
1033
|
+
return Array.isArray(held) ? held : [];
|
|
1034
|
+
}
|
|
980
1035
|
const EMPTY = {
|
|
981
1036
|
phase: "loading",
|
|
982
1037
|
error: "",
|
|
@@ -987,13 +1042,13 @@ window.__ModuleLoader__.load({
|
|
|
987
1042
|
passphrase: "",
|
|
988
1043
|
headers: "",
|
|
989
1044
|
dirs: [],
|
|
990
|
-
dirsEnabled:
|
|
1045
|
+
dirsEnabled: false,
|
|
991
1046
|
rememberPassword: true,
|
|
992
1047
|
autoEvery: -1,
|
|
993
1048
|
models: true,
|
|
994
|
-
defaults: true,
|
|
995
1049
|
plugins: true,
|
|
996
1050
|
patch: true,
|
|
1051
|
+
keys: true,
|
|
997
1052
|
relayEnabled: false,
|
|
998
1053
|
relayPort: RELAY_DEFAULT_PORT,
|
|
999
1054
|
dirty: false,
|
|
@@ -1002,6 +1057,8 @@ window.__ModuleLoader__.load({
|
|
|
1002
1057
|
messageFailed: false,
|
|
1003
1058
|
preview: void 0,
|
|
1004
1059
|
installOutput: "",
|
|
1060
|
+
installFailed: false,
|
|
1061
|
+
keysProblems: [],
|
|
1005
1062
|
reloadNeeded: false,
|
|
1006
1063
|
confirm: ""
|
|
1007
1064
|
};
|
|
@@ -1055,13 +1112,13 @@ window.__ModuleLoader__.load({
|
|
|
1055
1112
|
passphrase: "",
|
|
1056
1113
|
headers: view.config.headers,
|
|
1057
1114
|
dirs: [...view.config.dirs ?? []],
|
|
1058
|
-
dirsEnabled: view.config.dirsEnabled ??
|
|
1115
|
+
dirsEnabled: view.config.dirsEnabled ?? false,
|
|
1059
1116
|
rememberPassword: view.config.rememberPassword,
|
|
1060
1117
|
autoEvery: view.config.autoEvery,
|
|
1061
1118
|
models: view.config.models,
|
|
1062
|
-
defaults: view.config.defaults,
|
|
1063
1119
|
plugins: view.config.plugins,
|
|
1064
1120
|
patch: view.config.patch,
|
|
1121
|
+
keys: view.config.keys ?? true,
|
|
1065
1122
|
relayEnabled: view.config.relayEnabled ?? false,
|
|
1066
1123
|
relayPort: view.config.relayPort ?? 3426,
|
|
1067
1124
|
dirty: false
|
|
@@ -1215,6 +1272,7 @@ window.__ModuleLoader__.load({
|
|
|
1215
1272
|
message: "",
|
|
1216
1273
|
messageFailed: false,
|
|
1217
1274
|
installOutput: "",
|
|
1275
|
+
installFailed: false,
|
|
1218
1276
|
confirm: "",
|
|
1219
1277
|
reloadNeeded: false
|
|
1220
1278
|
});
|
|
@@ -1230,9 +1288,9 @@ window.__ModuleLoader__.load({
|
|
|
1230
1288
|
headers: this.state.headers,
|
|
1231
1289
|
autoEvery: this.state.autoEvery,
|
|
1232
1290
|
models: this.state.models,
|
|
1233
|
-
defaults: this.state.defaults,
|
|
1234
1291
|
plugins: this.state.plugins,
|
|
1235
1292
|
patch: this.state.patch,
|
|
1293
|
+
keys: this.state.keys,
|
|
1236
1294
|
dirs: this.state.dirs,
|
|
1237
1295
|
dirsEnabled: this.state.dirsEnabled,
|
|
1238
1296
|
rememberPassword: this.state.rememberPassword,
|
|
@@ -1263,7 +1321,8 @@ window.__ModuleLoader__.load({
|
|
|
1263
1321
|
sync() {
|
|
1264
1322
|
this.run("sync", () => requestSync({ force: true }), (result) => ({
|
|
1265
1323
|
preview: void 0,
|
|
1266
|
-
reloadNeeded: result.reloadNeeded === true
|
|
1324
|
+
reloadNeeded: result.reloadNeeded === true,
|
|
1325
|
+
keysProblems: keyProblemsOf(result)
|
|
1267
1326
|
})).then(() => this.load());
|
|
1268
1327
|
}
|
|
1269
1328
|
askRestore() {
|
|
@@ -1278,18 +1337,26 @@ window.__ModuleLoader__.load({
|
|
|
1278
1337
|
undo() {
|
|
1279
1338
|
this.run("undo", () => requestUndo(), (result) => ({
|
|
1280
1339
|
reloadNeeded: result.reloadNeeded === true,
|
|
1281
|
-
preview: void 0
|
|
1340
|
+
preview: void 0,
|
|
1341
|
+
keysProblems: keyProblemsOf(result)
|
|
1282
1342
|
})).then(() => this.load());
|
|
1283
1343
|
}
|
|
1284
1344
|
install() {
|
|
1285
1345
|
this.run("install", () => requestInstall(), (result) => ({
|
|
1286
1346
|
installOutput: String(result.output ?? ""),
|
|
1287
|
-
|
|
1347
|
+
installFailed: result.ok !== true,
|
|
1348
|
+
messageFailed: false
|
|
1288
1349
|
})).then(() => this.load());
|
|
1289
1350
|
}
|
|
1290
1351
|
dismiss() {
|
|
1291
1352
|
this.run("dismiss", () => requestDismiss(), () => ({})).then(() => this.load());
|
|
1292
1353
|
}
|
|
1354
|
+
closeOutput() {
|
|
1355
|
+
this.set({
|
|
1356
|
+
installOutput: "",
|
|
1357
|
+
installFailed: false
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1293
1360
|
answer(yes) {
|
|
1294
1361
|
const kind = this.state.confirm;
|
|
1295
1362
|
this.set({ confirm: "" });
|
|
@@ -1297,7 +1364,8 @@ window.__ModuleLoader__.load({
|
|
|
1297
1364
|
if (kind === "restore") {
|
|
1298
1365
|
this.run("restore", () => requestRestore(), (result) => ({
|
|
1299
1366
|
reloadNeeded: result.reloadNeeded === true,
|
|
1300
|
-
preview: void 0
|
|
1367
|
+
preview: void 0,
|
|
1368
|
+
keysProblems: keyProblemsOf(result)
|
|
1301
1369
|
})).then(() => this.load());
|
|
1302
1370
|
return;
|
|
1303
1371
|
}
|
|
@@ -1305,7 +1373,8 @@ window.__ModuleLoader__.load({
|
|
|
1305
1373
|
this.run("upload", () => requestUpload(), (result) => ({
|
|
1306
1374
|
preview: void 0,
|
|
1307
1375
|
messageFailed: false,
|
|
1308
|
-
reloadNeeded: result.reloadNeeded === true
|
|
1376
|
+
reloadNeeded: result.reloadNeeded === true,
|
|
1377
|
+
keysProblems: keyProblemsOf(result)
|
|
1309
1378
|
})).then(() => this.load());
|
|
1310
1379
|
return;
|
|
1311
1380
|
}
|
|
@@ -1335,7 +1404,8 @@ window.__ModuleLoader__.load({
|
|
|
1335
1404
|
askOff: () => this.askOff(),
|
|
1336
1405
|
answer: (yes) => this.answer(yes),
|
|
1337
1406
|
install: () => this.install(),
|
|
1338
|
-
dismiss: () => this.dismiss()
|
|
1407
|
+
dismiss: () => this.dismiss(),
|
|
1408
|
+
closeOutput: () => this.closeOutput()
|
|
1339
1409
|
};
|
|
1340
1410
|
}
|
|
1341
1411
|
};
|
|
@@ -1383,18 +1453,19 @@ window.__ModuleLoader__.load({
|
|
|
1383
1453
|
partsLegend: "同步内容",
|
|
1384
1454
|
partModels: "模型供应商与模型",
|
|
1385
1455
|
partModelsHint: "有哪些供应商、各自提供哪些模型、API key 从哪里读。",
|
|
1386
|
-
partDefaults: "默认模型",
|
|
1387
|
-
partDefaultsHint: "新开会话时默认用哪个供应商、哪个模型。",
|
|
1388
1456
|
partPlugins: "已安装插件",
|
|
1389
1457
|
partPluginsHint: "装了哪些插件、按什么顺序加载。插件包本身不传,每台机器自己安装。",
|
|
1390
1458
|
partPatch: "其余 profile 配置",
|
|
1391
1459
|
partPatchHint: "MCP 服务器、权限预设、界面设置、insert 块等其余配置。",
|
|
1460
|
+
partKeys: "API 密钥",
|
|
1461
|
+
partKeysHint: "这台电脑上存着的 API 密钥,跟着点它名的那些供应商一起走;两边都改过时以本机为准,任何一次同步都不会删掉密钥。离开本机前同样用口令加密。",
|
|
1392
1462
|
partDirs: "附加目录",
|
|
1393
1463
|
partDirsHint: "profile 之外的目录,连里面的文件一起同步;路径跟着这份配置走,恢复时写回同一个目录。",
|
|
1394
1464
|
partSynced: "已一致",
|
|
1395
1465
|
partOnlyHere: "仅本机有改动",
|
|
1396
1466
|
partOnlyThere: "仅服务器有改动",
|
|
1397
1467
|
partBoth: "两边都有改动",
|
|
1468
|
+
partUnsaved: "待保存",
|
|
1398
1469
|
lastCheck: "上次检查:{at}",
|
|
1399
1470
|
neverChecked: "尚未检查过",
|
|
1400
1471
|
lastSync: "上次同步:{at}",
|
|
@@ -1422,6 +1493,7 @@ window.__ModuleLoader__.load({
|
|
|
1422
1493
|
pendingInstallAction: "安装",
|
|
1423
1494
|
installOk: "安装完成。",
|
|
1424
1495
|
installFailed: "安装未成功,请看下面的输出。",
|
|
1496
|
+
outputClose: "关闭",
|
|
1425
1497
|
confirmRestore: "用服务器上的配置覆盖本机?本机现在的配置会先另存一份,之后可以撤销。",
|
|
1426
1498
|
confirmUpload: "用本机配置覆盖服务器上的文件?服务器上现有的文件会先另存一份。",
|
|
1427
1499
|
confirmOff: "关闭同步?服务器上的文件不会被删除。",
|
|
@@ -1440,6 +1512,8 @@ window.__ModuleLoader__.load({
|
|
|
1440
1512
|
dirPlaceholder: "这个目录的绝对路径",
|
|
1441
1513
|
dirRemove: "移除",
|
|
1442
1514
|
dirProblem: "「{path}」这个目录没读成:{message}",
|
|
1515
|
+
keyProblem: "「{ref}」这个 API 密钥没读成:{message}",
|
|
1516
|
+
keyStoreProblem: "本机存的 API 密钥没读成:{message}",
|
|
1443
1517
|
relayLegend: "本机转发",
|
|
1444
1518
|
relayEnable: "启用本机转发",
|
|
1445
1519
|
relayListening: "正在转发",
|
|
@@ -1479,18 +1553,19 @@ window.__ModuleLoader__.load({
|
|
|
1479
1553
|
partsLegend: "What is synced",
|
|
1480
1554
|
partModels: "Model providers and models",
|
|
1481
1555
|
partModelsHint: "Which providers there are, what models each offers, and where their API keys come from.",
|
|
1482
|
-
partDefaults: "Default model",
|
|
1483
|
-
partDefaultsHint: "Which provider and model a new session starts on.",
|
|
1484
1556
|
partPlugins: "Installed plugins",
|
|
1485
1557
|
partPluginsHint: "Which plugins are installed and in what order they load. The packages themselves stay behind: each computer installs them.",
|
|
1486
1558
|
partPatch: "Other profile configuration",
|
|
1487
1559
|
partPatchHint: "MCP servers, permission presets, interface settings, insert blocks, and the rest.",
|
|
1560
|
+
partKeys: "API keys",
|
|
1561
|
+
partKeysHint: "The API keys stored on this computer, travelling with the providers that name them; when both sides changed, this computer’s copy wins, and no sync ever deletes a key. They are sealed with the passphrase before the file leaves this computer.",
|
|
1488
1562
|
partDirs: "Extra directories",
|
|
1489
1563
|
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.",
|
|
1490
1564
|
partSynced: "In step",
|
|
1491
1565
|
partOnlyHere: "Changed here only",
|
|
1492
1566
|
partOnlyThere: "Changed on the server only",
|
|
1493
1567
|
partBoth: "Changed on both",
|
|
1568
|
+
partUnsaved: "Not saved yet",
|
|
1494
1569
|
lastCheck: "Last check: {at}",
|
|
1495
1570
|
neverChecked: "Never checked yet",
|
|
1496
1571
|
lastSync: "Last sync: {at}",
|
|
@@ -1518,6 +1593,7 @@ window.__ModuleLoader__.load({
|
|
|
1518
1593
|
pendingInstallAction: "Install",
|
|
1519
1594
|
installOk: "Installed.",
|
|
1520
1595
|
installFailed: "The install did not finish; the output is below.",
|
|
1596
|
+
outputClose: "Close",
|
|
1521
1597
|
confirmRestore: "Take the server’s setup over this computer’s? What is here now is kept aside first, so it can be undone.",
|
|
1522
1598
|
confirmUpload: "Write this computer’s setup over the file on the server? The file there is kept aside first.",
|
|
1523
1599
|
confirmOff: "Stop syncing? The file on the server is not deleted.",
|
|
@@ -1536,6 +1612,8 @@ window.__ModuleLoader__.load({
|
|
|
1536
1612
|
dirPlaceholder: "Absolute path of the directory",
|
|
1537
1613
|
dirRemove: "Remove",
|
|
1538
1614
|
dirProblem: "Could not read the directory {path}: {message}",
|
|
1615
|
+
keyProblem: "Could not read the API key {ref}: {message}",
|
|
1616
|
+
keyStoreProblem: "Could not read the stored API keys: {message}",
|
|
1539
1617
|
relayLegend: "Forwarding on this computer",
|
|
1540
1618
|
relayEnable: "Forward for other programs on this computer",
|
|
1541
1619
|
relayListening: "Forwarding",
|
package/lib/dirs.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The extra directories: reading them into the `dirs` part, and writing that
|
|
3
3
|
* part back out.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
5
|
+
* The profile parts are read from two files whose shape this plugin already
|
|
6
6
|
* knows. This one is different in three ways, and each of them shapes the code
|
|
7
7
|
* below:
|
|
8
8
|
*
|
package/lib/dirs.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The extra directories: reading them into the `dirs` part, and writing that
|
|
3
3
|
* part back out.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
5
|
+
* The profile parts are read from two files whose shape this plugin already
|
|
6
6
|
* knows. This one is different in three ways, and each of them shapes the code
|
|
7
7
|
* below:
|
|
8
8
|
*
|
package/lib/engine.d.ts
CHANGED
|
@@ -13,27 +13,44 @@
|
|
|
13
13
|
* with 409, a write that went out without the server checking its version, a body
|
|
14
14
|
* a relay cut in half.
|
|
15
15
|
*/
|
|
16
|
-
import { type Bundle, type CheckResult, type ConfigView, type Part, type PartHashes, type PreviewResult, type StatusResult, type SyncConfig, type SyncResult } from './wire.js';
|
|
16
|
+
import { type Bundle, type CheckResult, type ConfigView, type Part, type PartHashes, type KeyProblem, type PreviewResult, type StatusResult, type SyncConfig, type SyncResult } from './wire.js';
|
|
17
17
|
import { type DirsRead } from './dirs.js';
|
|
18
18
|
import { missingPlugins, type Collected, type ManifestFile, type PatchFile } from './store.js';
|
|
19
19
|
import { loadConfig, sameAccount, saveConfig, usable } from './state.js';
|
|
20
20
|
import type { ResolvedPaths } from './paths.js';
|
|
21
|
+
import { type KeysPort } from './keys.js';
|
|
21
22
|
/** One read of this computer's setup, with the files it came from. */
|
|
22
23
|
interface Snapshot {
|
|
23
24
|
patch: PatchFile;
|
|
24
25
|
manifest: ManifestFile;
|
|
25
|
-
/** The
|
|
26
|
+
/** The parts that come out of the profile's own two files, plus the keys. */
|
|
26
27
|
parts: Collected;
|
|
27
28
|
/** The extra directories, walked from disk, with whatever could not be read. */
|
|
28
29
|
dirs: DirsRead;
|
|
30
|
+
/**
|
|
31
|
+
* Credential references this read could not resolve, with the reason.
|
|
32
|
+
*
|
|
33
|
+
* Kept on the snapshot rather than inside `parts` for the same reason the
|
|
34
|
+
* directories keep theirs beside the part: a store that cannot be read must not
|
|
35
|
+
* travel as "this computer has no keys". See {@link readKeys}.
|
|
36
|
+
*/
|
|
37
|
+
keysProblems: KeyProblem[];
|
|
29
38
|
}
|
|
30
39
|
/** Everything a sync needs, resolved once. */
|
|
31
40
|
export interface Engine {
|
|
32
41
|
paths: ResolvedPaths;
|
|
33
42
|
config: SyncConfig;
|
|
43
|
+
/**
|
|
44
|
+
* The host's credential store, absent on a host without one.
|
|
45
|
+
*
|
|
46
|
+
* Carried on the engine for the same reason the paths are: it is the one thing
|
|
47
|
+
* a call needs that no configuration can say. Absent is a working state — the
|
|
48
|
+
* `keys` part then has nothing to read and nothing to write, and says so.
|
|
49
|
+
*/
|
|
50
|
+
credentials?: KeysPort | undefined;
|
|
34
51
|
}
|
|
35
|
-
/** The engine for a profile directory and
|
|
36
|
-
export declare function engineFor(paths: ResolvedPaths, config: SyncConfig): Engine;
|
|
52
|
+
/** The engine for a profile directory, a configuration, and the host's credential store. */
|
|
53
|
+
export declare function engineFor(paths: ResolvedPaths, config: SyncConfig, credentials?: KeysPort | undefined): Engine;
|
|
37
54
|
/**
|
|
38
55
|
* Run one merge.
|
|
39
56
|
*
|
|
@@ -89,7 +106,7 @@ export declare function check(engine: Engine): Promise<CheckResult>;
|
|
|
89
106
|
/** What one sync would do, decided the same way but writing nothing anywhere. */
|
|
90
107
|
export declare function preview(engine: Engine): Promise<PreviewResult>;
|
|
91
108
|
/** Read this computer's setup and the files it came from, in one pass. */
|
|
92
|
-
export declare function take(paths: ResolvedPaths, config: SyncConfig): Promise<Snapshot>;
|
|
109
|
+
export declare function take(paths: ResolvedPaths, config: SyncConfig, credentials?: KeysPort | undefined): Promise<Snapshot>;
|
|
93
110
|
/** Per-part hashes of a setup; a part that is not there has none. */
|
|
94
111
|
export declare function hashesOf(parts: Partial<Record<Part, unknown>> | Bundle): PartHashes;
|
|
95
112
|
/** Whether two sets of part hashes describe the same setup. */
|