dsh-tiddlywiki 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -25
- package/lib/client.bundle.js +903 -386
- package/lib/client.js +903 -386
- package/lib/index.js +796 -32
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/index.ts +10 -4
- package/src/client/knowledge-fab.ts +232 -0
- package/src/client/markdown-editor.ts +5 -0
- package/src/client/note-widget.ts +725 -475
- package/src/client/panel.ts +27 -62
- package/src/client/settings-page.ts +3 -3
- package/src/client/styles.ts +110 -0
- package/src/client/sync-button.ts +191 -188
- package/src/host/git.ts +21 -0
- package/src/host/routes.ts +145 -8
- package/src/host/seed-notes.ts +7 -5
- package/src/host/tools.ts +589 -281
- package/src/host/tw-api.ts +80 -6
- package/src/host/wiki.ts +12 -0
- package/src/index.ts +46 -6
- package/src/sdk.ts +8 -2
package/lib/client.js
CHANGED
|
@@ -134,6 +134,9 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
134
134
|
display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
|
|
135
135
|
font-family: inherit;
|
|
136
136
|
}
|
|
137
|
+
/* The wrapper's display:flex would beat the UA [hidden]{display:none} rule,
|
|
138
|
+
so the card's show/hide (root.hidden) needs an explicit rule. */
|
|
139
|
+
.dsh-tw-note[hidden] { display: none; }
|
|
137
140
|
.dsh-tw-note-card {
|
|
138
141
|
width: 340px; max-width: calc(100vw - 40px);
|
|
139
142
|
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
@@ -374,6 +377,113 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
374
377
|
.dsh-tw-settings-error { color: var(--dsw-alias-state-error-primary, #d13b3b); font-size: 12px; }
|
|
375
378
|
.dsh-tw-settings-search { flex: 0 0 auto; max-width: 220px; }
|
|
376
379
|
.dsh-tw-settings-check { accent-color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
380
|
+
|
|
381
|
+
/* ── "知识库" FAB (v0.5: quick-note + sync + panel status merged) ──────────
|
|
382
|
+
One fixed cluster above the shutdown launcher FAB (bottom:24). The FAB
|
|
383
|
+
carries a git status dot; the menu pops upward from it. */
|
|
384
|
+
.dsh-tw-fab-wrap {
|
|
385
|
+
position: fixed; right: 24px; bottom: 88px; z-index: 960;
|
|
386
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
|
387
|
+
}
|
|
388
|
+
.dsh-tw-fab {
|
|
389
|
+
position: relative;
|
|
390
|
+
width: 46px; height: 46px; border-radius: 50%;
|
|
391
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
392
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
393
|
+
box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
|
|
394
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
395
|
+
transition: background-color 120ms ease, transform 80ms ease;
|
|
396
|
+
}
|
|
397
|
+
.dsh-tw-fab:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
398
|
+
.dsh-tw-fab:active { transform: scale(.94); }
|
|
399
|
+
.dsh-tw-fab-icon { display: inline-flex; }
|
|
400
|
+
.dsh-tw-fab-dot {
|
|
401
|
+
position: absolute; right: 0; bottom: 0; width: 11px; height: 11px;
|
|
402
|
+
border-radius: 50%; border: 2px solid var(--dsw-alias-bg-layer-2, #fff);
|
|
403
|
+
background: #999; box-sizing: border-box;
|
|
404
|
+
}
|
|
405
|
+
.dsh-tw-fab-dot[data-state="clean"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
406
|
+
.dsh-tw-fab-dot[data-state="dirty"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
407
|
+
.dsh-tw-fab-dot[data-state="behind"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
408
|
+
.dsh-tw-fab-dot[data-state="syncing"] { background: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
409
|
+
.dsh-tw-fab-menu {
|
|
410
|
+
min-width: 220px; max-width: calc(100vw - 48px);
|
|
411
|
+
display: flex; flex-direction: column; gap: 2px; padding: 6px;
|
|
412
|
+
border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
|
|
413
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
414
|
+
box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
|
|
415
|
+
font-size: 13px;
|
|
416
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
417
|
+
animation: dsh-tw-note-in 160ms ease;
|
|
418
|
+
}
|
|
419
|
+
.dsh-tw-fab-menu[hidden] { display: none; }
|
|
420
|
+
.dsh-tw-fab-status {
|
|
421
|
+
display: flex; align-items: center; gap: 7px;
|
|
422
|
+
padding: 6px 9px; font-size: 12px; color: var(--dsw-alias-label-secondary, #666);
|
|
423
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.08));
|
|
424
|
+
margin-bottom: 4px;
|
|
425
|
+
}
|
|
426
|
+
.dsh-tw-fab-status + .dsh-tw-fab-status { border-bottom: 0; margin-bottom: 4px; }
|
|
427
|
+
.dsh-tw-fab-status-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
428
|
+
.dsh-tw-fab-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
429
|
+
.dsh-tw-fab-status-dot[data-state="running"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
430
|
+
.dsh-tw-fab-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
431
|
+
.dsh-tw-fab-status-dot[data-state="failed"], .dsh-tw-fab-status-dot[data-state="stopped"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
432
|
+
.dsh-tw-fab-status-dot[data-state="clean"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
433
|
+
.dsh-tw-fab-status-dot[data-state="dirty"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
434
|
+
.dsh-tw-fab-status-dot[data-state="behind"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
435
|
+
.dsh-tw-fab-status-dot[data-state="syncing"] { background: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
436
|
+
.dsh-tw-fab-item {
|
|
437
|
+
display: flex; align-items: center; gap: 6px; text-align: left;
|
|
438
|
+
border: none; background: transparent; color: inherit; font: inherit; font-size: 13px;
|
|
439
|
+
padding: 7px 9px; border-radius: 8px; cursor: pointer;
|
|
440
|
+
}
|
|
441
|
+
.dsh-tw-fab-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
442
|
+
.dsh-tw-fab-item:active { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); }
|
|
443
|
+
|
|
444
|
+
/* ── quick-note restored-draft banner ─────────────────────────────────────── */
|
|
445
|
+
.dsh-tw-note-draft {
|
|
446
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
447
|
+
padding: 5px 9px; border-radius: 8px; font-size: 12px;
|
|
448
|
+
background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, #d9822b) 12%, transparent);
|
|
449
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
450
|
+
}
|
|
451
|
+
.dsh-tw-note-draft[hidden] { display: none; }
|
|
452
|
+
.dsh-tw-note-draft-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
453
|
+
.dsh-tw-note-draft-discard {
|
|
454
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
455
|
+
background: transparent; color: inherit; font: inherit; font-size: 12px;
|
|
456
|
+
padding: 2px 10px; border-radius: 999px; cursor: pointer; flex: none;
|
|
457
|
+
}
|
|
458
|
+
.dsh-tw-note-draft-discard:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
459
|
+
|
|
460
|
+
/* ── quick-note recent-notes picker ───────────────────────────────────────── */
|
|
461
|
+
.dsh-tw-note-recent {
|
|
462
|
+
position: absolute; right: 0; bottom: calc(100% - 8px); z-index: 70;
|
|
463
|
+
width: 340px; max-width: calc(100vw - 40px); max-height: 280px; overflow-y: auto;
|
|
464
|
+
display: flex; flex-direction: column; gap: 2px; padding: 6px;
|
|
465
|
+
border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
|
|
466
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
467
|
+
box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
|
|
468
|
+
font-size: 13px;
|
|
469
|
+
}
|
|
470
|
+
.dsh-tw-note-recent[hidden] { display: none; }
|
|
471
|
+
.dsh-tw-note-recent-item {
|
|
472
|
+
display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
|
|
473
|
+
padding: 6px 8px; border-radius: 7px; cursor: pointer;
|
|
474
|
+
}
|
|
475
|
+
.dsh-tw-note-recent-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
476
|
+
.dsh-tw-note-recent-name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
477
|
+
.dsh-tw-note-recent-meta { flex: none; font-size: 11px; color: var(--dsw-alias-label-dimmed, #999); }
|
|
478
|
+
.dsh-tw-note-recent-muted { padding: 8px 10px; font-size: 12px; color: var(--dsw-alias-label-dimmed, #999); }
|
|
479
|
+
.dsh-tw-note-recent-btn {
|
|
480
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
481
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
482
|
+
font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
483
|
+
transition: filter 120ms ease, transform 80ms ease;
|
|
484
|
+
}
|
|
485
|
+
.dsh-tw-note-recent-btn:hover { filter: brightness(.96); }
|
|
486
|
+
.dsh-tw-note-recent-btn:active { transform: scale(.96); }
|
|
377
487
|
`;
|
|
378
488
|
function injectStyles() {
|
|
379
489
|
if (typeof document === "undefined") return;
|
|
@@ -570,7 +680,7 @@ const PANEL_HOST_SELECTOR = "[data-dsh-panel-host]";
|
|
|
570
680
|
const APP_OVERLAY_Z_INDEX = 20;
|
|
571
681
|
/** Safety re-measure cadence for shell layout changes CSS can't see. */
|
|
572
682
|
const SYNC_INTERVAL_MS = 2e3;
|
|
573
|
-
const STATUS_ENDPOINT$
|
|
683
|
+
const STATUS_ENDPOINT$3 = "/dsh-tiddlywiki/status";
|
|
574
684
|
const RESTART_ENDPOINT$1 = "/dsh-tiddlywiki/restart";
|
|
575
685
|
/**
|
|
576
686
|
* The panel's z-index: below any dsh-better-sidebar host layer so its
|
|
@@ -595,7 +705,7 @@ function conversationColumn() {
|
|
|
595
705
|
}
|
|
596
706
|
async function fetchStatus$1() {
|
|
597
707
|
try {
|
|
598
|
-
const res = await fetch(STATUS_ENDPOINT$
|
|
708
|
+
const res = await fetch(STATUS_ENDPOINT$3, { signal: AbortSignal.timeout(8e3) });
|
|
599
709
|
if (!res.ok) return null;
|
|
600
710
|
return await res.json();
|
|
601
711
|
} catch {
|
|
@@ -618,9 +728,6 @@ function mountPanel(state) {
|
|
|
618
728
|
let iframe;
|
|
619
729
|
let frameArea;
|
|
620
730
|
let errorArea;
|
|
621
|
-
let chip;
|
|
622
|
-
let statusDot;
|
|
623
|
-
let statusFloater;
|
|
624
731
|
let refreshTimer;
|
|
625
732
|
let refreshAttempts = 0;
|
|
626
733
|
const build = () => {
|
|
@@ -641,45 +748,6 @@ function mountPanel(state) {
|
|
|
641
748
|
view.append(frameArea, errorArea);
|
|
642
749
|
return view;
|
|
643
750
|
};
|
|
644
|
-
/**
|
|
645
|
-
* Bottom-right floating status/reload buttons (same visual language as the
|
|
646
|
-
* quick-note toggle) — the top panel bar is gone, so TW's menubar reaches
|
|
647
|
-
* the very top of the panel and clears the host's top-right buttons. Mount
|
|
648
|
-
* is gated by `ui.showPanelStatus` (settings page toggle).
|
|
649
|
-
*/
|
|
650
|
-
const buildStatusFloater = () => {
|
|
651
|
-
const wrap = document.createElement("div");
|
|
652
|
-
wrap.className = "dsh-tw-panel-status";
|
|
653
|
-
const statusBtn = document.createElement("button");
|
|
654
|
-
statusBtn.type = "button";
|
|
655
|
-
statusBtn.className = "dsh-tw-panel-status-btn";
|
|
656
|
-
statusBtn.title = "刷新状态";
|
|
657
|
-
statusDot = document.createElement("span");
|
|
658
|
-
statusDot.className = "dsh-tw-status-dot";
|
|
659
|
-
statusDot.dataset.state = "unknown";
|
|
660
|
-
chip = document.createElement("span");
|
|
661
|
-
chip.className = "dsh-tw-panel-status-text";
|
|
662
|
-
chip.textContent = "—";
|
|
663
|
-
statusBtn.append(statusDot, chip);
|
|
664
|
-
statusBtn.addEventListener("click", () => {
|
|
665
|
-
doRefresh();
|
|
666
|
-
});
|
|
667
|
-
const reloadBtn = document.createElement("button");
|
|
668
|
-
reloadBtn.type = "button";
|
|
669
|
-
reloadBtn.className = "dsh-tw-panel-status-btn";
|
|
670
|
-
reloadBtn.textContent = "重载";
|
|
671
|
-
reloadBtn.title = "重载 TiddlyWiki 面板";
|
|
672
|
-
reloadBtn.addEventListener("click", () => {
|
|
673
|
-
if (iframe !== void 0 && !iframe.hidden) iframe.src = iframe.src;
|
|
674
|
-
});
|
|
675
|
-
wrap.append(statusBtn, reloadBtn);
|
|
676
|
-
document.body.append(wrap);
|
|
677
|
-
return wrap;
|
|
678
|
-
};
|
|
679
|
-
const setChip = (stateName, text) => {
|
|
680
|
-
if (statusDot !== void 0) statusDot.dataset.state = stateName;
|
|
681
|
-
if (chip !== void 0) chip.textContent = text;
|
|
682
|
-
};
|
|
683
751
|
/** Pin the overlay to the center column's current viewport rect. */
|
|
684
752
|
const syncRect = () => {
|
|
685
753
|
if (container === void 0 || columnEl === void 0) return;
|
|
@@ -762,18 +830,17 @@ function mountPanel(state) {
|
|
|
762
830
|
}
|
|
763
831
|
const payload = await fetchStatus$1();
|
|
764
832
|
if (payload === null) {
|
|
765
|
-
setChip("failed", "状态不可达");
|
|
766
833
|
showError("无法访问 /dsh-tiddlywiki/status");
|
|
767
834
|
return;
|
|
768
835
|
}
|
|
769
|
-
if (payload.status === "running"
|
|
770
|
-
setChip("running", "在线");
|
|
836
|
+
if (payload.status === "running") {
|
|
771
837
|
refreshAttempts = 0;
|
|
772
|
-
showFrame(payload.
|
|
838
|
+
if (typeof payload.twProxy === "string") showFrame(new URL(payload.twProxy, window.location.origin).href);
|
|
839
|
+
else if (typeof payload.url === "string") showFrame(payload.url);
|
|
840
|
+
else showError("服务未返回编辑器地址");
|
|
773
841
|
return;
|
|
774
842
|
}
|
|
775
843
|
if (payload.status === "starting") {
|
|
776
|
-
setChip("starting", "启动中");
|
|
777
844
|
showStarting();
|
|
778
845
|
if (refreshAttempts < 30) {
|
|
779
846
|
refreshAttempts++;
|
|
@@ -783,7 +850,6 @@ function mountPanel(state) {
|
|
|
783
850
|
}
|
|
784
851
|
return;
|
|
785
852
|
}
|
|
786
|
-
setChip("failed", "离线");
|
|
787
853
|
refreshAttempts = 0;
|
|
788
854
|
showError(payload.error ?? `服务状态:${payload.status}`);
|
|
789
855
|
};
|
|
@@ -832,10 +898,10 @@ function mountPanel(state) {
|
|
|
832
898
|
const unsubscribe = state.subscribe(applyActive);
|
|
833
899
|
ensure();
|
|
834
900
|
applyActive();
|
|
835
|
-
|
|
836
|
-
if (
|
|
837
|
-
|
|
838
|
-
|
|
901
|
+
const onReloadRequest = () => {
|
|
902
|
+
if (iframe !== void 0 && !iframe.hidden) iframe.src = iframe.src;
|
|
903
|
+
};
|
|
904
|
+
document.addEventListener("dsh-tw-panel-reload", onReloadRequest);
|
|
839
905
|
return () => {
|
|
840
906
|
if (refreshTimer !== void 0) window.clearTimeout(refreshTimer);
|
|
841
907
|
window.clearInterval(syncInterval);
|
|
@@ -844,11 +910,11 @@ function mountPanel(state) {
|
|
|
844
910
|
resizeObserver.disconnect();
|
|
845
911
|
document.removeEventListener("click", onClickSidebarRow, true);
|
|
846
912
|
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
|
|
913
|
+
document.removeEventListener("dsh-tw-panel-reload", onReloadRequest);
|
|
847
914
|
waitObserver.disconnect();
|
|
848
915
|
unsubscribe();
|
|
849
916
|
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
850
917
|
container?.remove();
|
|
851
|
-
statusFloater?.remove();
|
|
852
918
|
};
|
|
853
919
|
}
|
|
854
920
|
//#endregion
|
|
@@ -26408,7 +26474,10 @@ function buildMarkdownEditor(opts = {}) {
|
|
|
26408
26474
|
return true;
|
|
26409
26475
|
}
|
|
26410
26476
|
}
|
|
26411
|
-
])
|
|
26477
|
+
]),
|
|
26478
|
+
EditorView.updateListener.of((update) => {
|
|
26479
|
+
if (update.docChanged) opts.onChange?.();
|
|
26480
|
+
})
|
|
26412
26481
|
]
|
|
26413
26482
|
})
|
|
26414
26483
|
});
|
|
@@ -26442,39 +26511,66 @@ function buildMarkdownEditor(opts = {}) {
|
|
|
26442
26511
|
//#endregion
|
|
26443
26512
|
//#region src/client/note-widget.ts
|
|
26444
26513
|
/**
|
|
26445
|
-
* Floating quick-note
|
|
26446
|
-
* collapsible
|
|
26447
|
-
*
|
|
26514
|
+
* Floating quick-note card (design doc §12, D6/D7) — a bottom-right, fixed,
|
|
26515
|
+
* collapsible card for jotting drafts / scratch notes while waiting for the AI
|
|
26516
|
+
* or drafting the next prompt. In v0.5 the trigger moved into the "知识库" FAB
|
|
26517
|
+
* (knowledge-fab.ts); this module ONLY owns the card itself via
|
|
26518
|
+
* `createNoteWidget()` (lazy build — no DOM until first open).
|
|
26519
|
+
*
|
|
26520
|
+
* Features:
|
|
26521
|
+
* - CodeMirror 6 Markdown editor (see markdown-editor.ts), file upload + drag
|
|
26522
|
+
* & drop, multi-tag chips with autocomplete (GET /dsh-tiddlywiki/tags).
|
|
26523
|
+
* - DRAFT PERSISTENCE: editor/title/tags auto-save to localStorage on a 500ms
|
|
26524
|
+
* debounce; reopening restores the draft with a "丢弃" banner. A successful
|
|
26525
|
+
* save / "在 TW 中编辑" clears it.
|
|
26526
|
+
* - RECENT PICKER: a 「最近」 button lists the newest notes (GET
|
|
26527
|
+
* /dsh-tiddlywiki/recent); clicking one loads it into the editor (GET
|
|
26528
|
+
* /dsh-tiddlywiki/get) to continue editing.
|
|
26529
|
+
* - Ctrl+Enter saves; 「✏️ 在 TW 中编辑」 saves and opens TW's native editor.
|
|
26448
26530
|
*
|
|
26449
26531
|
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
|
|
26450
26532
|
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
26451
26533
|
*
|
|
26452
|
-
* ui.showQuickNote config: the widget is mounted async and stays hidden (and
|
|
26453
|
-
* never appended) when the option is off, so the toggle button can be disabled
|
|
26454
|
-
* from the settings page without touching the shell.
|
|
26455
|
-
*
|
|
26456
|
-
* Tag editor: multi-select chips + autocomplete from the wiki's existing tags
|
|
26457
|
-
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
26458
|
-
* an empty field removes the last chip; the dropdown filters on typing.
|
|
26459
|
-
*
|
|
26460
|
-
* Markdown editing: a CodeMirror 6 editor (see markdown-editor.ts) with real
|
|
26461
|
-
* Lezer-based syntax highlighting (GFM: headings, lists, code, links, tables,
|
|
26462
|
-
* task lists, strikethrough…) plus undo/redo, wired into the same
|
|
26463
|
-
* `.dsh-tw-note-editor` surface the widget reads/writes.
|
|
26464
|
-
*
|
|
26465
|
-
* File upload: the body accepts files via the 📎 button or drag-and-drop;
|
|
26466
|
-
* each file is POSTed raw to /dsh-tiddlywiki/upload (saved under the wiki's
|
|
26467
|
-
* `files/` folder, git-tracked and served at `/files/<name>`) and a Markdown
|
|
26468
|
-
* image/link line is inserted at the caret.
|
|
26469
|
-
*
|
|
26470
26534
|
* @module dsh-tiddlywiki/client/note-widget
|
|
26471
26535
|
*/
|
|
26472
26536
|
const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
|
|
26473
26537
|
const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
|
|
26474
|
-
const STATUS_ENDPOINT$
|
|
26538
|
+
const STATUS_ENDPOINT$2 = "/dsh-tiddlywiki/status";
|
|
26475
26539
|
const TAGS_ENDPOINT = "/dsh-tiddlywiki/tags";
|
|
26540
|
+
const RECENT_ENDPOINT = "/dsh-tiddlywiki/recent";
|
|
26541
|
+
const GET_ENDPOINT = "/dsh-tiddlywiki/get";
|
|
26476
26542
|
const UPLOAD_ENDPOINT = "/dsh-tiddlywiki/upload";
|
|
26477
26543
|
const MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
|
|
26544
|
+
/** localStorage key for the autosaved quick-note draft. */
|
|
26545
|
+
const DRAFT_KEY = "dsh-tw-note-draft-v1";
|
|
26546
|
+
/** Draft auto-save debounce. */
|
|
26547
|
+
const DRAFT_DEBOUNCE_MS = 500;
|
|
26548
|
+
function loadDraft() {
|
|
26549
|
+
try {
|
|
26550
|
+
const raw = localStorage.getItem(DRAFT_KEY);
|
|
26551
|
+
if (raw === null) return null;
|
|
26552
|
+
const parsed = JSON.parse(raw);
|
|
26553
|
+
if (typeof parsed.text !== "string" || typeof parsed.title !== "string") return null;
|
|
26554
|
+
return {
|
|
26555
|
+
text: parsed.text,
|
|
26556
|
+
title: parsed.title,
|
|
26557
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : [],
|
|
26558
|
+
savedAt: typeof parsed.savedAt === "number" ? parsed.savedAt : 0
|
|
26559
|
+
};
|
|
26560
|
+
} catch {
|
|
26561
|
+
return null;
|
|
26562
|
+
}
|
|
26563
|
+
}
|
|
26564
|
+
function persistDraft(draft) {
|
|
26565
|
+
try {
|
|
26566
|
+
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
|
|
26567
|
+
} catch {}
|
|
26568
|
+
}
|
|
26569
|
+
function clearDraft() {
|
|
26570
|
+
try {
|
|
26571
|
+
localStorage.removeItem(DRAFT_KEY);
|
|
26572
|
+
} catch {}
|
|
26573
|
+
}
|
|
26478
26574
|
function pad$1(n) {
|
|
26479
26575
|
return n < 10 ? `0${n}` : String(n);
|
|
26480
26576
|
}
|
|
@@ -26482,13 +26578,28 @@ function pad$1(n) {
|
|
|
26482
26578
|
function timestampTitle(date = /* @__PURE__ */ new Date()) {
|
|
26483
26579
|
return `${date.getFullYear()}-${pad$1(date.getMonth() + 1)}-${pad$1(date.getDate())} ${pad$1(date.getHours())}:${pad$1(date.getMinutes())}`;
|
|
26484
26580
|
}
|
|
26581
|
+
/** Compact Chinese relative time for the recent picker (e.g. "3小时前"). */
|
|
26582
|
+
function relativeTime(iso) {
|
|
26583
|
+
if (iso === null) return "";
|
|
26584
|
+
const ms = new Date(iso).getTime();
|
|
26585
|
+
if (Number.isNaN(ms)) return "";
|
|
26586
|
+
const diff = Date.now() - ms;
|
|
26587
|
+
const min = Math.floor(diff / 6e4);
|
|
26588
|
+
if (min < 1) return "刚刚";
|
|
26589
|
+
if (min < 60) return `${min}分钟前`;
|
|
26590
|
+
const hr = Math.floor(min / 60);
|
|
26591
|
+
if (hr < 24) return `${hr}小时前`;
|
|
26592
|
+
const day = Math.floor(hr / 24);
|
|
26593
|
+
if (day < 30) return `${day}天前`;
|
|
26594
|
+
return new Date(ms).toLocaleDateString("zh-CN");
|
|
26595
|
+
}
|
|
26485
26596
|
async function fetchUiOptions() {
|
|
26486
26597
|
const fallback = {
|
|
26487
26598
|
showQuickNote: true,
|
|
26488
26599
|
defaultTag: "inbox"
|
|
26489
26600
|
};
|
|
26490
26601
|
try {
|
|
26491
|
-
const res = await fetch(STATUS_ENDPOINT$
|
|
26602
|
+
const res = await fetch(STATUS_ENDPOINT$2, { signal: AbortSignal.timeout(5e3) });
|
|
26492
26603
|
if (!res.ok) return fallback;
|
|
26493
26604
|
const payload = await res.json();
|
|
26494
26605
|
return {
|
|
@@ -26500,7 +26611,7 @@ async function fetchUiOptions() {
|
|
|
26500
26611
|
}
|
|
26501
26612
|
}
|
|
26502
26613
|
/** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
|
|
26503
|
-
function buildTagEditor() {
|
|
26614
|
+
function buildTagEditor(opts = {}) {
|
|
26504
26615
|
const wrap = document.createElement("div");
|
|
26505
26616
|
wrap.className = "dsh-tw-note-tags";
|
|
26506
26617
|
const chipWrap = document.createElement("div");
|
|
@@ -26516,6 +26627,9 @@ function buildTagEditor() {
|
|
|
26516
26627
|
const hideSuggest = () => {
|
|
26517
26628
|
suggest.hidden = true;
|
|
26518
26629
|
};
|
|
26630
|
+
const emit = () => {
|
|
26631
|
+
opts.onChange?.();
|
|
26632
|
+
};
|
|
26519
26633
|
const renderChips = () => {
|
|
26520
26634
|
chipWrap.replaceChildren();
|
|
26521
26635
|
for (const tag of chips) {
|
|
@@ -26532,6 +26646,7 @@ function buildTagEditor() {
|
|
|
26532
26646
|
if (i >= 0) {
|
|
26533
26647
|
chips.splice(i, 1);
|
|
26534
26648
|
renderChips();
|
|
26649
|
+
emit();
|
|
26535
26650
|
}
|
|
26536
26651
|
});
|
|
26537
26652
|
chip.append(x);
|
|
@@ -26546,6 +26661,7 @@ function buildTagEditor() {
|
|
|
26546
26661
|
renderChips();
|
|
26547
26662
|
hideSuggest();
|
|
26548
26663
|
input.focus();
|
|
26664
|
+
emit();
|
|
26549
26665
|
};
|
|
26550
26666
|
const commitInput = () => {
|
|
26551
26667
|
for (const raw of input.value.split(/\s+/)) addTag(raw);
|
|
@@ -26589,6 +26705,7 @@ function buildTagEditor() {
|
|
|
26589
26705
|
} else if (event.key === "Backspace" && input.value.length === 0 && chips.length > 0) {
|
|
26590
26706
|
chips.pop();
|
|
26591
26707
|
renderChips();
|
|
26708
|
+
emit();
|
|
26592
26709
|
} else if (event.key === "Escape") hideSuggest();
|
|
26593
26710
|
});
|
|
26594
26711
|
document.addEventListener("click", (event) => {
|
|
@@ -26608,22 +26725,20 @@ function buildTagEditor() {
|
|
|
26608
26725
|
renderChips();
|
|
26609
26726
|
}
|
|
26610
26727
|
}
|
|
26728
|
+
},
|
|
26729
|
+
setTags: (tags) => {
|
|
26730
|
+
chips.length = 0;
|
|
26731
|
+
for (const tag of Array.isArray(tags) ? tags : []) {
|
|
26732
|
+
const t = typeof tag === "string" ? tag.trim() : "";
|
|
26733
|
+
if (t.length > 0 && !chips.includes(t)) chips.push(t);
|
|
26734
|
+
}
|
|
26735
|
+
input.value = "";
|
|
26736
|
+
renderChips();
|
|
26737
|
+
hideSuggest();
|
|
26611
26738
|
}
|
|
26612
26739
|
};
|
|
26613
26740
|
}
|
|
26614
26741
|
/**
|
|
26615
|
-
* Build the CodeMirror 6 Markdown editor for the quick-note widget. The editor
|
|
26616
|
-
* lives inside `.dsh-tw-note-editor`; the widget reads/writes through
|
|
26617
|
-
* getValue/setValue and inserts uploaded files at the caret via insertAtCaret.
|
|
26618
|
-
* Mod-Enter (save) is routed through `onSave`.
|
|
26619
|
-
*/
|
|
26620
|
-
function buildEditor(onSave) {
|
|
26621
|
-
return buildMarkdownEditor({
|
|
26622
|
-
placeholder: "写点东西… Markdown 高亮,可 📎/拖入文件\nCtrl+Enter 保存",
|
|
26623
|
-
onSave
|
|
26624
|
-
});
|
|
26625
|
-
}
|
|
26626
|
-
/**
|
|
26627
26742
|
* Upload one file to the wiki (raw body, name in ?name=), then insert a
|
|
26628
26743
|
* Markdown image/link line at the caret of the given editor.
|
|
26629
26744
|
*/
|
|
@@ -26652,240 +26767,421 @@ async function uploadInto(file, editor) {
|
|
|
26652
26767
|
toast(`上传失败:${err instanceof Error ? err.message : String(err)}`);
|
|
26653
26768
|
}
|
|
26654
26769
|
}
|
|
26655
|
-
/**
|
|
26656
|
-
|
|
26657
|
-
|
|
26658
|
-
|
|
26659
|
-
|
|
26660
|
-
|
|
26661
|
-
|
|
26662
|
-
|
|
26663
|
-
head.className = "dsh-tw-note-head";
|
|
26664
|
-
const label = document.createElement("span");
|
|
26665
|
-
label.className = "dsh-tw-note-label";
|
|
26666
|
-
label.textContent = "📝 快速笔记";
|
|
26667
|
-
const closeBtn = document.createElement("button");
|
|
26668
|
-
closeBtn.type = "button";
|
|
26669
|
-
closeBtn.className = "dsh-tw-note-close";
|
|
26670
|
-
closeBtn.title = "收起";
|
|
26671
|
-
closeBtn.textContent = "✕";
|
|
26672
|
-
head.append(label, closeBtn);
|
|
26673
|
-
const fields = document.createElement("div");
|
|
26674
|
-
fields.className = "dsh-tw-note-fields";
|
|
26675
|
-
const titleInput = document.createElement("input");
|
|
26676
|
-
titleInput.className = "dsh-tw-note-title";
|
|
26677
|
-
titleInput.placeholder = "标题(默认时间戳)";
|
|
26678
|
-
const tagEditor = buildTagEditor();
|
|
26679
|
-
fields.append(titleInput, tagEditor.el);
|
|
26680
|
-
let doSave;
|
|
26681
|
-
const editor = buildEditor(() => {
|
|
26682
|
-
doSave?.();
|
|
26683
|
-
});
|
|
26684
|
-
const uploadBtn = document.createElement("button");
|
|
26685
|
-
uploadBtn.type = "button";
|
|
26686
|
-
uploadBtn.className = "dsh-tw-note-upload";
|
|
26687
|
-
uploadBtn.title = "上传文件到 wiki 并插入 Markdown 链接(也可直接拖入编辑器)";
|
|
26688
|
-
uploadBtn.textContent = "📎 上传";
|
|
26689
|
-
const fileInput = document.createElement("input");
|
|
26690
|
-
fileInput.type = "file";
|
|
26691
|
-
fileInput.multiple = true;
|
|
26692
|
-
fileInput.hidden = true;
|
|
26693
|
-
uploadBtn.addEventListener("click", () => {
|
|
26694
|
-
fileInput.click();
|
|
26695
|
-
});
|
|
26696
|
-
fileInput.addEventListener("change", () => {
|
|
26697
|
-
for (const file of Array.from(fileInput.files ?? [])) uploadInto(file, editor);
|
|
26698
|
-
fileInput.value = "";
|
|
26699
|
-
});
|
|
26700
|
-
let dragDepth = 0;
|
|
26701
|
-
editor.el.addEventListener("dragenter", (event) => {
|
|
26702
|
-
event.preventDefault();
|
|
26703
|
-
dragDepth++;
|
|
26704
|
-
editor.el.classList.add("dsh-tw-note-drop");
|
|
26705
|
-
});
|
|
26706
|
-
editor.el.addEventListener("dragover", (event) => {
|
|
26707
|
-
event.preventDefault();
|
|
26708
|
-
});
|
|
26709
|
-
editor.el.addEventListener("dragleave", (event) => {
|
|
26710
|
-
event.preventDefault();
|
|
26711
|
-
dragDepth = Math.max(0, dragDepth - 1);
|
|
26712
|
-
if (dragDepth === 0) editor.el.classList.remove("dsh-tw-note-drop");
|
|
26713
|
-
});
|
|
26714
|
-
editor.el.addEventListener("drop", (event) => {
|
|
26715
|
-
event.preventDefault();
|
|
26716
|
-
dragDepth = 0;
|
|
26717
|
-
editor.el.classList.remove("dsh-tw-note-drop");
|
|
26718
|
-
const files = event.dataTransfer?.files;
|
|
26719
|
-
if (files === void 0 || files.length === 0) return;
|
|
26720
|
-
for (const file of Array.from(files)) uploadInto(file, editor);
|
|
26721
|
-
});
|
|
26722
|
-
const foot = document.createElement("div");
|
|
26723
|
-
foot.className = "dsh-tw-note-foot";
|
|
26724
|
-
const footLeft = document.createElement("div");
|
|
26725
|
-
footLeft.className = "dsh-tw-note-foot-left";
|
|
26726
|
-
const hint = document.createElement("span");
|
|
26727
|
-
hint.className = "dsh-tw-note-hint";
|
|
26728
|
-
hint.textContent = "Ctrl+Enter";
|
|
26729
|
-
footLeft.append(uploadBtn, hint);
|
|
26730
|
-
const footRight = document.createElement("div");
|
|
26731
|
-
footRight.className = "dsh-tw-note-foot-right";
|
|
26732
|
-
const edit = document.createElement("button");
|
|
26733
|
-
edit.type = "button";
|
|
26734
|
-
edit.className = "dsh-tw-note-edit";
|
|
26735
|
-
edit.title = "保存并在 TiddlyWiki 原生编辑器中打开";
|
|
26736
|
-
edit.textContent = "✏️ 在 TW 中编辑";
|
|
26737
|
-
const save = document.createElement("button");
|
|
26738
|
-
save.type = "button";
|
|
26739
|
-
save.className = "dsh-tw-note-save";
|
|
26740
|
-
save.textContent = "保存";
|
|
26741
|
-
footRight.append(edit, save);
|
|
26742
|
-
foot.append(footLeft, footRight);
|
|
26743
|
-
card.append(head, fields, editor.el, foot);
|
|
26744
|
-
const toggle = document.createElement("button");
|
|
26745
|
-
toggle.type = "button";
|
|
26746
|
-
toggle.className = "dsh-tw-note-toggle";
|
|
26747
|
-
toggle.textContent = "📝 快速笔记";
|
|
26748
|
-
root.append(card, toggle);
|
|
26749
|
-
document.body.append(root);
|
|
26750
|
-
let defaultTag = "inbox";
|
|
26770
|
+
/**
|
|
26771
|
+
* Create the quick-note card. Lazy: no DOM is created until the first
|
|
26772
|
+
* `open()`, so a hidden widget (`ui.showQuickNote` off, or never used) leaves
|
|
26773
|
+
* no side effects. Returns a handle the "知识库" FAB controls.
|
|
26774
|
+
*/
|
|
26775
|
+
function createNoteWidget() {
|
|
26776
|
+
let ui;
|
|
26777
|
+
let disposed = false;
|
|
26751
26778
|
let opened = false;
|
|
26779
|
+
let defaultTag = "inbox";
|
|
26780
|
+
let draftTimer;
|
|
26781
|
+
let recentOpen = false;
|
|
26752
26782
|
const resetTitle = () => {
|
|
26753
|
-
titleInput.value = timestampTitle();
|
|
26783
|
+
if (ui !== void 0) ui.titleInput.value = timestampTitle();
|
|
26754
26784
|
};
|
|
26755
|
-
|
|
26756
|
-
|
|
26757
|
-
opened
|
|
26758
|
-
|
|
26759
|
-
|
|
26760
|
-
|
|
26761
|
-
|
|
26785
|
+
/** Debounced draft auto-save (500ms after the last change). */
|
|
26786
|
+
const scheduleDraft = () => {
|
|
26787
|
+
if (disposed || !opened || ui === void 0) return;
|
|
26788
|
+
if (draftTimer !== void 0) {
|
|
26789
|
+
clearTimeout(draftTimer);
|
|
26790
|
+
draftTimer = void 0;
|
|
26791
|
+
}
|
|
26792
|
+
draftTimer = window.setTimeout(() => {
|
|
26793
|
+
draftTimer = void 0;
|
|
26794
|
+
if (ui === void 0) return;
|
|
26795
|
+
const text = ui.editor.getValue();
|
|
26796
|
+
const title = ui.titleInput.value.trim();
|
|
26797
|
+
if (text.trim().length === 0 && title.length === 0) {
|
|
26798
|
+
clearDraft();
|
|
26799
|
+
return;
|
|
26800
|
+
}
|
|
26801
|
+
persistDraft({
|
|
26802
|
+
text,
|
|
26803
|
+
title,
|
|
26804
|
+
tags: ui.tagEditor.getTags(),
|
|
26805
|
+
savedAt: Date.now()
|
|
26806
|
+
});
|
|
26807
|
+
}, DRAFT_DEBOUNCE_MS);
|
|
26762
26808
|
};
|
|
26763
|
-
const
|
|
26764
|
-
|
|
26765
|
-
opened = false;
|
|
26809
|
+
const hideDraftBanner = () => {
|
|
26810
|
+
if (ui !== void 0) ui.draftBanner.hidden = true;
|
|
26766
26811
|
};
|
|
26767
|
-
|
|
26768
|
-
|
|
26769
|
-
|
|
26770
|
-
|
|
26771
|
-
|
|
26772
|
-
|
|
26773
|
-
if (text.length === 0) {
|
|
26774
|
-
toast("内容为空,未保存");
|
|
26775
|
-
return;
|
|
26776
|
-
}
|
|
26777
|
-
save.disabled = true;
|
|
26778
|
-
save.textContent = "保存中…";
|
|
26812
|
+
const closeRecent = () => {
|
|
26813
|
+
recentOpen = false;
|
|
26814
|
+
if (ui !== void 0) ui.recentWrap.hidden = true;
|
|
26815
|
+
};
|
|
26816
|
+
/** Load a tiddler into the editor (recent picker click). */
|
|
26817
|
+
const loadNote = async (title) => {
|
|
26779
26818
|
try {
|
|
26780
|
-
const res = await fetch(
|
|
26781
|
-
method: "POST",
|
|
26782
|
-
headers: { "content-type": "application/json" },
|
|
26783
|
-
body: JSON.stringify({
|
|
26784
|
-
title: titleInput.value.trim(),
|
|
26785
|
-
tags: tagEditor.getTags(),
|
|
26786
|
-
text
|
|
26787
|
-
}),
|
|
26788
|
-
signal: AbortSignal.timeout(1e4)
|
|
26789
|
-
});
|
|
26819
|
+
const res = await fetch(`${GET_ENDPOINT}?title=${encodeURIComponent(title)}`, { signal: AbortSignal.timeout(1e4) });
|
|
26790
26820
|
const payload = await res.json().catch(() => null);
|
|
26791
|
-
if (!res.ok || payload?.ok !== true) {
|
|
26792
|
-
toast(
|
|
26821
|
+
if (!res.ok || payload?.ok !== true || typeof payload.title !== "string") {
|
|
26822
|
+
toast(`读取失败:${payload?.notFound === true ? "不存在" : payload?.error ?? `HTTP ${res.status}`}`);
|
|
26793
26823
|
return;
|
|
26794
26824
|
}
|
|
26795
|
-
|
|
26796
|
-
|
|
26797
|
-
|
|
26798
|
-
|
|
26825
|
+
if (ui === void 0) return;
|
|
26826
|
+
ui.editor.setValue(payload.text ?? "");
|
|
26827
|
+
ui.titleInput.value = payload.title;
|
|
26828
|
+
ui.tagEditor.setTags(payload.tags ?? []);
|
|
26829
|
+
hideDraftBanner();
|
|
26830
|
+
closeRecent();
|
|
26831
|
+
toast(`已载入「${payload.title}」`);
|
|
26832
|
+
ui.editor.focus();
|
|
26799
26833
|
} catch (err) {
|
|
26800
|
-
toast(
|
|
26801
|
-
} finally {
|
|
26802
|
-
save.disabled = false;
|
|
26803
|
-
save.textContent = "保存";
|
|
26834
|
+
toast(`读取失败:${err instanceof Error ? err.message : String(err)}`);
|
|
26804
26835
|
}
|
|
26805
26836
|
};
|
|
26806
|
-
|
|
26807
|
-
|
|
26808
|
-
|
|
26809
|
-
|
|
26810
|
-
|
|
26811
|
-
|
|
26812
|
-
|
|
26813
|
-
|
|
26814
|
-
|
|
26815
|
-
|
|
26816
|
-
|
|
26817
|
-
|
|
26818
|
-
|
|
26819
|
-
|
|
26820
|
-
|
|
26821
|
-
|
|
26822
|
-
|
|
26823
|
-
|
|
26824
|
-
|
|
26825
|
-
|
|
26826
|
-
|
|
26827
|
-
|
|
26828
|
-
|
|
26829
|
-
|
|
26830
|
-
|
|
26837
|
+
/** Toggle the recent-notes dropdown (fetches lazily each open). */
|
|
26838
|
+
const toggleRecent = () => {
|
|
26839
|
+
if (ui === void 0) return;
|
|
26840
|
+
if (recentOpen) {
|
|
26841
|
+
closeRecent();
|
|
26842
|
+
return;
|
|
26843
|
+
}
|
|
26844
|
+
recentOpen = true;
|
|
26845
|
+
ui.recentWrap.hidden = false;
|
|
26846
|
+
ui.recentWrap.textContent = "";
|
|
26847
|
+
const loading = document.createElement("div");
|
|
26848
|
+
loading.className = "dsh-tw-note-recent-muted";
|
|
26849
|
+
loading.textContent = "加载中…";
|
|
26850
|
+
ui.recentWrap.append(loading);
|
|
26851
|
+
(async () => {
|
|
26852
|
+
try {
|
|
26853
|
+
const payload = await (await fetch(`${RECENT_ENDPOINT}?limit=15`, { signal: AbortSignal.timeout(1e4) })).json().catch(() => null);
|
|
26854
|
+
if (!recentOpen || ui === void 0) return;
|
|
26855
|
+
ui.recentWrap.replaceChildren();
|
|
26856
|
+
const items = payload?.ok === true ? payload.items ?? [] : [];
|
|
26857
|
+
if (items.length === 0) {
|
|
26858
|
+
const empty = document.createElement("div");
|
|
26859
|
+
empty.className = "dsh-tw-note-recent-muted";
|
|
26860
|
+
empty.textContent = payload?.ok === true ? "暂无笔记" : `加载失败:${payload?.error ?? "未知"}`;
|
|
26861
|
+
ui.recentWrap.append(empty);
|
|
26862
|
+
return;
|
|
26863
|
+
}
|
|
26864
|
+
for (const item of items) {
|
|
26865
|
+
const row = document.createElement("div");
|
|
26866
|
+
row.className = "dsh-tw-note-recent-item";
|
|
26867
|
+
row.title = item.snippet || item.title;
|
|
26868
|
+
const name = document.createElement("span");
|
|
26869
|
+
name.className = "dsh-tw-note-recent-name";
|
|
26870
|
+
name.textContent = item.title;
|
|
26871
|
+
const meta = document.createElement("span");
|
|
26872
|
+
meta.className = "dsh-tw-note-recent-meta";
|
|
26873
|
+
const bits = [relativeTime(item.modified)];
|
|
26874
|
+
if (item.tags.length > 0) bits.unshift(item.tags.slice(0, 3).join(","));
|
|
26875
|
+
meta.textContent = bits.join(" · ");
|
|
26876
|
+
row.append(name, meta);
|
|
26877
|
+
row.addEventListener("click", () => {
|
|
26878
|
+
loadNote(item.title);
|
|
26879
|
+
});
|
|
26880
|
+
ui.recentWrap.append(row);
|
|
26881
|
+
}
|
|
26882
|
+
} catch (err) {
|
|
26883
|
+
if (!recentOpen || ui === void 0) return;
|
|
26884
|
+
ui.recentWrap.replaceChildren();
|
|
26885
|
+
const empty = document.createElement("div");
|
|
26886
|
+
empty.className = "dsh-tw-note-recent-muted";
|
|
26887
|
+
empty.textContent = `加载失败:${err instanceof Error ? err.message : String(err)}`;
|
|
26888
|
+
ui.recentWrap.append(empty);
|
|
26831
26889
|
}
|
|
26832
|
-
|
|
26833
|
-
|
|
26890
|
+
})();
|
|
26891
|
+
};
|
|
26892
|
+
/** Build the whole card DOM once, then wire every interaction. */
|
|
26893
|
+
const build = () => {
|
|
26894
|
+
if (ui !== void 0) return;
|
|
26895
|
+
const root = document.createElement("div");
|
|
26896
|
+
root.className = "dsh-tw-note";
|
|
26897
|
+
root.hidden = true;
|
|
26898
|
+
const card = document.createElement("div");
|
|
26899
|
+
card.className = "dsh-tw-note-card";
|
|
26900
|
+
const head = document.createElement("div");
|
|
26901
|
+
head.className = "dsh-tw-note-head";
|
|
26902
|
+
const label = document.createElement("span");
|
|
26903
|
+
label.className = "dsh-tw-note-label";
|
|
26904
|
+
label.textContent = "📝 快速笔记";
|
|
26905
|
+
const closeBtn = document.createElement("button");
|
|
26906
|
+
closeBtn.type = "button";
|
|
26907
|
+
closeBtn.className = "dsh-tw-note-close";
|
|
26908
|
+
closeBtn.title = "收起";
|
|
26909
|
+
closeBtn.textContent = "✕";
|
|
26910
|
+
head.append(label, closeBtn);
|
|
26911
|
+
const draftBanner = document.createElement("div");
|
|
26912
|
+
draftBanner.className = "dsh-tw-note-draft";
|
|
26913
|
+
draftBanner.hidden = true;
|
|
26914
|
+
const bannerText = document.createElement("span");
|
|
26915
|
+
bannerText.className = "dsh-tw-note-draft-text";
|
|
26916
|
+
bannerText.textContent = "已恢复未保存草稿";
|
|
26917
|
+
const discardBtn = document.createElement("button");
|
|
26918
|
+
discardBtn.type = "button";
|
|
26919
|
+
discardBtn.className = "dsh-tw-note-draft-discard";
|
|
26920
|
+
discardBtn.textContent = "丢弃";
|
|
26921
|
+
draftBanner.append(bannerText, discardBtn);
|
|
26922
|
+
const fields = document.createElement("div");
|
|
26923
|
+
fields.className = "dsh-tw-note-fields";
|
|
26924
|
+
const titleInput = document.createElement("input");
|
|
26925
|
+
titleInput.className = "dsh-tw-note-title";
|
|
26926
|
+
titleInput.placeholder = "标题(默认时间戳)";
|
|
26927
|
+
const tagEditor = buildTagEditor({ onChange: scheduleDraft });
|
|
26928
|
+
fields.append(titleInput, tagEditor.el);
|
|
26929
|
+
titleInput.addEventListener("input", scheduleDraft);
|
|
26930
|
+
let doSave;
|
|
26931
|
+
const editor = buildMarkdownEditor({
|
|
26932
|
+
placeholder: "写点东西… Markdown 高亮,可 📎/拖入文件\nCtrl+Enter 保存",
|
|
26933
|
+
onSave: () => {
|
|
26934
|
+
doSave?.();
|
|
26935
|
+
},
|
|
26936
|
+
onChange: scheduleDraft
|
|
26937
|
+
});
|
|
26938
|
+
const uploadBtn = document.createElement("button");
|
|
26939
|
+
uploadBtn.type = "button";
|
|
26940
|
+
uploadBtn.className = "dsh-tw-note-upload";
|
|
26941
|
+
uploadBtn.title = "上传文件到 wiki 并插入 Markdown 链接(也可直接拖入编辑器)";
|
|
26942
|
+
uploadBtn.textContent = "📎 上传";
|
|
26943
|
+
const fileInput = document.createElement("input");
|
|
26944
|
+
fileInput.type = "file";
|
|
26945
|
+
fileInput.multiple = true;
|
|
26946
|
+
fileInput.hidden = true;
|
|
26947
|
+
uploadBtn.addEventListener("click", () => {
|
|
26948
|
+
fileInput.click();
|
|
26949
|
+
});
|
|
26950
|
+
fileInput.addEventListener("change", () => {
|
|
26951
|
+
for (const file of Array.from(fileInput.files ?? [])) uploadInto(file, editor);
|
|
26952
|
+
fileInput.value = "";
|
|
26953
|
+
});
|
|
26954
|
+
let dragDepth = 0;
|
|
26955
|
+
editor.el.addEventListener("dragenter", (event) => {
|
|
26956
|
+
event.preventDefault();
|
|
26957
|
+
dragDepth++;
|
|
26958
|
+
editor.el.classList.add("dsh-tw-note-drop");
|
|
26959
|
+
});
|
|
26960
|
+
editor.el.addEventListener("dragover", (event) => {
|
|
26961
|
+
event.preventDefault();
|
|
26962
|
+
});
|
|
26963
|
+
editor.el.addEventListener("dragleave", (event) => {
|
|
26964
|
+
event.preventDefault();
|
|
26965
|
+
dragDepth = Math.max(0, dragDepth - 1);
|
|
26966
|
+
if (dragDepth === 0) editor.el.classList.remove("dsh-tw-note-drop");
|
|
26967
|
+
});
|
|
26968
|
+
editor.el.addEventListener("drop", (event) => {
|
|
26969
|
+
event.preventDefault();
|
|
26970
|
+
dragDepth = 0;
|
|
26971
|
+
editor.el.classList.remove("dsh-tw-note-drop");
|
|
26972
|
+
const files = event.dataTransfer?.files;
|
|
26973
|
+
if (files === void 0 || files.length === 0) return;
|
|
26974
|
+
for (const file of Array.from(files)) uploadInto(file, editor);
|
|
26975
|
+
});
|
|
26976
|
+
const foot = document.createElement("div");
|
|
26977
|
+
foot.className = "dsh-tw-note-foot";
|
|
26978
|
+
const footLeft = document.createElement("div");
|
|
26979
|
+
footLeft.className = "dsh-tw-note-foot-left";
|
|
26980
|
+
const hint = document.createElement("span");
|
|
26981
|
+
hint.className = "dsh-tw-note-hint";
|
|
26982
|
+
hint.textContent = "Ctrl+Enter";
|
|
26983
|
+
const recentBtn = document.createElement("button");
|
|
26984
|
+
recentBtn.type = "button";
|
|
26985
|
+
recentBtn.className = "dsh-tw-note-recent-btn";
|
|
26986
|
+
recentBtn.title = "最近修改的笔记,点击载入继续编辑";
|
|
26987
|
+
recentBtn.textContent = "🕘 最近";
|
|
26988
|
+
recentBtn.addEventListener("click", toggleRecent);
|
|
26989
|
+
footLeft.append(uploadBtn, recentBtn, hint);
|
|
26990
|
+
const footRight = document.createElement("div");
|
|
26991
|
+
footRight.className = "dsh-tw-note-foot-right";
|
|
26992
|
+
const editBtn = document.createElement("button");
|
|
26993
|
+
editBtn.type = "button";
|
|
26994
|
+
editBtn.className = "dsh-tw-note-edit";
|
|
26995
|
+
editBtn.title = "保存并在 TiddlyWiki 原生编辑器中打开";
|
|
26996
|
+
editBtn.textContent = "✏️ 在 TW 中编辑";
|
|
26997
|
+
const saveBtn = document.createElement("button");
|
|
26998
|
+
saveBtn.type = "button";
|
|
26999
|
+
saveBtn.className = "dsh-tw-note-save";
|
|
27000
|
+
saveBtn.textContent = "保存";
|
|
27001
|
+
footRight.append(editBtn, saveBtn);
|
|
27002
|
+
foot.append(footLeft, footRight);
|
|
27003
|
+
const recentWrap = document.createElement("div");
|
|
27004
|
+
recentWrap.className = "dsh-tw-note-recent";
|
|
27005
|
+
recentWrap.hidden = true;
|
|
27006
|
+
card.append(head, draftBanner, fields, editor.el, foot);
|
|
27007
|
+
root.append(card, recentWrap);
|
|
27008
|
+
document.body.append(root);
|
|
27009
|
+
ui = {
|
|
27010
|
+
root,
|
|
27011
|
+
card,
|
|
27012
|
+
editor,
|
|
27013
|
+
titleInput,
|
|
27014
|
+
tagEditor,
|
|
27015
|
+
saveBtn,
|
|
27016
|
+
editBtn,
|
|
27017
|
+
draftBanner,
|
|
27018
|
+
recentBtn,
|
|
27019
|
+
recentWrap
|
|
27020
|
+
};
|
|
27021
|
+
const close = () => {
|
|
27022
|
+
opened = false;
|
|
27023
|
+
root.hidden = true;
|
|
27024
|
+
closeRecent();
|
|
27025
|
+
};
|
|
27026
|
+
const saveDone = () => {
|
|
27027
|
+
clearDraft();
|
|
27028
|
+
hideDraftBanner();
|
|
27029
|
+
editor.setValue("");
|
|
27030
|
+
titleInput.value = timestampTitle();
|
|
27031
|
+
tagEditor.setTags([]);
|
|
27032
|
+
close();
|
|
27033
|
+
};
|
|
27034
|
+
doSave = async () => {
|
|
27035
|
+
const text = editor.getValue().trim();
|
|
27036
|
+
if (text.length === 0) {
|
|
27037
|
+
toast("内容为空,未保存");
|
|
26834
27038
|
return;
|
|
26835
27039
|
}
|
|
26836
|
-
|
|
26837
|
-
|
|
26838
|
-
|
|
26839
|
-
|
|
26840
|
-
|
|
26841
|
-
|
|
26842
|
-
|
|
26843
|
-
|
|
27040
|
+
saveBtn.disabled = true;
|
|
27041
|
+
saveBtn.textContent = "保存中…";
|
|
27042
|
+
try {
|
|
27043
|
+
const res = await fetch(NOTE_ENDPOINT, {
|
|
27044
|
+
method: "POST",
|
|
27045
|
+
headers: { "content-type": "application/json" },
|
|
27046
|
+
body: JSON.stringify({
|
|
27047
|
+
title: titleInput.value.trim(),
|
|
27048
|
+
tags: tagEditor.getTags(),
|
|
27049
|
+
text
|
|
27050
|
+
}),
|
|
27051
|
+
signal: AbortSignal.timeout(1e4)
|
|
27052
|
+
});
|
|
27053
|
+
const payload = await res.json().catch(() => null);
|
|
27054
|
+
if (!res.ok || payload?.ok !== true) {
|
|
27055
|
+
toast(`保存失败:${payload?.error ?? `HTTP ${res.status}`}`);
|
|
27056
|
+
return;
|
|
27057
|
+
}
|
|
27058
|
+
saveDone();
|
|
27059
|
+
toast(`已保存「${payload.title ?? titleInput.value}」`);
|
|
27060
|
+
} catch (err) {
|
|
27061
|
+
toast(`保存失败:${err instanceof Error ? err.message : String(err)}`);
|
|
27062
|
+
} finally {
|
|
27063
|
+
saveBtn.disabled = false;
|
|
27064
|
+
saveBtn.textContent = "保存";
|
|
27065
|
+
}
|
|
27066
|
+
};
|
|
27067
|
+
saveBtn.addEventListener("click", () => {
|
|
27068
|
+
doSave?.();
|
|
27069
|
+
});
|
|
27070
|
+
/** Save (if non-empty) and open the tiddler in TW's native editor. */
|
|
27071
|
+
const doEdit = async () => {
|
|
27072
|
+
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
|
|
27073
|
+
const text = editor.getValue();
|
|
27074
|
+
const tags = tagEditor.getTags();
|
|
27075
|
+
editBtn.disabled = true;
|
|
27076
|
+
editBtn.textContent = "打开中…";
|
|
27077
|
+
try {
|
|
27078
|
+
const res = await fetch(EDIT_ENDPOINT, {
|
|
27079
|
+
method: "POST",
|
|
27080
|
+
headers: { "content-type": "application/json" },
|
|
27081
|
+
body: JSON.stringify({
|
|
27082
|
+
title,
|
|
27083
|
+
tags,
|
|
27084
|
+
text
|
|
27085
|
+
}),
|
|
27086
|
+
signal: AbortSignal.timeout(1e4)
|
|
27087
|
+
});
|
|
27088
|
+
const payload = await res.json().catch(() => null);
|
|
27089
|
+
if (!res.ok || payload?.ok !== true) {
|
|
27090
|
+
toast(`打开失败:${payload?.error ?? `HTTP ${res.status}`}`);
|
|
27091
|
+
return;
|
|
27092
|
+
}
|
|
27093
|
+
if (typeof payload.twUrl !== "string" || typeof payload.draftTitle !== "string") {
|
|
27094
|
+
toast("打开失败:服务未返回编辑器地址");
|
|
27095
|
+
return;
|
|
27096
|
+
}
|
|
27097
|
+
clearDraft();
|
|
27098
|
+
hideDraftBanner();
|
|
27099
|
+
openEditorPopup(`${new URL(payload.twUrl, location.origin).href}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title);
|
|
27100
|
+
toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`);
|
|
27101
|
+
} catch (err) {
|
|
27102
|
+
toast(`打开失败:${err instanceof Error ? err.message : String(err)}`);
|
|
27103
|
+
} finally {
|
|
27104
|
+
editBtn.disabled = false;
|
|
27105
|
+
editBtn.textContent = "✏️ 在 TW 中编辑";
|
|
27106
|
+
}
|
|
27107
|
+
};
|
|
27108
|
+
editBtn.addEventListener("click", () => {
|
|
27109
|
+
doEdit();
|
|
27110
|
+
});
|
|
27111
|
+
closeBtn.addEventListener("click", close);
|
|
27112
|
+
document.addEventListener("click", (event) => {
|
|
27113
|
+
if (!opened) return;
|
|
27114
|
+
const target = event.target;
|
|
27115
|
+
if (root.contains(target)) return;
|
|
27116
|
+
if (recentWrap.contains(target)) return;
|
|
27117
|
+
close();
|
|
27118
|
+
}, true);
|
|
27119
|
+
discardBtn.addEventListener("click", () => {
|
|
27120
|
+
clearDraft();
|
|
27121
|
+
hideDraftBanner();
|
|
27122
|
+
editor.setValue("");
|
|
27123
|
+
titleInput.value = timestampTitle();
|
|
27124
|
+
tagEditor.setTags([]);
|
|
27125
|
+
toast("已丢弃草稿");
|
|
27126
|
+
});
|
|
26844
27127
|
};
|
|
26845
|
-
edit.addEventListener("click", () => {
|
|
26846
|
-
doEdit();
|
|
26847
|
-
});
|
|
26848
27128
|
return {
|
|
26849
|
-
|
|
26850
|
-
|
|
26851
|
-
|
|
26852
|
-
|
|
26853
|
-
|
|
26854
|
-
|
|
26855
|
-
|
|
26856
|
-
|
|
26857
|
-
|
|
26858
|
-
|
|
26859
|
-
|
|
26860
|
-
|
|
26861
|
-
|
|
26862
|
-
|
|
26863
|
-
|
|
26864
|
-
|
|
26865
|
-
|
|
26866
|
-
|
|
27129
|
+
async open() {
|
|
27130
|
+
if (disposed) return;
|
|
27131
|
+
build();
|
|
27132
|
+
if (ui === void 0 || opened) return;
|
|
27133
|
+
opened = true;
|
|
27134
|
+
ui.root.hidden = false;
|
|
27135
|
+
const draft = loadDraft();
|
|
27136
|
+
if (draft !== null && (draft.text.trim().length > 0 || draft.title.trim().length > 0)) {
|
|
27137
|
+
ui.editor.setValue(draft.text);
|
|
27138
|
+
ui.titleInput.value = draft.title;
|
|
27139
|
+
ui.tagEditor.setTags(draft.tags);
|
|
27140
|
+
ui.draftBanner.hidden = false;
|
|
27141
|
+
} else {
|
|
27142
|
+
resetTitle();
|
|
27143
|
+
defaultTag = (await fetchUiOptions()).defaultTag;
|
|
27144
|
+
ui.tagEditor.setDefault(defaultTag);
|
|
27145
|
+
}
|
|
27146
|
+
ui.editor.focus();
|
|
27147
|
+
},
|
|
27148
|
+
close() {
|
|
27149
|
+
if (ui === void 0) return;
|
|
27150
|
+
opened = false;
|
|
27151
|
+
ui.root.hidden = true;
|
|
27152
|
+
closeRecent();
|
|
27153
|
+
},
|
|
27154
|
+
async toggle() {
|
|
27155
|
+
if (opened) this.close();
|
|
27156
|
+
else await this.open();
|
|
27157
|
+
},
|
|
27158
|
+
isOpen() {
|
|
27159
|
+
return opened;
|
|
27160
|
+
},
|
|
27161
|
+
dispose() {
|
|
27162
|
+
disposed = true;
|
|
27163
|
+
if (draftTimer !== void 0) {
|
|
27164
|
+
clearTimeout(draftTimer);
|
|
27165
|
+
draftTimer = void 0;
|
|
27166
|
+
}
|
|
27167
|
+
ui?.editor.view.destroy();
|
|
27168
|
+
ui?.root.remove();
|
|
27169
|
+
document.querySelector(".dsh-tw-toast")?.remove();
|
|
27170
|
+
ui = void 0;
|
|
27171
|
+
}
|
|
26867
27172
|
};
|
|
26868
|
-
(async () => {
|
|
26869
|
-
const ui = await fetchUiOptions();
|
|
26870
|
-
if (disposed || !ui.showQuickNote) return;
|
|
26871
|
-
const widget = buildWidget();
|
|
26872
|
-
root = widget.root;
|
|
26873
|
-
editorView = widget.editorView;
|
|
26874
|
-
})();
|
|
26875
|
-
return disposer;
|
|
26876
27173
|
}
|
|
26877
27174
|
//#endregion
|
|
26878
27175
|
//#region src/client/sync-button.ts
|
|
26879
27176
|
/**
|
|
26880
|
-
*
|
|
26881
|
-
*
|
|
26882
|
-
*
|
|
27177
|
+
* Sync controller — the git pull→commit→push logic + live git status, decoupled
|
|
27178
|
+
* from any DOM. In v0.5 the visual (status dot, 同步 entry, syncing spinner)
|
|
27179
|
+
* lives in the "知识库" FAB (knowledge-fab.ts); this module only owns state,
|
|
27180
|
+
* polling and the one-click sync call.
|
|
26883
27181
|
*
|
|
26884
|
-
* - honors `ui.showSyncButton`: the button is mounted async and never created
|
|
26885
|
-
* when the option is off (settings page toggle);
|
|
26886
27182
|
* - polls /dsh-tiddlywiki/status every 30s for the git state;
|
|
26887
|
-
* -
|
|
26888
|
-
*
|
|
27183
|
+
* - `trigger()` POSTs /dsh-tiddlywiki/sync (pull → commit → push), returns the
|
|
27184
|
+
* fresh state and notifies subscribers.
|
|
26889
27185
|
*
|
|
26890
27186
|
* Status dot mapping (git summary from /status):
|
|
26891
27187
|
* offline → gray (no repo / service unreachable)
|
|
@@ -26893,13 +27189,11 @@ function mountNoteWidget() {
|
|
|
26893
27189
|
* behind → red (remote has commits we don't — pull will rebase)
|
|
26894
27190
|
* clean → green (worktree clean)
|
|
26895
27191
|
*
|
|
26896
|
-
* @module dsh-tiddlywiki/client/sync-
|
|
27192
|
+
* @module dsh-tiddlywiki/client/sync-controller
|
|
26897
27193
|
*/
|
|
26898
|
-
const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
|
|
27194
|
+
const STATUS_ENDPOINT$1 = "/dsh-tiddlywiki/status";
|
|
26899
27195
|
const SYNC_ENDPOINT$1 = "/dsh-tiddlywiki/sync";
|
|
26900
27196
|
const POLL_MS = 3e4;
|
|
26901
|
-
/** Inline sync icon (circular arrows, stroke-based, theme-colored). */
|
|
26902
|
-
const SYNC_ICON = "<svg viewBox=\"0 0 16 16\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M13 8a5 5 0 1 1-1.4-3.5\"/><path d=\"M13 2.5v3h-3\"/></svg>";
|
|
26903
27197
|
function pad(n) {
|
|
26904
27198
|
return n < 10 ? `0${n}` : String(n);
|
|
26905
27199
|
}
|
|
@@ -26909,83 +27203,83 @@ function clock(date) {
|
|
|
26909
27203
|
}
|
|
26910
27204
|
async function fetchStatus() {
|
|
26911
27205
|
try {
|
|
26912
|
-
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(8e3) });
|
|
27206
|
+
const res = await fetch(STATUS_ENDPOINT$1, { signal: AbortSignal.timeout(8e3) });
|
|
26913
27207
|
if (!res.ok) return null;
|
|
26914
27208
|
return await res.json();
|
|
26915
27209
|
} catch {
|
|
26916
27210
|
return null;
|
|
26917
27211
|
}
|
|
26918
27212
|
}
|
|
27213
|
+
/** Map a git summary onto a SyncStateView. */
|
|
27214
|
+
function buildState(payload, lastSync) {
|
|
27215
|
+
const git = payload?.git;
|
|
27216
|
+
const bits = ["同步知识库"];
|
|
27217
|
+
let state = "offline";
|
|
27218
|
+
let label = "离线";
|
|
27219
|
+
if (git === null || git === void 0 || git.exists !== true) bits.push("git 仓库不可用");
|
|
27220
|
+
else {
|
|
27221
|
+
bits.push(`分支 ${git.branch ?? "?"}`);
|
|
27222
|
+
if (typeof git.lastCommit === "string") bits.push(git.lastCommit);
|
|
27223
|
+
const behind = typeof git.behind === "number" ? git.behind : 0;
|
|
27224
|
+
const ahead = typeof git.ahead === "number" ? git.ahead : 0;
|
|
27225
|
+
if (ahead > 0) bits.push(`领先 ${ahead}`);
|
|
27226
|
+
if (behind > 0) bits.push(`落后 ${behind}`);
|
|
27227
|
+
if (git.dirty === true) {
|
|
27228
|
+
state = "dirty";
|
|
27229
|
+
label = "待提交";
|
|
27230
|
+
bits.push(`有 ${git.dirtyFiles?.length ?? 0} 个未提交改动`);
|
|
27231
|
+
} else if (behind > 0) {
|
|
27232
|
+
state = "behind";
|
|
27233
|
+
label = "可更新";
|
|
27234
|
+
} else {
|
|
27235
|
+
state = "clean";
|
|
27236
|
+
label = "已同步";
|
|
27237
|
+
}
|
|
27238
|
+
}
|
|
27239
|
+
if (lastSync !== void 0) bits.push(`上次同步 ${clock(lastSync)}`);
|
|
27240
|
+
return {
|
|
27241
|
+
state,
|
|
27242
|
+
label,
|
|
27243
|
+
tooltip: bits.join(" · "),
|
|
27244
|
+
...lastSync !== void 0 ? { lastSync } : {}
|
|
27245
|
+
};
|
|
27246
|
+
}
|
|
26919
27247
|
/**
|
|
26920
|
-
*
|
|
26921
|
-
* `
|
|
27248
|
+
* Create the sync controller: starts a 30s status poll immediately and exposes
|
|
27249
|
+
* `trigger()` for the FAB's 同步 entry. No DOM is created here.
|
|
26922
27250
|
*/
|
|
26923
|
-
function
|
|
26924
|
-
let
|
|
26925
|
-
|
|
26926
|
-
|
|
26927
|
-
|
|
27251
|
+
function createSyncController() {
|
|
27252
|
+
let state = {
|
|
27253
|
+
state: "offline",
|
|
27254
|
+
label: "离线",
|
|
27255
|
+
tooltip: "同步知识库"
|
|
27256
|
+
};
|
|
26928
27257
|
let lastSync;
|
|
26929
27258
|
let timer;
|
|
26930
|
-
|
|
26931
|
-
|
|
26932
|
-
|
|
26933
|
-
|
|
26934
|
-
btn.title = "同步知识库(pull → commit → push)";
|
|
26935
|
-
const icon = document.createElement("span");
|
|
26936
|
-
icon.className = "dsh-tw-sync-icon";
|
|
26937
|
-
icon.innerHTML = SYNC_ICON;
|
|
26938
|
-
dot = document.createElement("span");
|
|
26939
|
-
dot.className = "dsh-tw-sync-dot";
|
|
26940
|
-
dot.dataset.state = "offline";
|
|
26941
|
-
label = document.createElement("span");
|
|
26942
|
-
label.className = "dsh-tw-sync-label";
|
|
26943
|
-
label.textContent = "同步";
|
|
26944
|
-
btn.append(icon, label, dot);
|
|
26945
|
-
btn.addEventListener("click", () => {
|
|
26946
|
-
doSync();
|
|
26947
|
-
});
|
|
26948
|
-
document.body.append(btn);
|
|
26949
|
-
return btn;
|
|
27259
|
+
let syncing = false;
|
|
27260
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
27261
|
+
const emit = () => {
|
|
27262
|
+
for (const cb of [...listeners]) cb();
|
|
26950
27263
|
};
|
|
26951
|
-
/** Map a git summary onto the dot + label + tooltip. */
|
|
26952
27264
|
const applyStatus = (payload) => {
|
|
26953
|
-
|
|
26954
|
-
|
|
26955
|
-
|
|
26956
|
-
|
|
26957
|
-
let word = "离线";
|
|
26958
|
-
if (git === null || git === void 0 || git.exists !== true) bits.push("git 仓库不可用");
|
|
26959
|
-
else {
|
|
26960
|
-
bits.push(`分支 ${git.branch ?? "?"}`);
|
|
26961
|
-
if (typeof git.lastCommit === "string") bits.push(git.lastCommit);
|
|
26962
|
-
const behind = typeof git.behind === "number" ? git.behind : 0;
|
|
26963
|
-
const ahead = typeof git.ahead === "number" ? git.ahead : 0;
|
|
26964
|
-
if (ahead > 0) bits.push(`领先 ${ahead}`);
|
|
26965
|
-
if (behind > 0) bits.push(`落后 ${behind}`);
|
|
26966
|
-
if (git.dirty === true) {
|
|
26967
|
-
state = "dirty";
|
|
26968
|
-
word = "待提交";
|
|
26969
|
-
bits.push(`有 ${git.dirtyFiles?.length ?? 0} 个未提交改动`);
|
|
26970
|
-
} else if (behind > 0) {
|
|
26971
|
-
state = "behind";
|
|
26972
|
-
word = "可更新";
|
|
26973
|
-
} else {
|
|
26974
|
-
state = "clean";
|
|
26975
|
-
word = "已同步";
|
|
26976
|
-
}
|
|
27265
|
+
const next = buildState(payload, lastSync);
|
|
27266
|
+
if (next.state !== state.state || next.tooltip !== state.tooltip) {
|
|
27267
|
+
state = next;
|
|
27268
|
+
emit();
|
|
26977
27269
|
}
|
|
26978
|
-
|
|
26979
|
-
|
|
26980
|
-
|
|
26981
|
-
label.textContent = word;
|
|
27270
|
+
};
|
|
27271
|
+
const poll = async () => {
|
|
27272
|
+
applyStatus(await fetchStatus());
|
|
26982
27273
|
};
|
|
26983
27274
|
const doSync = async () => {
|
|
26984
|
-
if (
|
|
26985
|
-
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
27275
|
+
if (syncing) return state;
|
|
27276
|
+
syncing = true;
|
|
27277
|
+
state = {
|
|
27278
|
+
...state,
|
|
27279
|
+
state: "syncing",
|
|
27280
|
+
label: "同步中…"
|
|
27281
|
+
};
|
|
27282
|
+
emit();
|
|
26989
27283
|
try {
|
|
26990
27284
|
const res = await fetch(SYNC_ENDPOINT$1, {
|
|
26991
27285
|
method: "POST",
|
|
@@ -27003,29 +27297,249 @@ function mountSyncButton() {
|
|
|
27003
27297
|
}
|
|
27004
27298
|
toast(`同步完成:${payload.message ?? "OK"}${detail}`);
|
|
27005
27299
|
}
|
|
27006
|
-
|
|
27300
|
+
await poll();
|
|
27007
27301
|
} catch (err) {
|
|
27008
27302
|
toast(`同步失败:${err instanceof Error ? err.message : String(err)}`);
|
|
27009
27303
|
} finally {
|
|
27010
|
-
|
|
27011
|
-
|
|
27304
|
+
syncing = false;
|
|
27305
|
+
await poll();
|
|
27012
27306
|
}
|
|
27307
|
+
return state;
|
|
27013
27308
|
};
|
|
27014
|
-
|
|
27015
|
-
|
|
27309
|
+
poll();
|
|
27310
|
+
timer = window.setInterval(() => {
|
|
27311
|
+
poll();
|
|
27312
|
+
}, POLL_MS);
|
|
27313
|
+
return {
|
|
27314
|
+
getState: () => state,
|
|
27315
|
+
trigger: doSync,
|
|
27316
|
+
subscribe(cb) {
|
|
27317
|
+
listeners.add(cb);
|
|
27318
|
+
return () => {
|
|
27319
|
+
listeners.delete(cb);
|
|
27320
|
+
};
|
|
27321
|
+
},
|
|
27322
|
+
dispose() {
|
|
27323
|
+
if (timer !== void 0) {
|
|
27324
|
+
clearInterval(timer);
|
|
27325
|
+
timer = void 0;
|
|
27326
|
+
}
|
|
27327
|
+
listeners.clear();
|
|
27328
|
+
}
|
|
27329
|
+
};
|
|
27330
|
+
}
|
|
27331
|
+
//#endregion
|
|
27332
|
+
//#region src/client/knowledge-fab.ts
|
|
27333
|
+
const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
|
|
27334
|
+
/** Book icon (same visual family as the sidebar entry). */
|
|
27335
|
+
const BOOK_ICON = "<svg viewBox=\"0 0 16 16\" width=\"17\" height=\"17\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 2.5h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z\"/><path d=\"M6 6h4M6 8.5h2.5\"/></svg>";
|
|
27336
|
+
async function fetchUiFlags() {
|
|
27337
|
+
try {
|
|
27338
|
+
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5e3) });
|
|
27339
|
+
if (!res.ok) return {
|
|
27340
|
+
showQuickNote: true,
|
|
27341
|
+
showPanelStatus: true,
|
|
27342
|
+
showSyncButton: true
|
|
27343
|
+
};
|
|
27344
|
+
const p = await res.json();
|
|
27345
|
+
return {
|
|
27346
|
+
showQuickNote: p.ui?.showQuickNote !== false,
|
|
27347
|
+
showPanelStatus: p.ui?.showPanelStatus !== false,
|
|
27348
|
+
showSyncButton: p.ui?.showSyncButton !== false
|
|
27349
|
+
};
|
|
27350
|
+
} catch {
|
|
27351
|
+
return {
|
|
27352
|
+
showQuickNote: true,
|
|
27353
|
+
showPanelStatus: true,
|
|
27354
|
+
showSyncButton: true
|
|
27355
|
+
};
|
|
27356
|
+
}
|
|
27357
|
+
}
|
|
27358
|
+
/** Fetch the TW service health for the menu's status line. */
|
|
27359
|
+
async function fetchTwStatus() {
|
|
27360
|
+
try {
|
|
27361
|
+
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(8e3) });
|
|
27362
|
+
if (!res.ok) return {
|
|
27363
|
+
state: "failed",
|
|
27364
|
+
text: "状态不可达"
|
|
27365
|
+
};
|
|
27366
|
+
const p = await res.json();
|
|
27367
|
+
if (p.status === "running") return {
|
|
27368
|
+
state: "running",
|
|
27369
|
+
text: `TW 在线 · ${p.url ?? ""}`
|
|
27370
|
+
};
|
|
27371
|
+
if (p.status === "starting") return {
|
|
27372
|
+
state: "starting",
|
|
27373
|
+
text: "TW 启动中…"
|
|
27374
|
+
};
|
|
27375
|
+
return {
|
|
27376
|
+
state: "failed",
|
|
27377
|
+
text: p.error ?? `TW 状态:${p.status ?? "?"}`
|
|
27378
|
+
};
|
|
27379
|
+
} catch {
|
|
27380
|
+
return {
|
|
27381
|
+
state: "failed",
|
|
27382
|
+
text: "状态不可达"
|
|
27383
|
+
};
|
|
27384
|
+
}
|
|
27385
|
+
}
|
|
27386
|
+
/**
|
|
27387
|
+
* Mount the knowledge FAB. Fetches /status first to read the ui.* flags; when
|
|
27388
|
+
* every entry is disabled the FAB is never created. Returns a disposer.
|
|
27389
|
+
*/
|
|
27390
|
+
function mountKnowledgeFab(state, note, sync) {
|
|
27391
|
+
let disposed = false;
|
|
27392
|
+
let root;
|
|
27393
|
+
let fabBtn;
|
|
27394
|
+
let dot;
|
|
27395
|
+
let menu;
|
|
27396
|
+
let twStatusEl;
|
|
27397
|
+
let twDot;
|
|
27398
|
+
let panelLabel;
|
|
27399
|
+
let menuOpen = false;
|
|
27400
|
+
let gitRowDot;
|
|
27401
|
+
let gitRowText;
|
|
27402
|
+
const closeMenu = () => {
|
|
27403
|
+
menuOpen = false;
|
|
27404
|
+
if (menu !== void 0) menu.hidden = true;
|
|
27405
|
+
};
|
|
27406
|
+
/** Refresh the git dot + tooltip from the controller (FAB dot + menu row). */
|
|
27407
|
+
const renderDot = () => {
|
|
27408
|
+
const s = sync.getState();
|
|
27409
|
+
if (dot !== void 0) dot.dataset.state = s.state;
|
|
27410
|
+
if (fabBtn !== void 0) fabBtn.title = `知识库 · ${s.tooltip}`;
|
|
27411
|
+
if (gitRowDot !== void 0) gitRowDot.dataset.state = s.state;
|
|
27412
|
+
if (gitRowText !== void 0) gitRowText.textContent = s.tooltip;
|
|
27413
|
+
};
|
|
27414
|
+
/** Refresh the TW service status line in the menu. */
|
|
27415
|
+
const refreshTwStatus = async () => {
|
|
27416
|
+
const info = await fetchTwStatus();
|
|
27417
|
+
if (disposed) return;
|
|
27418
|
+
if (twStatusEl !== void 0) twStatusEl.textContent = info.text;
|
|
27419
|
+
if (twDot !== void 0) twDot.dataset.state = info.state;
|
|
27420
|
+
};
|
|
27421
|
+
const build = (flags) => {
|
|
27422
|
+
if (disposed) return;
|
|
27423
|
+
root = document.createElement("div");
|
|
27424
|
+
root.className = "dsh-tw-fab-wrap";
|
|
27425
|
+
menu = document.createElement("div");
|
|
27426
|
+
menu.className = "dsh-tw-fab-menu";
|
|
27427
|
+
menu.hidden = true;
|
|
27428
|
+
if (flags.showPanelStatus) {
|
|
27429
|
+
const twRow = document.createElement("div");
|
|
27430
|
+
twRow.className = "dsh-tw-fab-status";
|
|
27431
|
+
twDot = document.createElement("span");
|
|
27432
|
+
twDot.className = "dsh-tw-fab-status-dot";
|
|
27433
|
+
twDot.dataset.state = "unknown";
|
|
27434
|
+
twStatusEl = document.createElement("span");
|
|
27435
|
+
twStatusEl.className = "dsh-tw-fab-status-text";
|
|
27436
|
+
twStatusEl.textContent = "TiddlyWiki 服务…";
|
|
27437
|
+
twRow.append(twDot, twStatusEl);
|
|
27438
|
+
menu.append(twRow);
|
|
27439
|
+
}
|
|
27440
|
+
if (flags.showSyncButton) {
|
|
27441
|
+
const gitRow = document.createElement("div");
|
|
27442
|
+
gitRow.className = "dsh-tw-fab-status";
|
|
27443
|
+
gitRowDot = document.createElement("span");
|
|
27444
|
+
gitRowDot.className = "dsh-tw-fab-status-dot";
|
|
27445
|
+
gitRowDot.dataset.state = sync.getState().state;
|
|
27446
|
+
gitRowText = document.createElement("span");
|
|
27447
|
+
gitRowText.className = "dsh-tw-fab-status-text";
|
|
27448
|
+
gitRowText.textContent = sync.getState().tooltip;
|
|
27449
|
+
gitRow.append(gitRowDot, gitRowText);
|
|
27450
|
+
menu.append(gitRow);
|
|
27451
|
+
}
|
|
27452
|
+
if (flags.showQuickNote) {
|
|
27453
|
+
const item = document.createElement("button");
|
|
27454
|
+
item.type = "button";
|
|
27455
|
+
item.className = "dsh-tw-fab-item";
|
|
27456
|
+
item.textContent = "📝 快速笔记";
|
|
27457
|
+
item.addEventListener("click", () => {
|
|
27458
|
+
closeMenu();
|
|
27459
|
+
note.toggle();
|
|
27460
|
+
});
|
|
27461
|
+
menu.append(item);
|
|
27462
|
+
}
|
|
27463
|
+
if (flags.showPanelStatus) {
|
|
27464
|
+
panelLabel = document.createElement("span");
|
|
27465
|
+
panelLabel.textContent = "打开 TW 面板";
|
|
27466
|
+
const item = document.createElement("button");
|
|
27467
|
+
item.type = "button";
|
|
27468
|
+
item.className = "dsh-tw-fab-item";
|
|
27469
|
+
item.append(document.createTextNode("🖥 "), panelLabel);
|
|
27470
|
+
item.addEventListener("click", () => {
|
|
27471
|
+
closeMenu();
|
|
27472
|
+
state.toggle();
|
|
27473
|
+
});
|
|
27474
|
+
menu.append(item);
|
|
27475
|
+
const reload = document.createElement("button");
|
|
27476
|
+
reload.type = "button";
|
|
27477
|
+
reload.className = "dsh-tw-fab-item";
|
|
27478
|
+
reload.textContent = "🔄 重载 TW 面板";
|
|
27479
|
+
reload.addEventListener("click", () => {
|
|
27480
|
+
closeMenu();
|
|
27481
|
+
document.dispatchEvent(new CustomEvent("dsh-tw-panel-reload"));
|
|
27482
|
+
refreshTwStatus();
|
|
27483
|
+
});
|
|
27484
|
+
menu.append(reload);
|
|
27485
|
+
}
|
|
27486
|
+
if (flags.showSyncButton) {
|
|
27487
|
+
const item = document.createElement("button");
|
|
27488
|
+
item.type = "button";
|
|
27489
|
+
item.className = "dsh-tw-fab-item";
|
|
27490
|
+
item.textContent = "🔁 同步";
|
|
27491
|
+
item.addEventListener("click", () => {
|
|
27492
|
+
closeMenu();
|
|
27493
|
+
sync.trigger();
|
|
27494
|
+
});
|
|
27495
|
+
menu.append(item);
|
|
27496
|
+
}
|
|
27497
|
+
fabBtn = document.createElement("button");
|
|
27498
|
+
fabBtn.type = "button";
|
|
27499
|
+
fabBtn.className = "dsh-tw-fab";
|
|
27500
|
+
fabBtn.setAttribute("aria-label", "TiddlyWiki 知识库");
|
|
27501
|
+
const icon = document.createElement("span");
|
|
27502
|
+
icon.className = "dsh-tw-fab-icon";
|
|
27503
|
+
icon.innerHTML = BOOK_ICON;
|
|
27504
|
+
dot = document.createElement("span");
|
|
27505
|
+
dot.className = "dsh-tw-fab-dot";
|
|
27506
|
+
dot.dataset.state = sync.getState().state;
|
|
27507
|
+
fabBtn.append(icon, dot);
|
|
27508
|
+
fabBtn.addEventListener("click", () => {
|
|
27509
|
+
if (menu === void 0) return;
|
|
27510
|
+
if (menuOpen) {
|
|
27511
|
+
closeMenu();
|
|
27512
|
+
return;
|
|
27513
|
+
}
|
|
27514
|
+
menuOpen = true;
|
|
27515
|
+
menu.hidden = false;
|
|
27516
|
+
renderDot();
|
|
27517
|
+
refreshTwStatus();
|
|
27518
|
+
});
|
|
27519
|
+
root.append(menu, fabBtn);
|
|
27520
|
+
document.body.append(root);
|
|
27521
|
+
renderDot();
|
|
27522
|
+
document.addEventListener("click", (event) => {
|
|
27523
|
+
if (!menuOpen) return;
|
|
27524
|
+
const target = event.target;
|
|
27525
|
+
if (root !== void 0 && root.contains(target)) return;
|
|
27526
|
+
closeMenu();
|
|
27527
|
+
}, true);
|
|
27016
27528
|
};
|
|
27529
|
+
const unsubPanel = state.subscribe(() => {
|
|
27530
|
+
if (panelLabel !== void 0) panelLabel.textContent = state.isOpen() ? "收起 TW 面板" : "打开 TW 面板";
|
|
27531
|
+
});
|
|
27532
|
+
const unsubSync = sync.subscribe(renderDot);
|
|
27017
27533
|
(async () => {
|
|
27018
|
-
const
|
|
27019
|
-
if (disposed
|
|
27020
|
-
|
|
27021
|
-
|
|
27022
|
-
timer = window.setInterval(() => {
|
|
27023
|
-
poll();
|
|
27024
|
-
}, POLL_MS);
|
|
27534
|
+
const flags = await fetchUiFlags();
|
|
27535
|
+
if (disposed) return;
|
|
27536
|
+
if (!flags.showQuickNote && !flags.showPanelStatus && !flags.showSyncButton) return;
|
|
27537
|
+
build(flags);
|
|
27025
27538
|
})();
|
|
27026
27539
|
return () => {
|
|
27027
27540
|
disposed = true;
|
|
27028
|
-
|
|
27541
|
+
unsubPanel();
|
|
27542
|
+
unsubSync();
|
|
27029
27543
|
root?.remove();
|
|
27030
27544
|
};
|
|
27031
27545
|
}
|
|
@@ -27228,9 +27742,9 @@ function renderConfigSection(body, config, refresh) {
|
|
|
27228
27742
|
numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
|
|
27229
27743
|
textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
|
|
27230
27744
|
textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
|
|
27231
|
-
checkField("ui.showQuickNote", "
|
|
27232
|
-
checkField("ui.showPanelStatus", "
|
|
27233
|
-
checkField("ui.showSyncButton", "
|
|
27745
|
+
checkField("ui.showQuickNote", "显示「知识库」按钮里的「快速笔记」入口", ui.showQuickNote !== false);
|
|
27746
|
+
checkField("ui.showPanelStatus", "显示「知识库」按钮里的 TW 面板/重载入口与状态行", ui.showPanelStatus !== false);
|
|
27747
|
+
checkField("ui.showSyncButton", "显示「知识库」按钮里的「同步」入口与 git 状态点", ui.showSyncButton !== false);
|
|
27234
27748
|
const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
|
|
27235
27749
|
save.type = "button";
|
|
27236
27750
|
save.addEventListener("click", () => {
|
|
@@ -27464,10 +27978,13 @@ function apply(ctx) {
|
|
|
27464
27978
|
const state = new PanelState();
|
|
27465
27979
|
const disposers = [];
|
|
27466
27980
|
try {
|
|
27981
|
+
const note = createNoteWidget();
|
|
27982
|
+
const sync = createSyncController();
|
|
27983
|
+
disposers.push(() => note.dispose());
|
|
27984
|
+
disposers.push(() => sync.dispose());
|
|
27467
27985
|
disposers.push(mountSidebarEntry(state));
|
|
27468
27986
|
disposers.push(mountPanel(state));
|
|
27469
|
-
disposers.push(
|
|
27470
|
-
disposers.push(mountSyncButton());
|
|
27987
|
+
disposers.push(mountKnowledgeFab(state, note, sync));
|
|
27471
27988
|
disposers.push(disposeEditorPopup);
|
|
27472
27989
|
} catch (error) {
|
|
27473
27990
|
console.error("[dsh-tiddlywiki] mount failed:", error);
|