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