dsh-obsidian-notes 0.5.1 → 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 +41 -10
- package/README.zh.md +19 -5
- package/lib/client.js +1582 -31
- package/package.json +1 -1
- package/src/client.js +923 -17
- package/src/graph.js +348 -0
- package/src/index.js +234 -2
- package/src/markdown.js +102 -14
- package/src/mindmap.js +207 -0
package/src/client.js
CHANGED
|
@@ -112,6 +112,44 @@
|
|
|
112
112
|
cancel: "取消",
|
|
113
113
|
discard: "放弃未保存的修改?",
|
|
114
114
|
cite: "引用到对话",
|
|
115
|
+
mindMap: "思维导图",
|
|
116
|
+
preview: "预览",
|
|
117
|
+
mapEmpty: "这篇笔记还没有标题或列表,画不出思维导图",
|
|
118
|
+
mapHint: "滚轮缩放 · 拖动平移 · 单击节点选中 · 双击跳到源码行",
|
|
119
|
+
mapJump: "跳到源码行",
|
|
120
|
+
mapAsk: "把分支插入对话",
|
|
121
|
+
branchCited: "已把分支插入对话({n} 行)",
|
|
122
|
+
graph: "关系图谱",
|
|
123
|
+
graphHint: "滚轮缩放 · 拖动平移 · 点击笔记打开 · 悬停看邻居",
|
|
124
|
+
graphGlobal: "全库",
|
|
125
|
+
graphLocal: "局部",
|
|
126
|
+
graphLocalHint: "以当前打开的笔记为中心显示 2 跳邻居",
|
|
127
|
+
graphRefresh: "重新读取 vault",
|
|
128
|
+
graphRerun: "重新布局",
|
|
129
|
+
graphClose: "关闭图谱",
|
|
130
|
+
graphLoading: "正在构建关系图谱…",
|
|
131
|
+
graphEmpty: "vault 里还没有可显示的笔记或双链",
|
|
132
|
+
graphError: "关系图谱加载失败:{msg}",
|
|
133
|
+
graphStats: "{notes} 篇笔记 · {tags} 个标签 · {edges} 条连线",
|
|
134
|
+
graphTruncated: "笔记较多,图谱只画了最重要的 {n} 个节点",
|
|
135
|
+
graphNoNote: "先在侧边栏打开一篇笔记,才能看局部图谱",
|
|
136
|
+
graphOpen: "打开关系图谱",
|
|
137
|
+
graphMove: "拖动移动面板",
|
|
138
|
+
graphNeedsRestart: "关系图谱需要重启 DSH 后启用",
|
|
139
|
+
graphRestartHint: "当前 DSH 进程里还是旧版 host(缺少 /obsidian/graph 路由)。完全退出 DSH(Cmd+Q)再打开,然后点「重试」。",
|
|
140
|
+
graphErrorTitle: "关系图谱加载失败",
|
|
141
|
+
graphBuilding: "正在从笔记构建图谱({done}/{total})…",
|
|
142
|
+
graphFocus: "聚焦",
|
|
143
|
+
graphFocusHint: "一键切到以当前打开笔记为中心的局部图谱(局部模式下再点 = 重新居中)",
|
|
144
|
+
graphLegendNote: "笔记",
|
|
145
|
+
graphLegendTag: "标签",
|
|
146
|
+
graphLegendFocus: "当前笔记",
|
|
147
|
+
graphLegendLink: "链接",
|
|
148
|
+
graphDepth: "跳数",
|
|
149
|
+
graphDepthHint: "局部图谱跳数,1–5 跳(与 Obsidian 局部图谱的 depth 范围一致)",
|
|
150
|
+
graphRetry: "重试",
|
|
151
|
+
graphDetails: "详细信息",
|
|
152
|
+
graphDetailsHide: "收起详细信息",
|
|
115
153
|
cited: "已引用到对话",
|
|
116
154
|
copied: "已复制引用,可粘贴到输入框",
|
|
117
155
|
noInput: "未找到对话输入框,引用已复制到剪贴板",
|
|
@@ -249,6 +287,44 @@
|
|
|
249
287
|
cancel: "Cancel",
|
|
250
288
|
discard: "Discard unsaved changes?",
|
|
251
289
|
cite: "Cite to chat",
|
|
290
|
+
mindMap: "Mind map",
|
|
291
|
+
preview: "Preview",
|
|
292
|
+
mapEmpty: "No headings or list items yet — nothing to draw",
|
|
293
|
+
mapHint: "Scroll to zoom · drag to pan · click a node to select · double-click to jump to its line",
|
|
294
|
+
mapJump: "Jump to line",
|
|
295
|
+
mapAsk: "Insert branch into chat",
|
|
296
|
+
branchCited: "Branch inserted into the chat ({n} lines)",
|
|
297
|
+
graph: "Graph view",
|
|
298
|
+
graphHint: "Scroll to zoom · drag to pan · click a note to open it · hover to see neighbours",
|
|
299
|
+
graphGlobal: "Vault",
|
|
300
|
+
graphLocal: "Local",
|
|
301
|
+
graphLocalHint: "Show the 2-hop neighbourhood of the note that is open",
|
|
302
|
+
graphRefresh: "Re-read the vault",
|
|
303
|
+
graphRerun: "Re-run the layout",
|
|
304
|
+
graphClose: "Close the graph",
|
|
305
|
+
graphLoading: "Building the graph…",
|
|
306
|
+
graphEmpty: "No notes or links to draw yet",
|
|
307
|
+
graphError: "Graph view failed: {msg}",
|
|
308
|
+
graphStats: "{notes} notes · {tags} tags · {edges} links",
|
|
309
|
+
graphTruncated: "Large vault: only the {n} most connected nodes are drawn",
|
|
310
|
+
graphNoNote: "Open a note in the sidebar first to see the local graph",
|
|
311
|
+
graphOpen: "Open the graph view",
|
|
312
|
+
graphMove: "Drag to move the panel",
|
|
313
|
+
graphNeedsRestart: "The graph view needs a DSH restart",
|
|
314
|
+
graphRestartHint: "The running DSH host still predates the /obsidian/graph route. Quit DSH completely (Cmd+Q), reopen it, then press Retry.",
|
|
315
|
+
graphErrorTitle: "The graph view failed to load",
|
|
316
|
+
graphBuilding: "Building the graph from notes ({done}/{total})…",
|
|
317
|
+
graphFocus: "Focus",
|
|
318
|
+
graphFocusHint: "Jump to the local graph centred on the open note (click again to re-centre)",
|
|
319
|
+
graphLegendNote: "note",
|
|
320
|
+
graphLegendTag: "tag",
|
|
321
|
+
graphLegendFocus: "current note",
|
|
322
|
+
graphLegendLink: "link",
|
|
323
|
+
graphDepth: "Depth",
|
|
324
|
+
graphDepthHint: "Local graph depth, 1–5 hops (same range as Obsidian's local graph depth)",
|
|
325
|
+
graphRetry: "Retry",
|
|
326
|
+
graphDetails: "Details",
|
|
327
|
+
graphDetailsHide: "Hide details",
|
|
252
328
|
cited: "Cited to chat",
|
|
253
329
|
copied: "Reference copied — paste it into the input",
|
|
254
330
|
noInput: "No chat input found — reference copied to clipboard",
|
|
@@ -299,6 +375,8 @@
|
|
|
299
375
|
// ── 共享外部 store(跨槽位:侧边栏 / Dock / Toast)──────────────────────────
|
|
300
376
|
const LS_NOTES_OPEN = "dsh-obsidian:notesOpen";
|
|
301
377
|
const LS_DOCK_WIDTH = "dsh-obsidian:dockWidth";
|
|
378
|
+
const LS_GRAPH_GEOMETRY = "dsh-obsidian:graphGeometry";
|
|
379
|
+
const LS_LOCAL_DEPTH = "dsh-obsidian:localDepth";
|
|
302
380
|
|
|
303
381
|
function readNotesOpen() {
|
|
304
382
|
try {
|
|
@@ -308,7 +386,7 @@
|
|
|
308
386
|
}
|
|
309
387
|
}
|
|
310
388
|
|
|
311
|
-
let shared = { notesOpen: readNotesOpen(), openNote: null, vaultRev: 0, toast: null, noteCount: 0, prompt: null };
|
|
389
|
+
let shared = { notesOpen: readNotesOpen(), openNote: null, graphOpen: false, vaultRev: 0, toast: null, noteCount: 0, prompt: null };
|
|
312
390
|
const sharedListeners = new Set();
|
|
313
391
|
|
|
314
392
|
function patchShared(patch) {
|
|
@@ -750,7 +828,69 @@
|
|
|
750
828
|
`.dsh-obs-md-lc{flex:1;min-width:0}`,
|
|
751
829
|
`.dsh-obs-dock-body .dsh-obs-md-row .dsh-obs-md-lc p,.dsh-obs-dock-body .dsh-obs-md-row .dsh-obs-md-lc h1,.dsh-obs-dock-body .dsh-obs-md-row .dsh-obs-md-lc h2,.dsh-obs-dock-body .dsh-obs-md-row .dsh-obs-md-lc h3,.dsh-obs-dock-body .dsh-obs-md-row .dsh-obs-md-lc h4,.dsh-obs-dock-body .dsh-obs-md-row .dsh-obs-md-lc h5,.dsh-obs-dock-body .dsh-obs-md-row .dsh-obs-md-lc h6,.dsh-obs-dock-body .dsh-obs-md-row .dsh-obs-md-lc blockquote{margin:0;padding:0}`,
|
|
752
830
|
`.dsh-obs-md-row.dsh-obs-md-heading .dsh-obs-md-lc{font-weight:600}`,
|
|
831
|
+
// 行内 Obsidian 语法:==高亮== / ~~删除线~~ / 裸链接与自动链接
|
|
832
|
+
`.dsh-obs-md mark,.dsh-obs-mark{background:rgba(255,208,0,.32);color:inherit;border-radius:2px;padding:0 1px}`,
|
|
833
|
+
`.dsh-obs-md del{opacity:.62;text-decoration:line-through}`,
|
|
834
|
+
`.dsh-obs-md .dsh-obs-autolink{color:var(--dsw-alias-brand-primary);text-decoration:underline;text-underline-offset:2px;word-break:break-all}`,
|
|
753
835
|
`.dsh-obs-md-row.dsh-obs-md-quote .dsh-obs-md-lc{border-left:3px solid var(--dsw-alias-brand-primary);padding-left:10px;color:var(--dsw-alias-label-secondary)}`,
|
|
836
|
+
// ── 思维导图视图(B1)──
|
|
837
|
+
`.dsh-obs-dock-map{overflow:hidden;padding:0}`,
|
|
838
|
+
`.dsh-obs-map{position:relative;display:block;width:100%;height:100%;min-height:300px;cursor:grab;overflow:hidden;user-select:none;-webkit-user-select:none}`,
|
|
839
|
+
`.dsh-obs-map-svg{display:block;width:100%;height:100%}`,
|
|
840
|
+
`.dsh-obs-map-edge{fill:none;stroke:var(--dsw-alias-border-l2);stroke-width:1.5}`,
|
|
841
|
+
`.dsh-obs-map-node{cursor:pointer}`,
|
|
842
|
+
`.dsh-obs-map-node rect{fill:var(--dsw-alias-bg-overlay);stroke:var(--dsw-alias-border-l2);stroke-width:1}`,
|
|
843
|
+
`.dsh-obs-map-heading rect{stroke:var(--dsw-alias-brand-primary)}`,
|
|
844
|
+
`.dsh-obs-map-root rect{fill:var(--dsw-alias-brand-primary);stroke:var(--dsw-alias-brand-primary)}`,
|
|
845
|
+
`.dsh-obs-map-root text{fill:#fff;font-weight:600}`,
|
|
846
|
+
`.dsh-obs-map-node text{font-size:12.5px;fill:var(--dsw-alias-label-primary);pointer-events:none}`,
|
|
847
|
+
`.dsh-obs-map-sel rect{stroke:var(--dsw-alias-brand-primary);stroke-width:2}`,
|
|
848
|
+
`.dsh-obs-map-hint{position:absolute;left:10px;bottom:8px;font-size:11px;color:var(--dsw-alias-label-tertiary);pointer-events:none}`,
|
|
849
|
+
`.dsh-obs-map-actions{position:absolute;right:10px;top:8px;display:flex;align-items:center;gap:6px;padding:4px 6px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-overlay);font-size:12px}`,
|
|
850
|
+
`.dsh-obs-map-actions .dsh-obs-map-line{color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}`,
|
|
851
|
+
`.dsh-obs-map-actions button{padding:3px 8px;font-size:12px}`,
|
|
852
|
+
`.dsh-obs-map-empty{display:flex;align-items:center;justify-content:center;height:100%;min-height:220px;padding:24px;color:var(--dsw-alias-label-tertiary);text-align:center}`,
|
|
853
|
+
`.dsh-obs-md-row.dsh-obs-md-flash{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 16%,transparent);border-radius:4px}`,
|
|
854
|
+
// ── 关系图谱(shell.overlay,vault 级独立视图)──
|
|
855
|
+
`.dsh-obs-graph{position:fixed;z-index:40;display:flex;flex-direction:column;box-sizing:border-box;min-width:560px;min-height:360px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-overlay);box-shadow:0 24px 64px rgba(0,0,0,.45);overflow:hidden;color:var(--dsw-alias-label-primary)}`,
|
|
856
|
+
`.dsh-obs-graph-head{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l2);flex-wrap:wrap;cursor:move;user-select:none;-webkit-user-select:none}`,
|
|
857
|
+
`.dsh-obs-graph-resize{position:absolute;right:2px;bottom:2px;width:16px;height:16px;cursor:nwse-resize;border-right:2px solid var(--dsw-alias-border-l2);border-bottom:2px solid var(--dsw-alias-border-l2);opacity:.75}`,
|
|
858
|
+
`.dsh-obs-graph-error{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;gap:8px;padding:32px;text-align:center}`,
|
|
859
|
+
`.dsh-obs-graph-error-icon{font-size:26px}`,
|
|
860
|
+
`.dsh-obs-graph-error-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}`,
|
|
861
|
+
`.dsh-obs-graph-error-text{font-size:12px;line-height:1.7;color:var(--dsw-alias-label-secondary);max-width:560px}`,
|
|
862
|
+
`.dsh-obs-graph-error-actions{display:flex;gap:8px;margin-top:8px}`,
|
|
863
|
+
`.dsh-obs-graph-error-actions button{padding:5px 12px;font-size:12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer}`,
|
|
864
|
+
`.dsh-obs-graph-error-actions button:hover{background:var(--dsw-alias-interactive-bg-hover)}`,
|
|
865
|
+
`.dsh-obs-graph-error-actions button.dsh-obs-primary{background:var(--dsw-alias-button-primary-fill);border-color:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}`,
|
|
866
|
+
`.dsh-obs-graph-error-details{max-width:560px;max-height:120px;overflow:auto;margin-top:6px;padding:8px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-all;text-align:left}`,
|
|
867
|
+
`.dsh-obs-graph-title{font-weight:600;font-size:14px}`,
|
|
868
|
+
`.dsh-obs-graph-stats{font-size:11px;color:var(--dsw-alias-label-tertiary)}`,
|
|
869
|
+
`.dsh-obs-graph-modes{display:flex;gap:4px;margin-left:auto}`,
|
|
870
|
+
`.dsh-obs-graph-modes button[disabled]{opacity:.45;cursor:not-allowed}`,
|
|
871
|
+
`.dsh-obs-graph-depth{display:inline-flex;align-items:center;gap:2px;margin-left:6px;padding-left:8px;border-left:1px solid var(--dsw-alias-border-l2)}`,
|
|
872
|
+
`.dsh-obs-graph-depth-label{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-right:2px}`,
|
|
873
|
+
`.dsh-obs-graph-depth button{min-width:20px;padding:0 4px}`,
|
|
874
|
+
`.dsh-obs-graph-head button{white-space:nowrap;flex:none}`,
|
|
875
|
+
`.dsh-obs-legend-dot{width:9px;height:9px;border-radius:50%;border:1.5px solid var(--dsw-alias-label-secondary);display:inline-block;box-sizing:border-box}`,
|
|
876
|
+
`.dsh-obs-legend-tag{width:8px;height:8px;border-radius:2px;background:var(--dsw-alias-brand-primary);display:inline-block}`,
|
|
877
|
+
`.dsh-obs-legend-focus{width:9px;height:9px;border-radius:50%;border:2px solid var(--dsw-alias-state-error-primary);display:inline-block;box-sizing:border-box}`,
|
|
878
|
+
`.dsh-obs-legend-link{width:14px;height:1.5px;background:var(--dsw-alias-border-l2);display:inline-block}`,
|
|
879
|
+
`.dsh-obs-graph-canvas{position:relative;flex:1;min-height:0;overflow:hidden;cursor:grab;user-select:none;-webkit-user-select:none}`,
|
|
880
|
+
`.dsh-obs-graph-svg{display:block;width:100%;height:100%}`,
|
|
881
|
+
`.dsh-obs-graph-edge{stroke:var(--dsw-alias-border-l2);stroke-width:1;opacity:.55}`,
|
|
882
|
+
`.dsh-obs-graph-edge-on{stroke:var(--dsw-alias-brand-primary);opacity:.9;stroke-width:1.6}`,
|
|
883
|
+
`.dsh-obs-graph-node{cursor:pointer}`,
|
|
884
|
+
`.dsh-obs-graph-note circle{fill:var(--dsw-alias-bg-overlay);stroke:var(--dsw-alias-label-secondary);stroke-width:1.4}`,
|
|
885
|
+
`.dsh-obs-graph-tag rect{fill:var(--dsw-alias-brand-primary);opacity:.75}`,
|
|
886
|
+
`.dsh-obs-graph-sel circle,.dsh-obs-graph-sel rect{stroke:var(--dsw-alias-brand-primary);stroke-width:2.2}`,
|
|
887
|
+
`.dsh-obs-graph-focus circle{stroke:var(--dsw-alias-state-error-primary);stroke-width:2.4}`,
|
|
888
|
+
`.dsh-obs-graph-dim{opacity:.25}`,
|
|
889
|
+
`.dsh-obs-graph-node text{font-size:11px;fill:var(--dsw-alias-label-primary);pointer-events:none}`,
|
|
890
|
+
`.dsh-obs-graph-tag text{fill:var(--dsw-alias-label-secondary)}`,
|
|
891
|
+
`.dsh-obs-graph-hint{padding:6px 14px;border-top:1px solid var(--dsw-alias-border-l2);font-size:11px;color:var(--dsw-alias-label-tertiary)}`,
|
|
892
|
+
`.dsh-obs-graph-empty{display:flex;align-items:center;justify-content:center;flex:1;padding:32px;color:var(--dsw-alias-label-tertiary);text-align:center}`,
|
|
893
|
+
`.dsh-obs-graph-error, .dsh-obs-graph-empty{flex:1;min-height:0}`,
|
|
754
894
|
// ── Obsidian callout(`> [!type] 标题`)配色:类型在根节点上带 data-callout ──
|
|
755
895
|
`[data-callout="note"]{--dsh-obs-callout-color:#448aff}`,
|
|
756
896
|
`[data-callout="abstract"]{--dsh-obs-callout-color:#00b8d4}`,
|
|
@@ -946,6 +1086,38 @@
|
|
|
946
1086
|
return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}`;
|
|
947
1087
|
}
|
|
948
1088
|
|
|
1089
|
+
/**
|
|
1090
|
+
* 会话行次序:与 DSH 内置侧边栏的**默认**排序(`orderBy: 'updated'`,
|
|
1091
|
+
* 「最近更新」)一致。
|
|
1092
|
+
*
|
|
1093
|
+
* 不能直接用 Workspace 的 `sessionIds` 次序:那是**挂载次序**(host 侧
|
|
1094
|
+
* `attachSession` 把新会话 prepend 到数组头),一个很久以前创建、现在仍在
|
|
1095
|
+
* 用的会话会停在旧位置(表现为「当前会话排在倒数第二」)。DSH 默认视图
|
|
1096
|
+
* 的做法是:按会话的 `updatedAt` 降序;时间相同时按 id 升序 —— 官方
|
|
1097
|
+
* `compareSessionRecency` 就是这两个比较,id 作稳定 tie-break 是为了同一份
|
|
1098
|
+
* 数据每次渲染次序稳定,不随 Map 迭代顺序抖动。
|
|
1099
|
+
* @param a - 会话 id。
|
|
1100
|
+
* @param b - 会话 id。
|
|
1101
|
+
* @param byId - 会话 id → 会话摘要(`updatedAt` 为 epoch ms)。
|
|
1102
|
+
* @returns 排序比较值(负 = a 更靠前)。
|
|
1103
|
+
*/
|
|
1104
|
+
function compareSessionRecency(a, b, byId) {
|
|
1105
|
+
const ta = byId[a]?.updatedAt ?? Number.NEGATIVE_INFINITY;
|
|
1106
|
+
const tb = byId[b]?.updatedAt ?? Number.NEGATIVE_INFINITY;
|
|
1107
|
+
if (ta !== tb) return tb - ta;
|
|
1108
|
+
return a < b ? -1 : 1;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* 按「最近更新」排会话 id 列表(不修改入参)。
|
|
1113
|
+
* @param sessionIds - 待排的会话 id。
|
|
1114
|
+
* @param byId - 会话 id → 会话摘要。
|
|
1115
|
+
* @returns 新的已排序数组。
|
|
1116
|
+
*/
|
|
1117
|
+
function sortedSessionIds(sessionIds, byId) {
|
|
1118
|
+
return [...sessionIds].sort((a, b) => compareSessionRecency(a, b, byId));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
949
1121
|
/**
|
|
950
1122
|
* 一个会话及其全部子代理后代(闭包,含自身)。
|
|
951
1123
|
* 删父会话必须连子代理日志一起删:父级日志消失后,子会话会变成无法归属的
|
|
@@ -1038,6 +1210,7 @@
|
|
|
1038
1210
|
// `![[附件/a.png]]` 会被当成 Vault 根下的 `附件/a.png` → 403 → 只剩断图。
|
|
1039
1211
|
assetUrl: (rel, from) => `/obsidian/file?path=${encodeURIComponent(rel)}`
|
|
1040
1212
|
+ (from ? `&from=${encodeURIComponent(from)}` : ""),
|
|
1213
|
+
graph: () => api("/graph"),
|
|
1041
1214
|
note: (rel) => api(`/note?path=${encodeURIComponent(rel)}`),
|
|
1042
1215
|
save: (rel, content) => api("/note", {
|
|
1043
1216
|
method: "POST",
|
|
@@ -1479,6 +1652,14 @@
|
|
|
1479
1652
|
onClick: (e) => { e.stopPropagation(); void newNote(""); },
|
|
1480
1653
|
}, "+"),
|
|
1481
1654
|
),
|
|
1655
|
+
rootInfo && rootInfo.exists && createElement("div", { className: "dsh-obs-root-row", title: t("graphOpen") },
|
|
1656
|
+
createElement("span", { className: "dsh-obs-root-path" }, `🕸 ${t("graph")}`),
|
|
1657
|
+
createElement("button", {
|
|
1658
|
+
className: "dsh-obs-mini",
|
|
1659
|
+
title: t("graphOpen"),
|
|
1660
|
+
onClick: (e) => { e.stopPropagation(); patchShared({ graphOpen: true }); },
|
|
1661
|
+
}, "↗"),
|
|
1662
|
+
),
|
|
1482
1663
|
rootInfo && rootInfo.exists && attachDir !== null && createElement("div", { className: "dsh-obs-root-row", title: t("attachSettings") },
|
|
1483
1664
|
createElement("span", { className: "dsh-obs-root-path" },
|
|
1484
1665
|
`📎 ${t("attachLocation")}: ${attachLocationLabel(attachCfg, attachDir, t)}`),
|
|
@@ -1621,7 +1802,9 @@
|
|
|
1621
1802
|
return true;
|
|
1622
1803
|
};
|
|
1623
1804
|
|
|
1624
|
-
// 分组:工作区 sessionIds 账户为主,cwd === path
|
|
1805
|
+
// 分组:工作区 sessionIds 账户为主,cwd === path 兜底;其余入「未分组」。
|
|
1806
|
+
// 组内次序**不**沿用 Workspace 的 sessionIds:那是挂载次序(新会话 prepend),
|
|
1807
|
+
// 会让人气正旺的旧会话沉底。统一按「最近更新」排,与 DSH 内置侧边栏默认一致。
|
|
1625
1808
|
const groups = useMemo(() => {
|
|
1626
1809
|
const accounted = new Set();
|
|
1627
1810
|
const list = [];
|
|
@@ -1640,11 +1823,12 @@
|
|
|
1640
1823
|
accounted.add(sid);
|
|
1641
1824
|
}
|
|
1642
1825
|
}
|
|
1643
|
-
list.push({ key: w.workspaceId, workspace: w, sessionIds: wsIds });
|
|
1826
|
+
list.push({ key: w.workspaceId, workspace: w, sessionIds: sortedSessionIds(wsIds, byId) });
|
|
1644
1827
|
}
|
|
1645
|
-
const ungrouped =
|
|
1646
|
-
.filter((sid) => !accounted.has(sid) && visibleSession(sid))
|
|
1647
|
-
|
|
1828
|
+
const ungrouped = sortedSessionIds(
|
|
1829
|
+
ids.filter((sid) => !accounted.has(sid) && visibleSession(sid)),
|
|
1830
|
+
byId,
|
|
1831
|
+
);
|
|
1648
1832
|
return { list, ungrouped };
|
|
1649
1833
|
}, [items, ids, byId, archived, current, purged]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
1650
1834
|
|
|
@@ -1699,15 +1883,13 @@
|
|
|
1699
1883
|
const q = query.trim().toLowerCase();
|
|
1700
1884
|
const matched = useMemo(() => {
|
|
1701
1885
|
if (q === "") return [];
|
|
1702
|
-
return ids
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
})
|
|
1710
|
-
.sort((a, b) => (byId[b].updatedAt ?? 0) - (byId[a].updatedAt ?? 0));
|
|
1886
|
+
return sortedSessionIds(ids.filter((sid) => {
|
|
1887
|
+
if (!visibleSession(sid)) return false;
|
|
1888
|
+
const s = byId[sid];
|
|
1889
|
+
const title = (s.title ?? "").toLowerCase();
|
|
1890
|
+
const display = (s.displayTitle ?? "").toLowerCase();
|
|
1891
|
+
return title.includes(q) || display.includes(q);
|
|
1892
|
+
}), byId);
|
|
1711
1893
|
}, [ids, byId, archived, q, current, purged]);
|
|
1712
1894
|
|
|
1713
1895
|
// ── 批量操作 ──────────────────────────────────────────────────────────────
|
|
@@ -2550,12 +2732,663 @@
|
|
|
2550
2732
|
);
|
|
2551
2733
|
}
|
|
2552
2734
|
|
|
2735
|
+
/**
|
|
2736
|
+
* 关系图谱视图:全库 .md + [[双链]] + 标签(host `/obsidian/graph` 计算,
|
|
2737
|
+
* 力导向布局在 src/graph.js)。可切到「局部」= 当前打开笔记的 2 跳邻居。
|
|
2738
|
+
*/
|
|
2739
|
+
function GraphView({ t, currentNote, onOpenNote, onClose }) {
|
|
2740
|
+
const [data, setData] = useState(null);
|
|
2741
|
+
const [error, setError] = useState("");
|
|
2742
|
+
const [loading, setLoading] = useState(true);
|
|
2743
|
+
const [mode, setMode] = useState("global");
|
|
2744
|
+
// 局部图谱跳数:与 Obsidian 局部图谱的 depth 滑杆对齐(范围 1–5,默认 1)
|
|
2745
|
+
const [fitKey, setFitKey] = useState(0);
|
|
2746
|
+
const [localDepth, setLocalDepth] = useState(() => {
|
|
2747
|
+
try {
|
|
2748
|
+
const v = parseInt(localStorage.getItem(LS_LOCAL_DEPTH) ?? "", 10);
|
|
2749
|
+
if (Number.isFinite(v) && v >= 1 && v <= 5) return v;
|
|
2750
|
+
} catch { /* ignore */ }
|
|
2751
|
+
return 1;
|
|
2752
|
+
});
|
|
2753
|
+
const [seed, setSeed] = useState(1);
|
|
2754
|
+
const [hovered, setHovered] = useState(null);
|
|
2755
|
+
const [reloadKey, setReloadKey] = useState(0);
|
|
2756
|
+
const [transform, setTransform] = useState({ x: 0, y: 0, s: 1 });
|
|
2757
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
2758
|
+
const [buildProgress, setBuildProgress] = useState(null);
|
|
2759
|
+
const dragRef = useRef(null);
|
|
2760
|
+
const wrapRef = useRef(null);
|
|
2761
|
+
const panelDragRef = useRef(null);
|
|
2762
|
+
const resizeRef = useRef(null);
|
|
2763
|
+
// 面板几何:可拖动、可缩放、可关闭(和笔记 Dock 同一套交互),位置/尺寸持久化
|
|
2764
|
+
const [geometry, setGeometry] = useState(() => {
|
|
2765
|
+
const fallback = { x: null, y: 64, width: 860, height: 600 };
|
|
2766
|
+
try {
|
|
2767
|
+
const raw = localStorage.getItem(LS_GRAPH_GEOMETRY);
|
|
2768
|
+
if (raw !== null) {
|
|
2769
|
+
const parsed = JSON.parse(raw);
|
|
2770
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2771
|
+
return {
|
|
2772
|
+
x: typeof parsed.x === "number" ? parsed.x : null,
|
|
2773
|
+
y: typeof parsed.y === "number" ? parsed.y : 64,
|
|
2774
|
+
width: Number.isFinite(parsed.width) ? Math.min(Math.max(560, parsed.width), 1600) : 860,
|
|
2775
|
+
height: Number.isFinite(parsed.height) ? Math.min(Math.max(360, parsed.height), 1200) : 600,
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
} catch { /* 忽略坏掉的本地存储 */ }
|
|
2780
|
+
return fallback;
|
|
2781
|
+
});
|
|
2782
|
+
|
|
2783
|
+
useEffect(() => {
|
|
2784
|
+
let cancelled = false;
|
|
2785
|
+
setLoading(true);
|
|
2786
|
+
setError("");
|
|
2787
|
+
setBuildProgress(null);
|
|
2788
|
+
// 旧 host 没有 /obsidian/graph 路由时的兜底:用已有的 tree/note 接口在浏览器里
|
|
2789
|
+
// 自建同一张图(插件升级后页面刷新即可用,不要求用户重启 DSH)。
|
|
2790
|
+
const buildFromClient = async () => {
|
|
2791
|
+
const dirs = [""];
|
|
2792
|
+
const notes = [];
|
|
2793
|
+
while (dirs.length > 0 && notes.length < 1200) {
|
|
2794
|
+
const rel = dirs.shift();
|
|
2795
|
+
let res;
|
|
2796
|
+
try {
|
|
2797
|
+
res = await treeApi.tree(rel);
|
|
2798
|
+
} catch {
|
|
2799
|
+
continue;
|
|
2800
|
+
}
|
|
2801
|
+
for (const entry of res.entries ?? []) {
|
|
2802
|
+
const path = rel === "" ? entry.name : `${rel}/${entry.name}`;
|
|
2803
|
+
if (entry.type === "dir") dirs.push(path);
|
|
2804
|
+
else if (entry.type === "note") notes.push({ path });
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
const collected = new Array(notes.length).fill(null);
|
|
2808
|
+
let done = 0;
|
|
2809
|
+
let cursor = 0;
|
|
2810
|
+
setBuildProgress({ done, total: notes.length });
|
|
2811
|
+
const worker = async () => {
|
|
2812
|
+
while (cursor < notes.length && !cancelled) {
|
|
2813
|
+
const index = cursor;
|
|
2814
|
+
cursor += 1;
|
|
2815
|
+
try {
|
|
2816
|
+
const note = await treeApi.note(notes[index].path);
|
|
2817
|
+
collected[index] = {
|
|
2818
|
+
path: notes[index].path,
|
|
2819
|
+
links: Array.isArray(note.links) ? note.links : [],
|
|
2820
|
+
tags: graphModule.extractTags(note.content ?? "", note.frontmatter),
|
|
2821
|
+
};
|
|
2822
|
+
} catch { /* 单篇读失败不影响整张图 */ }
|
|
2823
|
+
done += 1;
|
|
2824
|
+
if ((done % 8 === 0 || done === notes.length) && !cancelled) setBuildProgress({ done, total: notes.length });
|
|
2825
|
+
}
|
|
2826
|
+
};
|
|
2827
|
+
await Promise.all(Array.from({ length: Math.min(6, Math.max(1, notes.length)) }, worker));
|
|
2828
|
+
if (cancelled) return null;
|
|
2829
|
+
return graphModule.buildGraphFromNotes(collected.filter(Boolean));
|
|
2830
|
+
};
|
|
2831
|
+
treeApi.root().then((root) => {
|
|
2832
|
+
if (cancelled) return null;
|
|
2833
|
+
const features = root && Array.isArray(root.features) ? root.features : [];
|
|
2834
|
+
if (!features.includes("graph")) return buildFromClient();
|
|
2835
|
+
return treeApi.graph();
|
|
2836
|
+
}).then((res) => {
|
|
2837
|
+
if (cancelled || res === null || res === undefined) return;
|
|
2838
|
+
setData(res);
|
|
2839
|
+
setLoading(false);
|
|
2840
|
+
setBuildProgress(null);
|
|
2841
|
+
}).catch((err) => {
|
|
2842
|
+
if (cancelled) return;
|
|
2843
|
+
setError(String(err && err.message ? err.message : err));
|
|
2844
|
+
setLoading(false);
|
|
2845
|
+
});
|
|
2846
|
+
return () => { cancelled = true; };
|
|
2847
|
+
}, [reloadKey]);
|
|
2848
|
+
|
|
2849
|
+
// 位置/尺寸持久化
|
|
2850
|
+
useEffect(() => {
|
|
2851
|
+
try { localStorage.setItem(LS_GRAPH_GEOMETRY, JSON.stringify(geometry)); } catch { /* ignore */ }
|
|
2852
|
+
}, [geometry]);
|
|
2853
|
+
|
|
2854
|
+
useEffect(() => {
|
|
2855
|
+
try { localStorage.setItem(LS_LOCAL_DEPTH, String(localDepth)); } catch { /* ignore */ }
|
|
2856
|
+
}, [localDepth]);
|
|
2857
|
+
|
|
2858
|
+
// 拖动标题栏移动面板(按钮上不触发)
|
|
2859
|
+
const beginPanelDrag = useCallback((e) => {
|
|
2860
|
+
if (e.target && e.target.closest && e.target.closest("button")) return;
|
|
2861
|
+
e.preventDefault();
|
|
2862
|
+
const el = e.currentTarget && e.currentTarget.parentElement;
|
|
2863
|
+
const base = { x: geometry.x !== null ? geometry.x : (el && typeof el.getBoundingClientRect === "function" ? el.getBoundingClientRect().left : 80), y: geometry.y };
|
|
2864
|
+
panelDragRef.current = { startX: e.clientX, startY: e.clientY, base };
|
|
2865
|
+
const onMove = (ev) => {
|
|
2866
|
+
const st = panelDragRef.current;
|
|
2867
|
+
if (!st) return;
|
|
2868
|
+
setGeometry((prev) => ({
|
|
2869
|
+
...prev,
|
|
2870
|
+
x: Math.min(Math.max(0, st.base.x + ev.clientX - st.startX), Math.max(0, window.innerWidth - 240)),
|
|
2871
|
+
y: Math.min(Math.max(8, st.base.y + ev.clientY - st.startY), Math.max(8, window.innerHeight - 120)),
|
|
2872
|
+
}));
|
|
2873
|
+
};
|
|
2874
|
+
const onUp = () => {
|
|
2875
|
+
panelDragRef.current = null;
|
|
2876
|
+
window.removeEventListener("mousemove", onMove);
|
|
2877
|
+
window.removeEventListener("mouseup", onUp);
|
|
2878
|
+
};
|
|
2879
|
+
window.addEventListener("mousemove", onMove);
|
|
2880
|
+
window.addEventListener("mouseup", onUp);
|
|
2881
|
+
}, [geometry.x, geometry.y]);
|
|
2882
|
+
|
|
2883
|
+
// 右下角缩放面板
|
|
2884
|
+
const beginPanelResize = useCallback((e) => {
|
|
2885
|
+
e.preventDefault();
|
|
2886
|
+
e.stopPropagation();
|
|
2887
|
+
resizeRef.current = { startX: e.clientX, startY: e.clientY, baseW: geometry.width, baseH: geometry.height };
|
|
2888
|
+
const onMove = (ev) => {
|
|
2889
|
+
const st = resizeRef.current;
|
|
2890
|
+
if (!st) return;
|
|
2891
|
+
setGeometry((prev) => ({
|
|
2892
|
+
...prev,
|
|
2893
|
+
width: Math.min(Math.max(560, st.baseW + ev.clientX - st.startX), Math.round(window.innerWidth * 0.96)),
|
|
2894
|
+
height: Math.min(Math.max(360, st.baseH + ev.clientY - st.startY), Math.round(window.innerHeight * 0.94)),
|
|
2895
|
+
}));
|
|
2896
|
+
};
|
|
2897
|
+
const onUp = () => {
|
|
2898
|
+
resizeRef.current = null;
|
|
2899
|
+
window.removeEventListener("mousemove", onMove);
|
|
2900
|
+
window.removeEventListener("mouseup", onUp);
|
|
2901
|
+
};
|
|
2902
|
+
window.addEventListener("mousemove", onMove);
|
|
2903
|
+
window.addEventListener("mouseup", onUp);
|
|
2904
|
+
}, [geometry.width, geometry.height]);
|
|
2905
|
+
|
|
2906
|
+
const currentRel = currentNote && currentNote.rel ? currentNote.rel : null;
|
|
2907
|
+
const focusId = currentRel !== null ? `note:${currentRel}` : null;
|
|
2908
|
+
const localAvailable = data !== null && focusId !== null && data.nodes.some((node) => node.id === focusId);
|
|
2909
|
+
const shown = useMemo(() => {
|
|
2910
|
+
if (data === null) return { nodes: [], edges: [] };
|
|
2911
|
+
if (mode === "local" && localAvailable) {
|
|
2912
|
+
const sub = graphModule.localSubgraph(data, focusId, localDepth);
|
|
2913
|
+
return { nodes: sub.nodes, edges: sub.edges };
|
|
2914
|
+
}
|
|
2915
|
+
return { nodes: data.nodes, edges: data.edges };
|
|
2916
|
+
}, [data, mode, localAvailable, focusId, localDepth]);
|
|
2917
|
+
const plan = useMemo(
|
|
2918
|
+
() => graphModule.layoutGraph(shown.nodes, shown.edges, { seed, maxNodes: 420 }),
|
|
2919
|
+
[shown, seed],
|
|
2920
|
+
);
|
|
2921
|
+
|
|
2922
|
+
// 打开 / 切换范围 / 重新布局后把整张图适配进画布
|
|
2923
|
+
useEffect(() => {
|
|
2924
|
+
const el = wrapRef.current;
|
|
2925
|
+
if (!el || plan.nodes.length === 0) return;
|
|
2926
|
+
const w = el.clientWidth || 900;
|
|
2927
|
+
const h = el.clientHeight || 560;
|
|
2928
|
+
const fit = Math.min((w - 30) / plan.width, (h - 30) / plan.height, 1);
|
|
2929
|
+
setTransform({ x: (w - plan.width * fit) / 2, y: (h - plan.height * fit) / 2, s: fit });
|
|
2930
|
+
}, [plan, fitKey]);
|
|
2931
|
+
|
|
2932
|
+
const onWheel = useCallback((e) => {
|
|
2933
|
+
if (plan.nodes.length === 0) return;
|
|
2934
|
+
if (e.cancelable !== false && e.preventDefault) e.preventDefault();
|
|
2935
|
+
const rect = e.currentTarget && e.currentTarget.getBoundingClientRect
|
|
2936
|
+
? e.currentTarget.getBoundingClientRect()
|
|
2937
|
+
: { left: 0, top: 0 };
|
|
2938
|
+
const px = (e.clientX || 0) - rect.left;
|
|
2939
|
+
const py = (e.clientY || 0) - rect.top;
|
|
2940
|
+
setTransform((prev) => {
|
|
2941
|
+
const next = Math.min(4, Math.max(0.18, prev.s * (e.deltaY > 0 ? 0.9 : 1.11)));
|
|
2942
|
+
const k = next / prev.s;
|
|
2943
|
+
return { s: next, x: px - (px - prev.x) * k, y: py - (py - prev.y) * k };
|
|
2944
|
+
});
|
|
2945
|
+
}, [plan.nodes.length]);
|
|
2946
|
+
const onMouseDown = useCallback((e) => {
|
|
2947
|
+
if (e.button !== undefined && e.button !== 0) return;
|
|
2948
|
+
dragRef.current = { x: e.clientX, y: e.clientY, ox: transform.x, oy: transform.y };
|
|
2949
|
+
}, [transform.x, transform.y]);
|
|
2950
|
+
const onMouseMove = useCallback((e) => {
|
|
2951
|
+
const drag = dragRef.current;
|
|
2952
|
+
if (!drag) return;
|
|
2953
|
+
setTransform((prev) => ({ ...prev, x: drag.ox + (e.clientX - drag.x), y: drag.oy + (e.clientY - drag.y) }));
|
|
2954
|
+
}, []);
|
|
2955
|
+
const endDrag = useCallback(() => { dragRef.current = null; }, []);
|
|
2956
|
+
|
|
2957
|
+
const neighbourSet = useMemo(() => {
|
|
2958
|
+
if (hovered === null) return null;
|
|
2959
|
+
const set = new Set([hovered]);
|
|
2960
|
+
for (const edge of shown.edges) {
|
|
2961
|
+
if (edge.source === hovered) set.add(edge.target);
|
|
2962
|
+
if (edge.target === hovered) set.add(edge.source);
|
|
2963
|
+
}
|
|
2964
|
+
return set;
|
|
2965
|
+
}, [hovered, shown.edges]);
|
|
2966
|
+
|
|
2967
|
+
const fitLabel = (text, maxUnits) => {
|
|
2968
|
+
let out = "";
|
|
2969
|
+
let units = 0;
|
|
2970
|
+
for (const ch of text) {
|
|
2971
|
+
const w = /[\u2E80-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF00-\uFFEF]/.test(ch) ? 2 : 1;
|
|
2972
|
+
if (units + w > maxUnits) return `${out}…`;
|
|
2973
|
+
out += ch;
|
|
2974
|
+
units += w;
|
|
2975
|
+
}
|
|
2976
|
+
return out;
|
|
2977
|
+
};
|
|
2978
|
+
const showAllLabels = plan.nodes.length <= 150;
|
|
2979
|
+
|
|
2980
|
+
// 小按钮工厂:分段控件(全库/局部/聚焦)+ 右侧图标按钮,统一尺寸,永不换行
|
|
2981
|
+
const modeButton = (label, active, onClick, options = {}) => createElement("button", {
|
|
2982
|
+
className: "dsh-obs-mini",
|
|
2983
|
+
style: {
|
|
2984
|
+
flex: "none",
|
|
2985
|
+
width: "auto",
|
|
2986
|
+
height: 24,
|
|
2987
|
+
padding: "0 10px",
|
|
2988
|
+
border: "none",
|
|
2989
|
+
borderRight: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
|
|
2990
|
+
borderRadius: 0,
|
|
2991
|
+
background: active ? "var(--dsw-alias-interactive-bg-hover-accent, rgba(99,102,241,.18))" : "transparent",
|
|
2992
|
+
color: active ? "var(--dsw-alias-brand-primary, #8b7cf6)" : "var(--dsw-alias-label-secondary, #b3b8c2)",
|
|
2993
|
+
whiteSpace: "nowrap",
|
|
2994
|
+
cursor: options.disabled === true ? "not-allowed" : "pointer",
|
|
2995
|
+
opacity: options.disabled === true ? 0.45 : 1,
|
|
2996
|
+
},
|
|
2997
|
+
disabled: options.disabled === true,
|
|
2998
|
+
title: options.title,
|
|
2999
|
+
onClick,
|
|
3000
|
+
}, label);
|
|
3001
|
+
const iconButton = (glyph, title, onClick) => createElement("button", {
|
|
3002
|
+
className: "dsh-obs-mini",
|
|
3003
|
+
style: { flex: "none", width: "auto", height: 24, minWidth: 24, padding: "0 4px" },
|
|
3004
|
+
title,
|
|
3005
|
+
onClick,
|
|
3006
|
+
}, glyph);
|
|
3007
|
+
|
|
3008
|
+
const head = createElement("div", {
|
|
3009
|
+
className: "dsh-obs-graph-head",
|
|
3010
|
+
title: t("graphMove"),
|
|
3011
|
+
onMouseDown: beginPanelDrag,
|
|
3012
|
+
style: {
|
|
3013
|
+
display: "flex",
|
|
3014
|
+
alignItems: "center",
|
|
3015
|
+
gap: 10,
|
|
3016
|
+
padding: "8px 12px",
|
|
3017
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
|
|
3018
|
+
cursor: "move",
|
|
3019
|
+
userSelect: "none",
|
|
3020
|
+
flex: "none",
|
|
3021
|
+
},
|
|
3022
|
+
},
|
|
3023
|
+
createElement("div", {
|
|
3024
|
+
className: "dsh-obs-graph-title",
|
|
3025
|
+
style: { whiteSpace: "nowrap", flex: "none" },
|
|
3026
|
+
}, `🕸 ${t("graph")}`),
|
|
3027
|
+
data !== null && createElement("div", {
|
|
3028
|
+
className: "dsh-obs-graph-stats",
|
|
3029
|
+
style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: "0 1 auto" },
|
|
3030
|
+
}, t("graphStats", { notes: data.stats.notes, tags: data.stats.tags, edges: data.stats.edges })),
|
|
3031
|
+
// 右侧控制区整体作为一块:分段按钮组(全库/局部/聚焦)+ 局部跳数 + 图标按钮
|
|
3032
|
+
createElement("div", {
|
|
3033
|
+
style: { display: "flex", alignItems: "center", gap: 10, marginLeft: "auto", flex: "none" },
|
|
3034
|
+
},
|
|
3035
|
+
createElement("div", {
|
|
3036
|
+
className: "dsh-obs-graph-modes",
|
|
3037
|
+
style: {
|
|
3038
|
+
display: "flex",
|
|
3039
|
+
alignItems: "center",
|
|
3040
|
+
flex: "none",
|
|
3041
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
|
|
3042
|
+
borderRadius: 8,
|
|
3043
|
+
overflow: "hidden",
|
|
3044
|
+
width: "auto",
|
|
3045
|
+
},
|
|
3046
|
+
},
|
|
3047
|
+
modeButton(t("graphGlobal"), mode === "global", () => setMode("global")),
|
|
3048
|
+
modeButton(t("graphLocal"), mode === "local", () => setMode("local"), {
|
|
3049
|
+
disabled: !localAvailable,
|
|
3050
|
+
title: localAvailable ? t("graphLocalHint") : t("graphNoNote"),
|
|
3051
|
+
}),
|
|
3052
|
+
modeButton(t("graphFocus"), false, () => {
|
|
3053
|
+
if (!localAvailable) return;
|
|
3054
|
+
setMode("local");
|
|
3055
|
+
setFitKey((v) => v + 1);
|
|
3056
|
+
}, {
|
|
3057
|
+
disabled: !localAvailable,
|
|
3058
|
+
title: localAvailable ? t("graphFocusHint") : t("graphNoNote"),
|
|
3059
|
+
}),
|
|
3060
|
+
),
|
|
3061
|
+
// 局部模式:跳数(与 Obsidian 局部图谱 depth 滑杆同范围 1–5)
|
|
3062
|
+
mode === "local" && createElement("span", {
|
|
3063
|
+
className: "dsh-obs-graph-depth",
|
|
3064
|
+
style: { display: "flex", alignItems: "center", gap: 2, flex: "none" },
|
|
3065
|
+
title: t("graphDepthHint"),
|
|
3066
|
+
},
|
|
3067
|
+
createElement("span", {
|
|
3068
|
+
className: "dsh-obs-graph-depth-label",
|
|
3069
|
+
style: { fontSize: 11, marginRight: 2, whiteSpace: "nowrap" },
|
|
3070
|
+
}, t("graphDepth")),
|
|
3071
|
+
[1, 2, 3, 4, 5].map((depth) => createElement("button", {
|
|
3072
|
+
key: depth,
|
|
3073
|
+
className: `dsh-obs-mini${localDepth === depth ? " dsh-obs-on" : ""}`,
|
|
3074
|
+
style: { flex: "none", width: "auto", height: 24, minWidth: 22, padding: "0 5px", whiteSpace: "nowrap" },
|
|
3075
|
+
onClick: () => setLocalDepth(depth),
|
|
3076
|
+
}, String(depth))),
|
|
3077
|
+
),
|
|
3078
|
+
createElement("div", { style: { display: "flex", alignItems: "center", gap: 2, flex: "none" } },
|
|
3079
|
+
iconButton("↻", t("graphRerun"), () => setSeed((v) => v + 1)),
|
|
3080
|
+
iconButton("⟳", t("graphRefresh"), () => setReloadKey((v) => v + 1)),
|
|
3081
|
+
iconButton("✕", t("graphClose"), onClose),
|
|
3082
|
+
),
|
|
3083
|
+
),
|
|
3084
|
+
);
|
|
3085
|
+
|
|
3086
|
+
let body;
|
|
3087
|
+
if (loading) {
|
|
3088
|
+
body = createElement("div", { className: "dsh-obs-graph-empty" },
|
|
3089
|
+
buildProgress !== null
|
|
3090
|
+
? t("graphBuilding", { done: buildProgress.done, total: buildProgress.total })
|
|
3091
|
+
: t("graphLoading"));
|
|
3092
|
+
} else if (error !== "") {
|
|
3093
|
+
body = createElement("div", { className: "dsh-obs-graph-error" },
|
|
3094
|
+
createElement("div", { className: "dsh-obs-graph-error-icon" }, "⚠️"),
|
|
3095
|
+
createElement("div", { className: "dsh-obs-graph-error-title" }, t("graphErrorTitle")),
|
|
3096
|
+
createElement("div", { className: "dsh-obs-graph-error-text" }, t("graphError", { msg: error })),
|
|
3097
|
+
createElement("div", { className: "dsh-obs-graph-error-actions" },
|
|
3098
|
+
createElement("button", { className: "dsh-obs-primary", onClick: () => setReloadKey((v) => v + 1) }, `↻ ${t("graphRetry")}`),
|
|
3099
|
+
createElement("button", {
|
|
3100
|
+
onClick: () => setDetailsOpen((v) => !v),
|
|
3101
|
+
}, detailsOpen ? t("graphDetailsHide") : t("graphDetails")),
|
|
3102
|
+
),
|
|
3103
|
+
detailsOpen && createElement("pre", { className: "dsh-obs-graph-error-details" }, error),
|
|
3104
|
+
);
|
|
3105
|
+
} else if (plan.nodes.length === 0) {
|
|
3106
|
+
body = createElement("div", { className: "dsh-obs-graph-empty" },
|
|
3107
|
+
mode === "local" ? t("graphNoNote") : t("graphEmpty"));
|
|
3108
|
+
} else {
|
|
3109
|
+
const planById = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
3110
|
+
body = createElement("div", {
|
|
3111
|
+
ref: wrapRef,
|
|
3112
|
+
className: "dsh-obs-graph-canvas",
|
|
3113
|
+
style: { position: "relative", flex: 1, minHeight: 0, overflow: "hidden", cursor: "grab" },
|
|
3114
|
+
onWheel,
|
|
3115
|
+
onMouseDown,
|
|
3116
|
+
onMouseMove,
|
|
3117
|
+
onMouseUp: endDrag,
|
|
3118
|
+
onMouseLeave: endDrag,
|
|
3119
|
+
},
|
|
3120
|
+
createElement("svg", { className: "dsh-obs-graph-svg" },
|
|
3121
|
+
createElement("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.s})` },
|
|
3122
|
+
shown.edges.map((edge) => {
|
|
3123
|
+
const a = planById.get(edge.source);
|
|
3124
|
+
const b = planById.get(edge.target);
|
|
3125
|
+
if (!a || !b) return null;
|
|
3126
|
+
const active = neighbourSet !== null && (neighbourSet.has(edge.source) || neighbourSet.has(edge.target));
|
|
3127
|
+
return createElement("line", {
|
|
3128
|
+
key: `${edge.source}-${edge.target}`,
|
|
3129
|
+
className: `dsh-obs-graph-edge${active ? " dsh-obs-graph-edge-on" : ""}`,
|
|
3130
|
+
x1: a.x,
|
|
3131
|
+
y1: a.y,
|
|
3132
|
+
x2: b.x,
|
|
3133
|
+
y2: b.y,
|
|
3134
|
+
});
|
|
3135
|
+
}),
|
|
3136
|
+
plan.nodes.map((node) => {
|
|
3137
|
+
const isTag = node.kind === "tag";
|
|
3138
|
+
const radius = isTag ? 7 : 4 + Math.min(9, node.degree * 1.5);
|
|
3139
|
+
const dim = neighbourSet !== null && !neighbourSet.has(node.id);
|
|
3140
|
+
const cls = `dsh-obs-graph-node ${isTag ? "dsh-obs-graph-tag" : "dsh-obs-graph-note"}`
|
|
3141
|
+
+ `${node.id === hovered ? " dsh-obs-graph-sel" : ""}${dim ? " dsh-obs-graph-dim" : ""}`
|
|
3142
|
+
+ `${focusId !== null && node.id === focusId ? " dsh-obs-graph-focus" : ""}`;
|
|
3143
|
+
const label = showAllLabels || node.degree >= 5 || node.id === hovered
|
|
3144
|
+
? fitLabel(node.label, 26)
|
|
3145
|
+
: "";
|
|
3146
|
+
return createElement("g", {
|
|
3147
|
+
key: node.id,
|
|
3148
|
+
className: cls,
|
|
3149
|
+
transform: `translate(${node.x},${node.y})`,
|
|
3150
|
+
onMouseEnter: () => setHovered(node.id),
|
|
3151
|
+
onMouseLeave: () => setHovered(null),
|
|
3152
|
+
onClick: (e) => {
|
|
3153
|
+
if (e && e.stopPropagation) e.stopPropagation();
|
|
3154
|
+
if (!isTag && onOpenNote) onOpenNote(node.path, node.label);
|
|
3155
|
+
else setHovered(node.id);
|
|
3156
|
+
},
|
|
3157
|
+
},
|
|
3158
|
+
createElement(isTag ? "rect" : "circle", isTag
|
|
3159
|
+
? { x: -radius, y: -radius, width: radius * 2, height: radius * 2, rx: 3 }
|
|
3160
|
+
: { r: radius }),
|
|
3161
|
+
createElement("title", null, node.label),
|
|
3162
|
+
label !== "" && createElement("text", { x: radius + 6, y: 4 }, label),
|
|
3163
|
+
);
|
|
3164
|
+
}),
|
|
3165
|
+
),
|
|
3166
|
+
),
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
const truncatedNote = plan.truncated ? t("graphTruncated", { n: plan.nodes.length }) : t("graphHint");
|
|
3171
|
+
// 关键布局全部走内联样式:即使插件的 <style> 注入失败(旧 bundle / 特殊宿主),
|
|
3172
|
+
// 面板也不会变成左上角裸文本;z-index 也内联,避免被宿主容器压住。
|
|
3173
|
+
const defaultX = typeof window !== "undefined"
|
|
3174
|
+
? Math.max(16, Math.round((window.innerWidth - geometry.width) / 2))
|
|
3175
|
+
: 80;
|
|
3176
|
+
const style = {
|
|
3177
|
+
position: "fixed",
|
|
3178
|
+
left: geometry.x === null ? defaultX : geometry.x,
|
|
3179
|
+
top: geometry.y,
|
|
3180
|
+
width: geometry.width,
|
|
3181
|
+
height: geometry.height,
|
|
3182
|
+
zIndex: 2147483000,
|
|
3183
|
+
display: "flex",
|
|
3184
|
+
flexDirection: "column",
|
|
3185
|
+
boxSizing: "border-box",
|
|
3186
|
+
overflow: "hidden",
|
|
3187
|
+
background: "var(--dsw-alias-bg-overlay, #1f2328)",
|
|
3188
|
+
color: "var(--dsw-alias-label-primary, #e6e6e6)",
|
|
3189
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.15))",
|
|
3190
|
+
borderRadius: 14,
|
|
3191
|
+
boxShadow: "0 24px 64px rgba(0,0,0,.45)",
|
|
3192
|
+
};
|
|
3193
|
+
return createElement("div", {
|
|
3194
|
+
className: "dsh-obs-graph",
|
|
3195
|
+
role: "dialog",
|
|
3196
|
+
"aria-label": t("graph"),
|
|
3197
|
+
style,
|
|
3198
|
+
},
|
|
3199
|
+
head,
|
|
3200
|
+
body,
|
|
3201
|
+
createElement("div", {
|
|
3202
|
+
className: "dsh-obs-graph-hint",
|
|
3203
|
+
style: {
|
|
3204
|
+
flex: "none",
|
|
3205
|
+
display: "flex",
|
|
3206
|
+
alignItems: "center",
|
|
3207
|
+
gap: 12,
|
|
3208
|
+
padding: "6px 14px",
|
|
3209
|
+
borderTop: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
|
|
3210
|
+
fontSize: 11,
|
|
3211
|
+
color: "var(--dsw-alias-label-tertiary, #9aa0aa)",
|
|
3212
|
+
},
|
|
3213
|
+
},
|
|
3214
|
+
createElement("span", {
|
|
3215
|
+
style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: "0 1 auto" },
|
|
3216
|
+
}, truncatedNote),
|
|
3217
|
+
createElement("span", {
|
|
3218
|
+
className: "dsh-obs-graph-legend",
|
|
3219
|
+
style: { display: "flex", alignItems: "center", gap: 10, flex: "none", marginLeft: "auto", whiteSpace: "nowrap" },
|
|
3220
|
+
},
|
|
3221
|
+
createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 4 } },
|
|
3222
|
+
createElement("span", { className: "dsh-obs-legend-dot" }), t("graphLegendNote")),
|
|
3223
|
+
createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 4 } },
|
|
3224
|
+
createElement("span", { className: "dsh-obs-legend-tag" }), t("graphLegendTag")),
|
|
3225
|
+
createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 4 } },
|
|
3226
|
+
createElement("span", { className: "dsh-obs-legend-focus" }), t("graphLegendFocus")),
|
|
3227
|
+
createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 4 } },
|
|
3228
|
+
createElement("span", { className: "dsh-obs-legend-link" }), t("graphLegendLink")),
|
|
3229
|
+
),
|
|
3230
|
+
),
|
|
3231
|
+
createElement("div", { className: "dsh-obs-graph-resize", title: t("resize"), onMouseDown: beginPanelResize }),
|
|
3232
|
+
);
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
/** shell.overlay 包装:只有 shared.graphOpen 时才挂载 GraphView(关闭即卸载)。 */
|
|
3236
|
+
function GraphOverlay({ t }) {
|
|
3237
|
+
const { graphOpen, openNote } = useShared();
|
|
3238
|
+
if (!graphOpen) return null;
|
|
3239
|
+
return createElement(GraphView, {
|
|
3240
|
+
t,
|
|
3241
|
+
currentNote: openNote,
|
|
3242
|
+
onOpenNote: (rel, name) => patchShared({ openNote: { rel, name } }),
|
|
3243
|
+
onClose: () => patchShared({ graphOpen: false }),
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
|
|
2553
3247
|
// ── 右侧笔记 Dock(shell.overlay)──────────────────────────────────────────
|
|
3248
|
+
|
|
3249
|
+
/**
|
|
3250
|
+
* 思维导图视图(B1):把 note.content 的标题/列表画成横向树。
|
|
3251
|
+
* - 滚轮缩放(以指针为中心)/ 拖动平移;
|
|
3252
|
+
* - 单击节点:选中并显示操作条(跳到源码行 / 把分支插入对话);
|
|
3253
|
+
* - 双击节点:直接跳到该节点的源码行。
|
|
3254
|
+
*/
|
|
3255
|
+
function MindMapView({ note, t, onJumpLine, onAskBranch }) {
|
|
3256
|
+
const tree = useMemo(
|
|
3257
|
+
() => mindMapModule.buildTree(note.content, { title: note.name.replace(/\.md$/i, "") }),
|
|
3258
|
+
[note.content, note.name],
|
|
3259
|
+
);
|
|
3260
|
+
const plan = useMemo(() => mindMapModule.layout(tree), [tree]);
|
|
3261
|
+
const [viewState, setViewState] = useState({ x: 0, y: 14, s: 1 });
|
|
3262
|
+
const [selected, setSelected] = useState(null);
|
|
3263
|
+
const dragRef = useRef(null);
|
|
3264
|
+
const wrapRef = useRef(null);
|
|
3265
|
+
const hasNodes = plan.nodes.length > 1;
|
|
3266
|
+
|
|
3267
|
+
// 初始把整张导图缩放进 Dock 可视区(导图比面板宽,不 fit 只能看到最左一列)
|
|
3268
|
+
useEffect(() => {
|
|
3269
|
+
const el = wrapRef.current;
|
|
3270
|
+
if (!el || !hasNodes) return;
|
|
3271
|
+
const w = el.clientWidth || 480;
|
|
3272
|
+
const h = el.clientHeight || 320;
|
|
3273
|
+
// 横向导图比面板宽:只按高度适配(保持字号可读),横向靠拖动平移
|
|
3274
|
+
const fit = Math.min(1, (h - 28) / plan.height);
|
|
3275
|
+
setViewState({
|
|
3276
|
+
x: 10,
|
|
3277
|
+
y: Math.max(10, (h - plan.height * fit) / 2),
|
|
3278
|
+
s: fit,
|
|
3279
|
+
});
|
|
3280
|
+
}, [plan, hasNodes]);
|
|
3281
|
+
|
|
3282
|
+
// SVG 无自动省略:按估算字宽截断,完整文本放 <title> 悬停显示
|
|
3283
|
+
const fitText = (text, width) => {
|
|
3284
|
+
const maxUnits = Math.max(6, Math.floor((width - 26) / 7.2));
|
|
3285
|
+
let out = "";
|
|
3286
|
+
let units = 0;
|
|
3287
|
+
for (const ch of text) {
|
|
3288
|
+
const u = /[\u2E80-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF00-\uFFEF]/.test(ch) ? 2 : 1;
|
|
3289
|
+
if (units + u > maxUnits) return `${out}…`;
|
|
3290
|
+
out += ch;
|
|
3291
|
+
units += u;
|
|
3292
|
+
}
|
|
3293
|
+
return out;
|
|
3294
|
+
};
|
|
3295
|
+
|
|
3296
|
+
const onWheel = useCallback((e) => {
|
|
3297
|
+
if (!hasNodes) return;
|
|
3298
|
+
if (e.cancelable !== false && e.preventDefault) e.preventDefault();
|
|
3299
|
+
const rect = e.currentTarget && e.currentTarget.getBoundingClientRect
|
|
3300
|
+
? e.currentTarget.getBoundingClientRect()
|
|
3301
|
+
: { left: 0, top: 0 };
|
|
3302
|
+
const px = (e.clientX || 0) - rect.left;
|
|
3303
|
+
const py = (e.clientY || 0) - rect.top;
|
|
3304
|
+
setViewState((prev) => {
|
|
3305
|
+
const next = Math.min(2.6, Math.max(0.35, prev.s * (e.deltaY > 0 ? 0.88 : 1.14)));
|
|
3306
|
+
const k = next / prev.s;
|
|
3307
|
+
return { s: next, x: px - (px - prev.x) * k, y: py - (py - prev.y) * k };
|
|
3308
|
+
});
|
|
3309
|
+
}, [hasNodes]);
|
|
3310
|
+
|
|
3311
|
+
const onMouseDown = useCallback((e) => {
|
|
3312
|
+
if (e.button !== undefined && e.button !== 0) return;
|
|
3313
|
+
dragRef.current = { x: e.clientX, y: e.clientY, ox: viewState.x, oy: viewState.y };
|
|
3314
|
+
}, [viewState.x, viewState.y]);
|
|
3315
|
+
const onMouseMove = useCallback((e) => {
|
|
3316
|
+
const drag = dragRef.current;
|
|
3317
|
+
if (!drag) return;
|
|
3318
|
+
setViewState((prev) => ({ ...prev, x: drag.ox + (e.clientX - drag.x), y: drag.oy + (e.clientY - drag.y) }));
|
|
3319
|
+
}, []);
|
|
3320
|
+
const endDrag = useCallback(() => { dragRef.current = null; }, []);
|
|
3321
|
+
|
|
3322
|
+
if (!hasNodes) {
|
|
3323
|
+
return createElement("div", { className: "dsh-obs-map-empty" }, t("mapEmpty"));
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
const selectedNode = selected !== null ? plan.byId[selected] : null;
|
|
3327
|
+
return createElement("div", {
|
|
3328
|
+
ref: wrapRef,
|
|
3329
|
+
className: "dsh-obs-map",
|
|
3330
|
+
onWheel,
|
|
3331
|
+
onMouseDown,
|
|
3332
|
+
onMouseMove,
|
|
3333
|
+
onMouseUp: endDrag,
|
|
3334
|
+
onMouseLeave: endDrag,
|
|
3335
|
+
},
|
|
3336
|
+
createElement("svg", { className: "dsh-obs-map-svg" },
|
|
3337
|
+
createElement("g", { transform: `translate(${viewState.x},${viewState.y}) scale(${viewState.s})` },
|
|
3338
|
+
plan.edges.map((edge) => {
|
|
3339
|
+
const a = plan.byId[edge.from];
|
|
3340
|
+
const b = plan.byId[edge.to];
|
|
3341
|
+
if (!a || !b) return null;
|
|
3342
|
+
const x1 = a.x + a.w;
|
|
3343
|
+
const x2 = b.x;
|
|
3344
|
+
const mx = (x1 + x2) / 2;
|
|
3345
|
+
return createElement("path", {
|
|
3346
|
+
key: `${edge.from}-${edge.to}`,
|
|
3347
|
+
className: "dsh-obs-map-edge",
|
|
3348
|
+
d: `M ${x1} ${a.y} C ${mx} ${a.y}, ${mx} ${b.y}, ${x2} ${b.y}`,
|
|
3349
|
+
});
|
|
3350
|
+
}),
|
|
3351
|
+
plan.nodes.map((node) => createElement("g", {
|
|
3352
|
+
key: node.id,
|
|
3353
|
+
className: `dsh-obs-map-node dsh-obs-map-${node.kind}${selected === node.id ? " dsh-obs-map-sel" : ""}`,
|
|
3354
|
+
transform: `translate(${node.x},${node.y - node.h / 2})`,
|
|
3355
|
+
onClick: (e) => {
|
|
3356
|
+
if (e && e.stopPropagation) e.stopPropagation();
|
|
3357
|
+
setSelected(node.id);
|
|
3358
|
+
},
|
|
3359
|
+
onDoubleClick: (e) => {
|
|
3360
|
+
if (e && e.stopPropagation) e.stopPropagation();
|
|
3361
|
+
if (node.line > 0) onJumpLine(node.line);
|
|
3362
|
+
},
|
|
3363
|
+
},
|
|
3364
|
+
createElement("rect", { width: node.w, height: node.h, rx: 9 }),
|
|
3365
|
+
createElement("text", { x: 13, y: node.h / 2 + 5 }, fitText(node.text, node.w)),
|
|
3366
|
+
createElement("title", null, node.text),
|
|
3367
|
+
)),
|
|
3368
|
+
),
|
|
3369
|
+
),
|
|
3370
|
+
selectedNode !== null && createElement("div", { className: "dsh-obs-map-actions" },
|
|
3371
|
+
createElement("span", { className: "dsh-obs-map-line" }, `L${selectedNode.line}`),
|
|
3372
|
+
createElement("button", {
|
|
3373
|
+
onClick: () => onJumpLine(selectedNode.line),
|
|
3374
|
+
}, `↩ ${t("mapJump")}`),
|
|
3375
|
+
createElement("button", {
|
|
3376
|
+
className: "dsh-obs-primary",
|
|
3377
|
+
onClick: () => onAskBranch(selectedNode),
|
|
3378
|
+
}, `⤴ ${t("mapAsk")}`),
|
|
3379
|
+
),
|
|
3380
|
+
createElement("div", { className: "dsh-obs-map-hint" }, t("mapHint")),
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
3383
|
+
|
|
2554
3384
|
function Dock({ t, workspaces }) {
|
|
2555
3385
|
const { openNote, vaultRev } = useShared();
|
|
2556
3386
|
const [note, setNote] = useState(null);
|
|
2557
3387
|
const [root, setRoot] = useState("");
|
|
2558
3388
|
const [editing, setEditing] = useState(false);
|
|
3389
|
+
// 视图:预览(行号)/ 思维导图;编辑态始终是编辑器
|
|
3390
|
+
const [view, setView] = useState("preview");
|
|
3391
|
+
const [jumpLine, setJumpLine] = useState(null);
|
|
2559
3392
|
const [draft, setDraft] = useState("");
|
|
2560
3393
|
const [saving, setSaving] = useState(false);
|
|
2561
3394
|
const [error, setError] = useState("");
|
|
@@ -2852,6 +3685,60 @@
|
|
|
2852
3685
|
patchShared({ openNote: { rel: found.path, name: found.name } });
|
|
2853
3686
|
}, [editing, draft, note, t]);
|
|
2854
3687
|
|
|
3688
|
+
/** 思维导图 → 预览:跳到源码行并短暂高亮(行元素带 data-line)。 */
|
|
3689
|
+
const jumpToLine = useCallback((line) => {
|
|
3690
|
+
if (!line || line < 1) return;
|
|
3691
|
+
setView("preview");
|
|
3692
|
+
setJumpLine(line);
|
|
3693
|
+
}, []);
|
|
3694
|
+
|
|
3695
|
+
/** 思维导图 → 对话:把「该节点 + 全部后代」的源码区间按选区提问同款 prompt 插入输入框。 */
|
|
3696
|
+
const askBranch = useCallback((node) => {
|
|
3697
|
+
if (!note) return;
|
|
3698
|
+
const span = mindMapModule.branchSpan(note.content, node);
|
|
3699
|
+
const prompt = buildPrompt(
|
|
3700
|
+
t,
|
|
3701
|
+
treeApi.absPath(note.path, root),
|
|
3702
|
+
note.content,
|
|
3703
|
+
{ text: span.text, start: span.start, end: span.end, mode: "preview" },
|
|
3704
|
+
"",
|
|
3705
|
+
);
|
|
3706
|
+
if (injectPrompt(prompt)) {
|
|
3707
|
+
showToast(t("branchCited", { n: span.endLine - span.startLine + 1 }));
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
try {
|
|
3711
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
3712
|
+
navigator.clipboard.writeText(prompt).then(() => showToast(t("noInput"))).catch(() => showToast(t("citeFail")));
|
|
3713
|
+
} else {
|
|
3714
|
+
showToast(t("citeFail"));
|
|
3715
|
+
}
|
|
3716
|
+
} catch {
|
|
3717
|
+
showToast(t("citeFail"));
|
|
3718
|
+
}
|
|
3719
|
+
}, [note, t, root]);
|
|
3720
|
+
|
|
3721
|
+
// 切笔记时回到预览态(避免上一份笔记的导图/跳转状态残留)
|
|
3722
|
+
useEffect(() => {
|
|
3723
|
+
setView("preview");
|
|
3724
|
+
setJumpLine(null);
|
|
3725
|
+
}, [openNote]);
|
|
3726
|
+
|
|
3727
|
+
// 预览行高亮:跳到行号后滚动到该行并闪一下(1.8s 后恢复)
|
|
3728
|
+
useEffect(() => {
|
|
3729
|
+
if (jumpLine === null || view !== "preview" || !previewRef.current) return undefined;
|
|
3730
|
+
const el = previewRef.current.querySelector(`[data-line="${jumpLine}"]`);
|
|
3731
|
+
if (el) {
|
|
3732
|
+
if (typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "center" });
|
|
3733
|
+
el.classList.add("dsh-obs-md-flash");
|
|
3734
|
+
}
|
|
3735
|
+
const timer = window.setTimeout(() => {
|
|
3736
|
+
if (el) el.classList.remove("dsh-obs-md-flash");
|
|
3737
|
+
setJumpLine(null);
|
|
3738
|
+
}, 1800);
|
|
3739
|
+
return () => window.clearTimeout(timer);
|
|
3740
|
+
}, [jumpLine, view, note, vaultRev]);
|
|
3741
|
+
|
|
2855
3742
|
const dirty = editing && note !== null && draft !== note.content;
|
|
2856
3743
|
|
|
2857
3744
|
// 编辑态行号(每行一个,对应源码行数;textarea 为 white-space:pre 无折行,行号与文本行一一对齐)
|
|
@@ -2875,6 +3762,11 @@
|
|
|
2875
3762
|
createElement("span", null, "📝"),
|
|
2876
3763
|
createElement("span", null, note ? note.name : openNote.name),
|
|
2877
3764
|
),
|
|
3765
|
+
!editing && note && createElement("button", {
|
|
3766
|
+
className: view === "map" ? "dsh-obs-on" : undefined,
|
|
3767
|
+
title: view === "map" ? t("preview") : t("mindMap"),
|
|
3768
|
+
onClick: () => setView((prev) => (prev === "map" ? "preview" : "map")),
|
|
3769
|
+
}, view === "map" ? `📄 ${t("preview")}` : `🧠 ${t("mindMap")}`),
|
|
2878
3770
|
!editing && note && createElement("button", { onClick: () => void cite() }, `⤴ ${t("cite")}`),
|
|
2879
3771
|
!editing && note && createElement("button", { onClick: () => { setDraft(note.content); setEditing(true); } }, t("edit")),
|
|
2880
3772
|
editing && createElement("button", {
|
|
@@ -2926,8 +3818,17 @@
|
|
|
2926
3818
|
},
|
|
2927
3819
|
}),
|
|
2928
3820
|
)
|
|
2929
|
-
:
|
|
2930
|
-
createElement("div", {
|
|
3821
|
+
: view === "map"
|
|
3822
|
+
? createElement("div", { className: "dsh-obs-dock-body dsh-obs-dock-map" },
|
|
3823
|
+
createElement(MindMapView, {
|
|
3824
|
+
note,
|
|
3825
|
+
t,
|
|
3826
|
+
onJumpLine: jumpToLine,
|
|
3827
|
+
onAskBranch: askBranch,
|
|
3828
|
+
}),
|
|
3829
|
+
)
|
|
3830
|
+
: createElement("div", { className: "dsh-obs-dock-body" },
|
|
3831
|
+
createElement("div", { ref: previewRef, className: "dsh-obs-md" },
|
|
2931
3832
|
renderNumbered(note.content, {
|
|
2932
3833
|
onWikilink: (target) => void openWikilink(target),
|
|
2933
3834
|
onText: () => {},
|
|
@@ -3216,10 +4117,15 @@
|
|
|
3216
4117
|
{ name: "shell.overlay", id: "obsidian-prompt", order: 6, label: "Obsidian text prompt", locale: NS },
|
|
3217
4118
|
Prompt,
|
|
3218
4119
|
);
|
|
4120
|
+
const d4 = ctx.slots.register(
|
|
4121
|
+
{ name: "shell.overlay", id: "obsidian-graph", order: 32, label: "Obsidian relation graph", locale: NS },
|
|
4122
|
+
GraphOverlay,
|
|
4123
|
+
);
|
|
3219
4124
|
return () => {
|
|
3220
4125
|
d1();
|
|
3221
4126
|
d2();
|
|
3222
4127
|
d3();
|
|
4128
|
+
d4();
|
|
3223
4129
|
};
|
|
3224
4130
|
});
|
|
3225
4131
|
}
|