@lancc/cmark 1.1.0 → 1.2.1

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.
Files changed (139) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -21
  3. package/README.md +54 -82
  4. package/assets/abnfDiagram-WEYOIA3C-CKV_7Upb.js +1 -0
  5. package/assets/architectureDiagram-ZHVYKT53-BvLudDvk.js +1 -0
  6. package/assets/blockDiagram-RJAE7QA3--gzkMGi2.js +1 -0
  7. package/assets/c4Diagram-XDPPPE54-ByfRiJGR.js +1 -0
  8. package/assets/chunk-2E7WHVMD-TfbC_dmh.js +1 -0
  9. package/assets/chunk-2Q5K7J3B-0YJgChVo.js +1 -0
  10. package/assets/chunk-4EMNKR2T-DHoYW7g5.js +1 -0
  11. package/assets/chunk-55XCU76K-Cq25Yw_G.js +1 -0
  12. package/assets/chunk-5VM5RSS4-CJaRXFHK.js +1 -0
  13. package/assets/chunk-JWPE2WC7-BP2iXBhe.js +1 -0
  14. package/assets/chunk-VQ5W62QI-B95zur4F.js +1 -0
  15. package/assets/chunk-XXDRQBXY-Ct0MhvQo.js +1 -0
  16. package/assets/chunk-ZCHMQSOR-C5VhW-wL.js +2 -0
  17. package/assets/classDiagram-v2-4MYWR3UA-C1G-Vkj0.js +1 -0
  18. package/assets/cose-bilkent-JH36ORCC-qIQoIBv0.js +1 -0
  19. package/assets/cynefin-EF2NZ3EQ-CR1E3YiZ.js +1 -0
  20. package/assets/cynefinDiagram-WF24UDRP-DaJFvZ3b.js +1 -0
  21. package/assets/cytoscape.esm-SjxgSg6I.js +9 -0
  22. package/assets/dagre-NK4GIZVX-BFRQhpii.js +1 -0
  23. package/assets/diagram-CBK3MWTC-DlueL6XZ.js +1 -0
  24. package/assets/diagram-I5XIIV2D-bITyafYX.js +1 -0
  25. package/assets/diagram-ODIL2UCW-Dlb4hg_-.js +1 -0
  26. package/assets/diagram-PFBEJUXE-CRfen0O7.js +1 -0
  27. package/assets/diagram-RJA3I4VV-CK-FlhGV.js +1 -0
  28. package/assets/diagram-TYUNFSNR-DmgVsxqa.js +1 -0
  29. package/assets/ebnfDiagram-V4P3TTPT-B9KGI9eQ.js +1 -0
  30. package/assets/echarts-CytsvCdp.js +25 -0
  31. package/assets/elk-QONTBCTQ-VgDftvsU.js +1 -0
  32. package/assets/erDiagram-KYQIGQAA-DkpP7sS4.js +1 -0
  33. package/assets/flowDiagram-IUGLLWBU-CRXa9Kxy.js +1 -0
  34. package/assets/ganttDiagram-R7GPNTJY-Cu2I55Wb.js +1 -0
  35. package/assets/gitGraphDiagram-PDYL7XHY-CwSo663I.js +1 -0
  36. package/assets/graph-Dz7AM1Q5.js +1 -0
  37. package/assets/index-BJ-NdQc4.js +100 -0
  38. package/assets/index-eB_q_VA3.css +1 -0
  39. package/assets/infoDiagram-MVDRKAX5-D3Qknzuo.js +1 -0
  40. package/assets/ishikawaDiagram-HPEHV2GE-9qYT75tk.js +1 -0
  41. package/assets/journeyDiagram-BOSHW7O7-DeSrJg9o.js +1 -0
  42. package/assets/kanban-definition-FNSA4HC5-DINkB6G7.js +1 -0
  43. package/assets/katex-DN8cTsI3.js +1 -0
  44. package/assets/markdown-S2mWvoE-.js +5 -0
  45. package/assets/mermaid-T_6Ze_cT.js +7 -0
  46. package/assets/mindmap-definition-6OY57UG3-DW5hqiAe.js +1 -0
  47. package/assets/pegDiagram-ECIUJNBX-BWy_tkzy.js +1 -0
  48. package/assets/pieDiagram-GNMPY74T-CniJbr6l.js +1 -0
  49. package/assets/quadrantDiagram-D57K4MC3-BVnDJkCv.js +1 -0
  50. package/assets/railroadDiagram-Y72NRBL7-BNKZpJJb.js +1 -0
  51. package/assets/requirementDiagram-EQYSW5RA-BETipHj0.js +1 -0
  52. package/assets/sankeyDiagram-SY24JHO5-ByEYqT7T.js +1 -0
  53. package/assets/sequenceDiagram-BLEHNHQU-BqCJ2tKG.js +1 -0
  54. package/assets/sizeCapture-INFHLROL-DlhrWFj-.js +1 -0
  55. package/assets/stateDiagram-v2-A6IESKWZ-DJUBWLTP.js +1 -0
  56. package/assets/swimlanes-B3ENRKFQ-D7k7tF5U.js +1 -0
  57. package/assets/swimlanesDiagram-AYQRV4AK-DoQBgB9o.js +1 -0
  58. package/assets/timeline-definition-JXYKGLGB-BCNhwnn6.js +1 -0
  59. package/assets/usecaseDiagram-WPOLBNNX-DRa_Rgl3.js +1 -0
  60. package/assets/vennDiagram-Q3Y3IDCW-Ba2eaO68.js +1 -0
  61. package/assets/wardleyDiagram-IVAIDKWA-W_i9vzY-.js +1 -0
  62. package/assets/xychartDiagram-4URNHDU4-ByfaGjvc.js +1 -0
  63. package/export-file/github-markdown.css +549 -549
  64. package/export-file/index.html +32 -32
  65. package/index.html +4 -4
  66. package/logo.png +0 -0
  67. package/package.json +1 -1
  68. package/plugin.json +8 -218
  69. package/preload/branch-bar.js +44 -2
  70. package/preload/export-css.js +59 -0
  71. package/preload/git-panel.js +102 -23
  72. package/preload/git-service.js +99 -6
  73. package/preload/host.js +130 -0
  74. package/preload/remote-setup.js +425 -0
  75. package/preload/remote-wizard.js +330 -0
  76. package/preload/rename-note.js +4 -5
  77. package/preload/services.js +6 -15
  78. package/preload/settings-panel.js +468 -356
  79. package/preload/ws-prefs.js +55 -0
  80. package/preload.js +291 -6
  81. package/assets/abnfDiagram-WEYOIA3C-BgXsTF7a.js +0 -1
  82. package/assets/architectureDiagram-ZHVYKT53-BsCWa_qC.js +0 -36
  83. package/assets/blockDiagram-RJAE7QA3-wbIxEiYN.js +0 -140
  84. package/assets/c4Diagram-XDPPPE54-5zdyxFvA.js +0 -38
  85. package/assets/chunk-2E7WHVMD-F85Lca-i.js +0 -88
  86. package/assets/chunk-2Q5K7J3B-CjpAZ0OI.js +0 -1
  87. package/assets/chunk-4EMNKR2T-C8Kbh83c.js +0 -1
  88. package/assets/chunk-55XCU76K-BINcFiLN.js +0 -1
  89. package/assets/chunk-5VM5RSS4-Diy5h2LS.js +0 -15
  90. package/assets/chunk-JWPE2WC7-BXnjDdh1.js +0 -1
  91. package/assets/chunk-VQ5W62QI-BM7ZZo1I.js +0 -1
  92. package/assets/chunk-XXDRQBXY-CW8CjB1w.js +0 -1
  93. package/assets/chunk-ZCHMQSOR-C7ATz1L1.js +0 -2
  94. package/assets/classDiagram-v2-4MYWR3UA-CMoqNk-b.js +0 -217
  95. package/assets/cose-bilkent-JH36ORCC-6rqR5iJk.js +0 -1
  96. package/assets/cynefin-EF2NZ3EQ-DypLIJxl.js +0 -141
  97. package/assets/cynefinDiagram-WF24UDRP-8b2CEBlR.js +0 -62
  98. package/assets/cytoscape.esm-CjX6Hyah.js +0 -331
  99. package/assets/dagre-NK4GIZVX-Dv6vuUbR.js +0 -4
  100. package/assets/diagram-CBK3MWTC-DJgMDeYd.js +0 -24
  101. package/assets/diagram-I5XIIV2D-BB9cMoRW.js +0 -24
  102. package/assets/diagram-ODIL2UCW-BVcRtsdy.js +0 -30
  103. package/assets/diagram-PFBEJUXE-BbGSOtJx.js +0 -3
  104. package/assets/diagram-RJA3I4VV-CzkY_8Fo.js +0 -41
  105. package/assets/diagram-TYUNFSNR-YTl5QdfL.js +0 -200
  106. package/assets/ebnfDiagram-V4P3TTPT-COVjPAjE.js +0 -1
  107. package/assets/echarts-BAPDkhe3.js +0 -61
  108. package/assets/elk-QONTBCTQ-DHsjY8Z_.js +0 -27
  109. package/assets/erDiagram-KYQIGQAA-CUAbTUl1.js +0 -99
  110. package/assets/flowDiagram-IUGLLWBU-CWsiqTQC.js +0 -213
  111. package/assets/ganttDiagram-R7GPNTJY-DetBW0Wm.js +0 -292
  112. package/assets/gitGraphDiagram-PDYL7XHY-ByrdgJmw.js +0 -106
  113. package/assets/graph-D8UmR1vK.js +0 -1
  114. package/assets/index-BLONSItV.css +0 -1
  115. package/assets/index-Cz6qaZbV.js +0 -744
  116. package/assets/infoDiagram-MVDRKAX5-BM48fjhU.js +0 -2
  117. package/assets/ishikawaDiagram-HPEHV2GE-DtHYywHZ.js +0 -70
  118. package/assets/journeyDiagram-BOSHW7O7-DJ1LiRh3.js +0 -139
  119. package/assets/kanban-definition-FNSA4HC5-DPPt8pwJ.js +0 -89
  120. package/assets/katex-CQL9J12G.js +0 -257
  121. package/assets/markdown-DVehI1Sg.js +0 -314
  122. package/assets/mermaid-C_q9pug0.js +0 -312
  123. package/assets/mindmap-definition-6OY57UG3-zbqYOEY6.js +0 -98
  124. package/assets/pegDiagram-ECIUJNBX-e7QG7TMq.js +0 -1
  125. package/assets/pieDiagram-GNMPY74T-Duf6YP0N.js +0 -39
  126. package/assets/quadrantDiagram-D57K4MC3-B2eYoUUy.js +0 -7
  127. package/assets/railroadDiagram-Y72NRBL7-BWCzPcI_.js +0 -1
  128. package/assets/requirementDiagram-EQYSW5RA-HhPMEqB1.js +0 -84
  129. package/assets/sankeyDiagram-SY24JHO5-CMUTwFBB.js +0 -40
  130. package/assets/sequenceDiagram-BLEHNHQU-C9wR2ll7.js +0 -169
  131. package/assets/sizeCapture-INFHLROL-DXfz37S6.js +0 -1
  132. package/assets/stateDiagram-v2-A6IESKWZ-D_gtonhE.js +0 -291
  133. package/assets/swimlanes-B3ENRKFQ-DdJOuuMV.js +0 -1
  134. package/assets/swimlanesDiagram-AYQRV4AK-BWt3HxHV.js +0 -8
  135. package/assets/timeline-definition-JXYKGLGB-DQsdwJe-.js +0 -120
  136. package/assets/usecaseDiagram-WPOLBNNX-DTRyFq7d.js +0 -370
  137. package/assets/vennDiagram-Q3Y3IDCW-dihYszm0.js +0 -34
  138. package/assets/wardleyDiagram-IVAIDKWA-BoPpgYfK.js +0 -78
  139. package/assets/xychartDiagram-4URNHDU4-DoLjYxeB.js +0 -7
@@ -117,6 +117,14 @@ module.exports = function attachSettingsPanel (win) {
117
117
  /* 状态卡里的远端地址可能很长,允许在任意位置折行 */
118
118
  .zgs-kv-url{word-break:break-all;}
119
119
  /* 只读地址框:看起来是"展示",不是"待填" */
120
+ #zgs-body .zgs-auth-err{margin:10px 0 0;padding:10px 12px;border-radius:8px;font-size:12px;line-height:1.7;
121
+ color:#cf222e;background:rgba(207,34,46,.10);border:1px solid rgba(207,34,46,.35);word-break:break-word;}
122
+ :root[data-theme='dark'] #zgs-body .zgs-auth-err{color:#ff8f85;background:rgba(255,120,110,.12);border-color:rgba(255,120,110,.4);}
123
+ /* 抽屉内的小节(提交 / 自动同步):用细分隔线与上方仓库状态隔开 */
124
+ #zgs-body .zgs-sub{margin-top:16px;padding-top:14px;border-top:1px solid rgba(128,128,128,.22);}
125
+ #zgs-body .zgs-sub h5{font-size:12px;font-weight:600;margin:0 0 8px;letter-spacing:.02em;}
126
+ /* Git 未启用时,提交/自动同步整体置灰且不可交互 */
127
+ #zgs-body .zgp-sec.is-disabled{opacity:.45;pointer-events:none;filter:grayscale(.4);}
120
128
  #zgs-body .zgp-in[readonly]{opacity:.8;cursor:default;background:rgba(127,127,127,.08);}
121
129
  #zgs-body .zgp-in{padding-left:12px;padding-right:12px;height:34px;box-sizing:border-box;}
122
130
  #zgs-body .zgp-in:focus{outline:none;border-color:rgba(120,160,255,.55);
@@ -251,6 +259,8 @@ module.exports = function attachSettingsPanel (win) {
251
259
 
252
260
  // ---------- 渲染 ----------
253
261
  async function render () {
262
+ // 页面重载后 <html> 上的 data-theme 有可能还没写上,这里兜一次
263
+ try { if (win.__CMARK_THEME && win.__CMARK_THEME.apply) win.__CMARK_THEME.apply() } catch (e) {}
254
264
  if (!G) {
255
265
  $('zgs-body').innerHTML = '<div class="zgp-sec"><h5>不可用</h5><div class="zgp-desc">Git 服务未加载</div></div>'
256
266
  return
@@ -268,109 +278,68 @@ module.exports = function attachSettingsPanel (win) {
268
278
 
269
279
  $('zgs-body').innerHTML = `
270
280
  <div class="zgp-sec">
271
- <h5>笔记目录</h5>
272
- <dl class="zgs-kv"><dt>当前目录</dt><dd class="mono" id="s-ws">${esc(win.__ZNOTES_WS || '-')}</dd></dl>
273
- <div class="zgp-row" style="margin-top:8px">
274
- <button class="zgp-b" id="s-ws-pick">更换目录</button>
275
- <button class="zgp-b" id="s-ws-open">在资源管理器打开</button>
281
+ <h5>外观</h5>
282
+ <div class="zgp-row" id="s-theme-row">
283
+ <button class="zgp-b" id="s-theme-light">浅色</button>
284
+ <button class="zgp-b" id="s-theme-dark">深色</button>
285
+ <button class="zgp-b" id="s-theme-system">跟随系统</button>
276
286
  </div>
277
- <div class="zgp-desc">更换后重新扫描该目录下的 Markdown 文件;文件本身不会被移动或修改。</div>
287
+ <div class="zgp-desc">主题对所有界面生效,选择后立即应用并记住。</div>
278
288
  </div>
279
289
  <div class="zgp-sec">
280
- <h5>版本控制</h5>
281
- <dl class="zgs-kv" id="s-vc-kv">
282
- <dt>状态</dt><dd><span class="zgs-dot off"></span>读取中…</dd>
290
+ <h5>工作区信息</h5>
291
+ <dl class="zgs-kv">
292
+ <dt>工作区目录</dt><dd class="mono" id="s-repo-dir">—</dd>
293
+ <dt>笔记目录</dt><dd class="mono" id="s-ws">—</dd>
294
+ <dt>附件目录</dt><dd class="mono" id="s-attach-dir">—</dd>
295
+ <dt>附件</dt><dd id="s-attach">统计中…</dd>
283
296
  </dl>
284
-
285
- <div id="s-vc-init" style="display:none">
286
- <div class="zgp-row" style="margin-top:14px">
287
- <button class="zgp-b primary" id="s-vc-init-btn">初始化本地仓库</button>
288
- <button class="zgp-b" id="s-vc-adopt-btn">从远程仓库加载</button>
289
- </div>
290
- <div class="zgp-desc">
291
- 初始化:在本地建立版本历史,之后可再关联远端。<br>
292
- 从远程加载:把远端已有的笔记拉到本地,要求工作区为空。
293
- </div>
294
- </div>
295
-
296
- <div id="s-vc-ready" style="display:none">
297
- <!-- 只有本机仓库、还没绑定远端时,只露一个入口,不先摆一个空地址框 -->
298
- <div id="s-vc-link-row" style="display:none">
299
- <div class="zgp-row" style="margin-top:14px">
300
- <button class="zgp-b primary" id="s-vc-link-btn">将本地仓库关联到远程</button>
301
- </div>
302
- </div>
303
- <!-- 已关联远端(或点了上面的按钮)才展开地址框 -->
304
- <div id="s-vc-remote-box" style="display:none">
305
- <div class="zgp-row" style="margin-top:12px">
306
- <button class="zgp-b danger" id="s-vc-remote-clear">移除远端</button>
307
- </div>
308
- </div>
297
+ <div class="zgp-row" style="margin-top:8px">
298
+ <button class="zgp-b" id="s-repo-pick">修改工作区</button>
299
+ <button class="zgp-b" id="s-notes-pick">选择笔记目录</button>
300
+ <button class="zgp-b" id="s-attach-clean" title="直接删除没有任何笔记引用的附件(不做回收站,误删可从 git 历史取回)">清理未引用附件</button>
309
301
  </div>
302
+ <div class="zgp-desc">默认在 <span class="mono">文档\CMark</span>;修改后重新扫描该目录下的 Markdown 文件,文件本身不会被移动或修改。</div>
310
303
  </div>
311
304
  <div class="zgp-sec">
312
- <h5>提交</h5>
313
- <div class="zgp-field">
314
- <div class="zgp-lab"><span>自定义模板</span>
315
- <label class="zgp-sw"><input type="checkbox" id="s-msg-custom"><i></i></label>
316
- </div>
317
- <div class="zgp-desc">关闭时使用默认模板与全局 git 身份;打开后可以分别自定义。</div>
305
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:10px">
306
+ <h5 style="margin:0">Git 集成</h5>
307
+ <label class="zgp-sw"><input type="checkbox" id="s-git-enabled"><i></i></label>
318
308
  </div>
319
- <div class="zgp-drawer" id="s-msg-drawer">
320
- <div class="zgp-field">
321
- <label class="zgs-lab" for="s-msg">提交信息模板</label>
322
- <input class="zgp-in" id="s-msg" value="${esc(cfg.commitMessage)}">
323
- <div class="zgp-desc">可用变量:{{date}} {{time}} {{datetime}} {{count}}</div>
309
+ <div class="zgp-desc" style="margin-top:6px">打开后可以提交历史、回溯版本,并同步到你自己的远程仓库。</div>
310
+ <div class="zgp-drawer" id="s-git-drawer">
311
+ <div id="s-git-body"></div>
312
+ <div class="zgs-sub" id="s-sec-auto">
313
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:10px">
314
+ <h5 style="margin:0">自动同步</h5>
315
+ <label class="zgp-sw"><input type="checkbox" id="s-auto" ${cfg.autoSync ? 'checked' : ''}><i></i></label>
324
316
  </div>
325
- <div class="zgp-field">
326
- <label class="zgs-lab" for="s-vc-name2">提交者姓名</label>
327
- <input class="zgp-in" id="s-vc-name2" placeholder="留空则使用全局 git 配置">
328
- <label class="zgs-lab" for="s-vc-email2" style="margin-top:10px">提交者邮箱</label>
329
- <input class="zgp-in" id="s-vc-email2" placeholder="留空则使用全局 git 配置">
330
- <div class="zgp-row" style="margin-top:10px">
331
- <button class="zgp-b" id="s-vc-id-save">保存提交者身份</button>
317
+ <div class="zgp-desc" style="margin:6px 0 10px">按下方策略执行;只在有改动时才产生提交。</div>
318
+ <div class="zgp-drawer${cfg.autoSync ? ' open' : ''}" id="s-auto-drawer">
319
+ <div class="zgp-field">
320
+ <div class="zgp-lab"><span>触发方式</span></div>
321
+ <select class="zgp-sel" id="s-mode">
322
+ <option value="interval"${mode === 'interval' ? ' selected' : ''}>固定间隔</option>
323
+ <option value="stop-edit"${mode === 'stop-edit' ? ' selected' : ''}>停止编辑后</option>
324
+ </select>
325
+ </div>
326
+ <div class="zgp-field" id="s-interval-row"${mode === 'interval' ? '' : ' style="display:none"'}>
327
+ <div class="zgp-lab"><span>间隔时长</span></div>
328
+ <select class="zgp-sel" id="s-interval">${ivHtml}</select>
329
+ <div class="zgp-desc">跨会话生效:即使插件没开够一个间隔,下次启动会补一次。</div>
330
+ </div>
331
+ <div class="zgp-field" id="s-stop-row"${mode === 'stop-edit' ? '' : ' style="display:none"'}>
332
+ <div class="zgp-lab"><span>静默时长</span></div>
333
+ <select class="zgp-sel" id="s-stop">${stopHtml}</select>
334
+ <div class="zgp-desc">停止输入 N 分钟后触发一次,适合边写边自动保存。</div>
332
335
  </div>
333
- <div class="zgp-desc" id="s-vc-id-hint2"></div>
334
- </div>
335
- </div>
336
- </div>
337
- <div class="zgp-sec">
338
- <h5>自动同步</h5>
339
- <div class="zgp-field">
340
- <div class="zgp-lab"><span>定时提交并推送</span>
341
- <label class="zgp-sw"><input type="checkbox" id="s-auto" ${cfg.autoSync ? 'checked' : ''}><i></i></label>
342
336
  </div>
343
- <div class="zgp-desc">按下方策略执行;只在有改动时才产生提交。</div>
344
337
  </div>
345
- <div class="zgp-drawer${cfg.autoSync ? ' open' : ''}" id="s-auto-drawer">
346
- <div class="zgp-field">
347
- <div class="zgp-lab"><span>触发方式</span></div>
348
- <select class="zgp-sel" id="s-mode">
349
- <option value="interval"${mode === 'interval' ? ' selected' : ''}>固定间隔</option>
350
- <option value="stop-edit"${mode === 'stop-edit' ? ' selected' : ''}>停止编辑后</option>
351
- </select>
352
- <div class="zgp-desc">两种方式互斥,只能选一种。</div>
353
- </div>
354
- <div class="zgp-field" id="s-interval-row"${mode === 'interval' ? '' : ' style="display:none"'}>
355
- <div class="zgp-lab"><span>间隔时长</span></div>
356
- <select class="zgp-sel" id="s-interval">${ivHtml}</select>
357
- <div class="zgp-desc">跨会话生效:即使插件没开够一个间隔,下次启动会补一次。</div>
358
- </div>
359
- <div class="zgp-field" id="s-stop-row"${mode === 'stop-edit' ? '' : ' style="display:none"'}>
360
- <div class="zgp-lab"><span>静默时长</span></div>
361
- <select class="zgp-sel" id="s-stop">${stopHtml}</select>
362
- <div class="zgp-desc">停止输入 N 分钟后触发一次,适合边写边自动保存。</div>
363
- </div>
364
338
  </div>
365
339
  </div>
366
340
  <div class="zgp-sec">
367
341
  <h5>运行状态</h5>
368
342
  <div class="zgp-desc" id="s-repo">读取中…</div>
369
- <div class="zgp-desc" id="s-info"></div>
370
- <div class="zgp-desc" id="s-attach" style="margin-top:8px">附件:统计中…</div>
371
- <div style="margin-top:8px">
372
- <button class="zgp-b" id="s-attach-clean" title="直接删除没有任何笔记引用的附件(不做回收站,误删可从 git 历史取回)">清理未引用附件</button>
373
- </div>
374
343
  </div>
375
344
  `
376
345
 
@@ -384,19 +353,23 @@ module.exports = function attachSettingsPanel (win) {
384
353
  if (v >= 1024) return (v / 1024).toFixed(0) + ' KB'
385
354
  return v + ' B'
386
355
  }
356
+ const attachDirEl = $('s-attach-dir')
387
357
  const refreshAttachStat = function () {
358
+ if (attachDirEl) {
359
+ const ws = win.__ZNOTES_WS || ''
360
+ attachDirEl.textContent = ws ? (ws.replace(/[\\/]+$/, '') + '\\.attachments') : '未设置笔记目录'
361
+ }
388
362
  if (!attachBox) return
389
363
  try {
390
364
  const api = win.__CMARK
391
365
  const s = api && api.scanAttachments ? api.scanAttachments() : null
392
366
  if (!s || !s.ok) {
393
- attachBox.textContent = '附件:读取失败' + (s && s.message ? '(' + s.message + ')' : '')
367
+ attachBox.textContent = '读取失败' + (s && s.message ? '(' + s.message + ')' : '')
394
368
  return
395
369
  }
396
370
  const total = Number(s.total) || 0
397
371
  const unused = (s.unused || []).length
398
- attachBox.textContent =
399
- '附件:' + total + ' 个,共 ' + fmtSize(s.allBytes) +
372
+ attachBox.textContent = total + ' 个,共 ' + fmtSize(s.allBytes) +
400
373
  ';未引用 ' + unused + ' 个(' + fmtSize(s.unusedBytes) + ')'
401
374
  } catch (e) {}
402
375
  }
@@ -425,267 +398,374 @@ module.exports = function attachSettingsPanel (win) {
425
398
  }
426
399
  }
427
400
 
428
- const pickBtn = $('s-ws-pick')
429
- if (pickBtn) {
430
- pickBtn.onclick = function () {
431
- try {
432
- if (win.__znotesPickDir) win.__znotesPickDir()
433
- else toast('更换目录功能不可用', 'err')
434
- } catch (e) {
435
- toast('打开目录选择失败:' + (e && e.message), 'err')
436
- }
401
+ // ---------- 外观:主题三态 ----------
402
+ const themeSvc = win.__CMARK_THEME
403
+ const curMode = (themeSvc && themeSvc.get && themeSvc.get().mode) || 'system'
404
+ const modeLabel = { light: '浅色', dark: '深色', system: '跟随系统' }
405
+ ;['light', 'dark', 'system'].forEach(function (m) {
406
+ const btn = $('s-theme-' + m)
407
+ if (!btn) return
408
+ if (m === curMode) btn.classList.add('primary')
409
+ btn.onclick = function () {
410
+ if (!themeSvc || !themeSvc.set) return toast('主题服务未就绪', 'err')
411
+ themeSvc.set(m)
412
+ ;['light', 'dark', 'system'].forEach(function (mm) {
413
+ const bb = $('s-theme-' + mm)
414
+ if (bb) bb.classList.toggle('primary', mm === m)
415
+ })
416
+ toast('已切换为' + modeLabel[m])
437
417
  }
438
- }
418
+ })
439
419
 
440
- const openWsBtn = $('s-ws-open')
441
- if (openWsBtn) {
442
- openWsBtn.onclick = function () {
443
- try {
444
- const p = win.__ZNOTES_WS
445
- if (!p) return
446
- if (win.ztools && typeof win.ztools.shellOpenPath === 'function') win.ztools.shellOpenPath(p)
447
- else if (win.ztools && typeof win.ztools.shellOpenExternal === 'function') win.ztools.shellOpenExternal('file://' + p)
448
- } catch (e) { toast('打开失败:' + (e && e.message), 'err') }
449
- }
450
- }
420
+ // ---------- Git 集成:开关 + 按目录状态渲染 ----------
421
+ const fs = require('fs')
422
+ const GIT_KEY = 'cmark-git-enabled'
423
+ const gitSwitch = $('s-git-enabled')
424
+ const gitDrawer = $('s-git-drawer')
425
+ const gitBody = $('s-git-body')
426
+ const secAuto = $('s-sec-auto')
451
427
 
452
- // ---- 版本控制(按仓库状态渲染两种形态)----
453
- async function refreshVc () {
454
- let st = {}
428
+ // 状态源:优先 preload 暴露的配置文件接口(file:// 下 localStorage 可能不可用)
429
+ const readGitFlag = function (fallback) {
455
430
  try {
456
- // inspect() 不含分支,用 state() 才能拿到 branch
457
- const r = await Promise.resolve(G && typeof G.state === 'function' ? G.state() : null)
458
- st = (r && r.ok && r.data) ? r.data : ((G && typeof G.inspect === 'function') ? G.inspect() : {})
431
+ const v = win.__CMARK_GIT && win.__CMARK_GIT.get ? win.__CMARK_GIT.get() : undefined
432
+ if (v === true || v === false) return v
459
433
  } catch (e) {}
460
- const isRepo = !!(st && st.isRepo)
461
- const initBox = $('s-vc-init')
462
- const readyBox = $('s-vc-ready')
463
- const kvEl = $('s-vc-kv')
464
- const autoSw = $('s-auto')
465
- if (autoSw) {
466
- autoSw.disabled = !isRepo
467
- if (!isRepo) autoSw.checked = false
468
- }
469
- // 这一段只回答"配置成什么样":启用没有、绑没绑远端。
470
- // 仓库根 / 分支 / 改动数属于运行诊断,统一放在面板底部的「运行状态」里,避免两处重复。
471
- let rdKv = {}
472
434
  try {
473
- const ri = await Promise.resolve(G && typeof G.remoteInfo === 'function' ? G.remoteInfo() : null)
474
- rdKv = (ri && ri.ok && ri.data) || {}
435
+ const v = win.localStorage.getItem(GIT_KEY)
436
+ if (v === '1') return true
437
+ if (v === '0') return false
475
438
  } catch (e) {}
476
- if (kvEl) {
477
- // 状态卡就是这一块的完整现状:状态 / 本地仓库路径 / 远程仓库地址
478
- kvEl.innerHTML = isRepo
479
- ? '<dt>状态</dt><dd><span class="zgs-dot"></span>已启用</dd>' +
480
- '<dt>本地仓库</dt><dd class="mono zgs-kv-url">' + esc(st.repoRoot || '-') + '</dd>' +
481
- '<dt>远程仓库</dt><dd' + (rdKv.hasRemote ? ' class="mono zgs-kv-url"' : '') + '>' +
482
- (rdKv.hasRemote ? esc(rdKv.url || '-') : '未绑定') + '</dd>'
483
- : '<dt>状态</dt><dd><span class="zgs-dot off"></span>未启用</dd>'
484
- }
485
- if (initBox) initBox.style.display = isRepo ? 'none' : ''
486
- if (readyBox) readyBox.style.display = isRepo ? '' : 'none'
487
-
488
- if (!isRepo) {
489
- // 预填全局身份:否则新机器首次提交会失败,initRepo 会回滚刚建的 .git
490
- let d = {}
491
- try {
492
- const id = await Promise.resolve(G && typeof G.identity === 'function' ? G.identity() : null)
493
- d = (id && id.ok && id.data) || {}
494
- } catch (e) {}
495
- // 提交者身份沿用全局 git 配置,未启用时不再单独给输入框(要改去「提交」分区)
496
- return
439
+ return fallback
440
+ }
441
+ const writeGitFlag = function (on) {
442
+ try {
443
+ if (win.__CMARK_GIT && win.__CMARK_GIT.set) { win.__CMARK_GIT.set(on); return } } catch (e) {}
444
+ try { win.localStorage.setItem(GIT_KEY, on ? '1' : '0') } catch (e) {}
445
+ try { win.dispatchEvent(new CustomEvent('cmark-git-changed', { detail: { enabled: !!on } })) } catch (e) {}
446
+ }
447
+ const setSectionsDisabled = function (disabled, reason) {
448
+ const secs = [secAuto]
449
+ for (let i = 0; i < secs.length; i++) {
450
+ const sec = secs[i]
451
+ if (!sec) continue
452
+ sec.classList.toggle('is-disabled', !!disabled)
453
+ // 置灰时把原因挂在容器上:disabled 的控件本身不弹 title,但祖先的 title 会生效
454
+ if (disabled) sec.setAttribute('title', reason || '请先配置 Git 仓库')
455
+ else sec.removeAttribute('title')
456
+ const nodes = sec.querySelectorAll('input,select,button')
457
+ for (let j = 0; j < nodes.length; j++) nodes[j].disabled = !!disabled
497
458
  }
459
+ }
460
+ const openWizard = function (p) {
461
+ if (win.__CMARK_OPEN_WIZARD) win.__CMARK_OPEN_WIZARD(p)
462
+ else toast('建仓向导未加载', 'err')
463
+ }
464
+ const afterGitChange = function () {
465
+ try { win.dispatchEvent(new CustomEvent('cmark-git-changed')) } catch (e) {}
466
+ refreshGitSection().catch(function () {})
467
+ }
498
468
 
499
- // 已初始化:只在本机 → 露「关联到远程」入口;已绑定 → 露「移除远端」
500
- const rd = rdKv
501
- const linkRow = $('s-vc-link-row')
502
- const remoteBox = $('s-vc-remote-box')
503
- if (linkRow) linkRow.style.display = rd.hasRemote ? 'none' : 'block'
504
- if (remoteBox) remoteBox.style.display = rd.hasRemote ? 'block' : 'none'
505
-
506
- // 「提交」分区抽屉里的身份回填:收起状态也先填好,展开就能看到当前值
507
- const n2 = $('s-vc-name2')
508
- const e2 = $('s-vc-email2')
509
- if (n2 || e2) {
510
- let d = {}
511
- try {
512
- const id = await Promise.resolve(G && typeof G.identity === 'function' ? G.identity() : null)
513
- d = (id && id.ok && id.data) || {}
514
- } catch (e) {}
515
- if (n2 && !n2.value) n2.value = d.localName || ''
516
- if (e2 && !e2.value) e2.value = d.localEmail || ''
517
- const hint = $('s-vc-id-hint2')
518
- if (hint) {
519
- hint.textContent = (d.localName || d.localEmail)
520
- ? '当前使用仓库内的身份,覆盖全局配置。'
521
- : '当前沿用全局 git 配置:' + ([d.globalName, d.globalEmail].filter(Boolean).join(' / ') || '未配置')
522
- }
523
- }
469
+ // 认证失败时在抽屉顶部插入提示条,并给一个直接重新授权的入口
470
+ const showAuthError = function (msg) {
471
+ if (!gitBody) return
472
+ const old = gitBody.querySelector('.zgs-auth-err')
473
+ if (old) old.remove()
474
+ let prov = 'github'
475
+ try {
476
+ const cur = (win.__CMARK_REMOTE && win.__CMARK_REMOTE.current) ? win.__CMARK_REMOTE.current() : null
477
+ if (cur && cur.provider) prov = cur.provider
478
+ } catch (e) {}
479
+ const box = doc.createElement('div')
480
+ box.className = 'zgs-auth-err'
481
+ box.innerHTML = '授权可能已失效,推送/拉取被拒绝:' + esc(String(msg || '').slice(0, 180)) +
482
+ '<div class="zgp-row" style="margin-top:8px"><button class="zgp-b primary" id="s-git-reauth">重新授权</button></div>'
483
+ gitBody.insertBefore(box, gitBody.firstChild)
484
+ const rb = box.querySelector('#s-git-reauth')
485
+ if (rb) rb.onclick = function () { openWizard(prov) }
486
+ toast('授权可能已失效,可点「重新授权」', 'err')
524
487
  }
525
488
 
526
- // 渲染完成后拉一次版本控制状态,填充状态卡并按仓库状态切换两块形态
527
- // (这行之前被误删,导致面板一直停在"读取中…")
528
- refreshVc().catch(function () {})
529
-
530
- const initBtn = $('s-vc-init-btn')
531
- if (initBtn) {
532
- initBtn.onclick = async function () {
533
- const branch = 'main' // 分支固定 main
534
- const ok = await zgsConfirm(
535
- '在\n' + (win.__ZNOTES_WS || '-') + '\n初始化本地 git 仓库?\n\n' +
536
- '现有笔记会被提交一次,内容不会被修改。\n' +
537
- '之后可以在下面把本地仓库关联到远端。', '初始化')
538
- if (!ok) return
539
- initBtn.disabled = true
540
- try { if (win.__znotesFlushSave) win.__znotesFlushSave() } catch (e) {}
541
- let r = null
489
+ // 本机 git 可用性:首次检测后缓存,装完 git 可点「重新检测」刷新
490
+ let gitProbe = null
491
+ // 每次打开面板只做一次「未初始化则关开关」的校正;否则用户点开关会被立刻打回
492
+ let gitGateDone = false
493
+ const AUTH_RE = /Authentication failed|could not read Username|could not read Password|Permission denied|Authentication required|Invalid username or password|returned error: 403|403 Forbidden|support for password authentication/i
494
+ const isAuthError = function (msg) { return AUTH_RE.test(String(msg || '')) }
495
+
496
+ async function refreshGitSection () {
497
+ if (!gitBody) return
498
+
499
+ if (gitProbe === null) {
542
500
  try {
543
- r = await G.initRepo({
544
- mode: 'local',
545
- branch: branch
546
- })
547
- } catch (e) { r = { ok: false, message: (e && e.message) || String(e) } }
548
- initBtn.disabled = false
549
- if (r && r.ok) {
550
- toast('仓库已初始化', 'ok')
551
- try { if (G.rediscover) G.rediscover() } catch (e) {}
552
- try { if (win.__zgpRefresh) win.__zgpRefresh() } catch (e) {}
553
- render()
554
- } else {
555
- toast('初始化失败:' + ((r && r.message) || '未知原因'), 'err')
501
+ const cp = require('child_process')
502
+ const out = cp.execFileSync('git', ['--version'], { encoding: 'utf8', timeout: 6000, windowsHide: true })
503
+ gitProbe = { ok: true, ver: String(out || '').trim() }
504
+ } catch (e) { gitProbe = { ok: false, ver: '' } }
505
+ }
506
+ if (!gitProbe.ok) {
507
+ if (gitSwitch) { gitSwitch.checked = false; gitSwitch.disabled = true }
508
+ if (gitDrawer) gitDrawer.classList.add('open')
509
+ setSectionsDisabled(true, '请先安装 Git')
510
+ gitBody.innerHTML =
511
+ '<div class="zgs-auth-err">没检测到本机安装 Git,无法使用 Git 集成。</div>' +
512
+ '<div class="zgp-desc">装好 Git for Windows 后点「重新检测」;如果装完仍提示未安装,重启一次 ZTools 让 PATH 生效。</div>' +
513
+ '<div class="zgp-row" style="margin-top:10px">' +
514
+ '<button class="zgp-b primary" data-git="redetect">重新检测</button>' +
515
+ '<button class="zgp-b" data-git="dlgit">打开下载页</button></div>'
516
+ const rd = gitBody.querySelector('[data-git="redetect"]')
517
+ if (rd) rd.onclick = function () { gitProbe = null; refreshGitSection().catch(function () {}) }
518
+ const dl = gitBody.querySelector('[data-git="dlgit"]')
519
+ if (dl) dl.onclick = async function () {
520
+ const ok = await zgsConfirm('即将打开外部窗口,CMark 窗口可能会被隐藏。装完 Git 后重新呼出 CMark 可继续设置。', '继续打开')
521
+ if (ok) { try { win.ztools.shellOpenExternal('https://git-scm.com/download/win') } catch (e) {} }
556
522
  }
523
+ return
557
524
  }
558
- }
525
+ if (gitSwitch) gitSwitch.disabled = false
526
+
527
+ const insp = (G && G.inspect) ? G.inspect() : { isRepo: false }
528
+ let data = {}
529
+ try { const st = await G.state(); data = (st && st.data) || {} } catch (e) {}
530
+ let remote = { hasRemote: false, url: '' }
531
+ try { const r = await G.remoteInfo(); if (r && r.ok) remote = r.data } catch (e) {}
532
+ const ws = win.__ZNOTES_WS || ''
533
+ let count = 0
534
+ try {
535
+ if (ws && fs.existsSync(ws)) {
536
+ count = fs.readdirSync(ws).filter(function (n) {
537
+ return ['.git', '.cmark', '.attachments', 'backup'].indexOf(n) < 0
538
+ }).length
539
+ }
540
+ } catch (e) {}
559
541
 
560
- // 从远程仓库加载:弹窗填地址 → 校验连通性 + 空目录 → 通过才拉取
561
- const adoptBtn = $('s-vc-adopt-btn')
562
- if (adoptBtn) {
563
- adoptBtn.onclick = async function () {
564
- const url = await zgsPrompt(
565
- '从远程仓库加载\n\n输入远端仓库地址。点「检测并继续」会先确认仓库可访问、当前工作区为空,通过后才开始拉取。',
566
- 'https://gitee.com/账号/仓库.git', '',
567
- async function (v) {
568
- const chk = await Promise.resolve(
569
- G && typeof G.checkRemote === 'function' ? G.checkRemote(v) : { ok: true })
570
- if (!chk || !chk.ok) {
571
- return { ok: false, message: '仓库地址不可用:' + ((chk && chk.message) || '无法连接') }
542
+ let enabled = readGitFlag(null)
543
+ if (enabled === null) { enabled = !!insp.isRepo; writeGitFlag(enabled) }
544
+ // 每次打开面板后的首次渲染校正一次:目录还不是仓库却开着开关 → 落盘为关闭,
545
+ // 这样「打开开关后没继续建仓就关面板」下次进来仍是关闭。
546
+ if (!gitGateDone) {
547
+ gitGateDone = true
548
+ if (!insp.isRepo && enabled) { enabled = false; writeGitFlag(false) }
549
+ }
550
+ if (gitSwitch) gitSwitch.checked = enabled
551
+ if (gitDrawer) gitDrawer.classList.toggle('open', enabled)
552
+ setSectionsDisabled(
553
+ !enabled || !insp.isRepo,
554
+ !enabled ? '请先开启 Git 集成' : '请先配置 Git 仓库'
555
+ )
556
+
557
+ const branch = data.branch || 'main'
558
+ const ahead = data.ahead || 0
559
+ const behind = data.behind || 0
560
+ const dirty = typeof data.dirty === 'number' ? data.dirty : -1
561
+
562
+ let kind = 'empty'
563
+ if (insp.isRepo && remote.hasRemote) kind = 'remote'
564
+ else if (insp.isRepo) kind = 'local'
565
+ else if (count > 0) kind = 'files'
566
+
567
+ let html = ''
568
+ if (kind === 'empty') {
569
+ html = '<div class="zgp-desc">目录是空的,建一个仓库就能开始记。</div>' +
570
+ '<div class="zgp-row" style="margin-top:10px">' +
571
+ '<button class="zgp-b primary" data-git="gh">使用 GitHub 作为远程仓库</button>' +
572
+ '<button class="zgp-b" data-git="gt">使用 Gitee 作为远程仓库</button>' +
573
+ '<button class="zgp-b" data-git="adopt">从远程仓库加载</button>' +
574
+ '<button class="zgp-b" data-git="local">仅本地仓库</button></div>'
575
+ } else if (kind === 'files') {
576
+ html = '<div class="zgp-desc">目录里有 ' + count + ' 项内容。初始化后这些文件成为第一个提交,内容不会被改动。</div>' +
577
+ '<div class="zgp-row" style="margin-top:10px">' +
578
+ '<button class="zgp-b primary" data-git="gh">使用 GitHub 作为远程仓库</button>' +
579
+ '<button class="zgp-b" data-git="gt">使用 Gitee 作为远程仓库</button>' +
580
+ '<button class="zgp-b" data-git="local">仅本地仓库</button></div>'
581
+ } else if (kind === 'local') {
582
+ html = '<dl class="zgs-kv"><dt>仓库</dt><dd>本地仓库</dd>' +
583
+ '<dt>分支</dt><dd>' + esc(branch) + '</dd>' +
584
+ '<dt>改动</dt><dd>' + (dirty >= 0 ? dirty + ' 个文件' : '读取失败') + '</dd></dl>' +
585
+ '<div class="zgp-desc">还没有远程仓库,笔记只存在这台机器上。</div>' +
586
+ '<div class="zgp-row" style="margin-top:10px">' +
587
+ '<button class="zgp-b primary" data-git="gh">使用 GitHub 作为远程仓库</button>' +
588
+ '<button class="zgp-b" data-git="gt">使用 Gitee 作为远程仓库</button>' +
589
+ '<button class="zgp-b" data-git="manual">手动填远程地址</button></div>'
590
+ } else {
591
+ html = '<dl class="zgs-kv"><dt>远程</dt><dd class="mono">' + esc(remote.url || '') + '</dd>' +
592
+ '<dt>分支</dt><dd>' + esc(branch) + '</dd>' +
593
+ '<dt>状态</dt><dd>' + (dirty >= 0 ? dirty + ' 处改动' : '读取失败') +
594
+ (ahead ? ' · 待推送 ' + ahead : '') + (behind ? ' · 待拉取 ' + behind : '') + '</dd></dl>' +
595
+ '<div class="zgp-row" style="margin-top:10px">' +
596
+ '<button class="zgp-b" data-git="push">推送</button>' +
597
+ '<button class="zgp-b" data-git="pull">拉取</button>' +
598
+ '<button class="zgp-b danger" data-git="unbind">移除远端</button>' +
599
+ '<button class="zgp-b" data-git="clear-token">清除已保存凭据</button></div>'
600
+ }
601
+ gitBody.innerHTML = html
602
+
603
+ const btns = gitBody.querySelectorAll('[data-git]')
604
+ for (let i = 0; i < btns.length; i++) {
605
+ const b = btns[i]
606
+ b.onclick = async function () {
607
+ const act = b.getAttribute('data-git')
608
+ if (act === 'gh') return openWizard('github')
609
+ if (act === 'gt') return openWizard('gitee')
610
+ if (act === 'adopt') return loadFromRemote()
611
+ if (act === 'local') {
612
+ b.disabled = true
613
+ const r = await G.initRepo({ mode: 'local' })
614
+ b.disabled = false
615
+ if (r && r.ok) { toast('已创建本地仓库'); afterGitChange() } else toast((r && r.message) || '初始化失败', 'err')
616
+ return
617
+ }
618
+ if (act === 'manual') {
619
+ const bindRemote = async function (u) {
620
+ const r = await G.initRepo({ mode: 'remote', remoteUrl: u, push: true })
621
+ if (r && r.ok) { toast('已绑定远程仓库'); afterGitChange(); return { ok: true } }
622
+ return { ok: false, message: (r && r.message) || '绑定失败' }
572
623
  }
573
- const pre = G && typeof G.adoptPrecheck === 'function' ? G.adoptPrecheck() : null
574
- if (pre && !pre.ok) {
575
- const items = (pre.blocking && pre.blocking.length)
576
- ? ':' + pre.blocking.slice(0, 3).join('、') + (pre.blocking.length > 3 ? ' 等' : '')
577
- : ''
578
- return { ok: false, message: '加载需要空工作区。' + (pre.reason || '') + items }
624
+ // 第二步:需要授权时让用户贴令牌,存进本工作区后再探测并绑定
625
+ const askToken = function (u, hint) {
626
+ setTimeout(function () {
627
+ zgsPrompt(
628
+ '这个仓库需要授权访问\n把访问令牌粘贴到下面(GitHub PAT / Gitee 私人令牌 / 私有 GitLab token)',
629
+ '粘贴访问令牌(GitHub PAT / Gitee 私人令牌 / 私有 GitLab token)', '',
630
+ async function (tk) {
631
+ const t = String(tk || '').trim()
632
+ if (!t) return { ok: false, message: '请填写访问令牌' }
633
+ const saved = G.setRemoteToken(t, u)
634
+ if (saved && saved.ok === false) return { ok: false, message: saved.message || '保存令牌失败' }
635
+ const p2 = await G.probeRemote(u, t)
636
+ if (!p2 || !p2.ok) {
637
+ return { ok: false, message: '令牌无效或没有写权限:' + ((p2 && p2.message) || '未知原因') }
638
+ }
639
+ const r = await bindRemote(u)
640
+ if (r.ok) toast('已绑定远程仓库(凭据已保存在本机)')
641
+ return r
642
+ }
643
+ )
644
+ }, 140)
645
+ return { ok: true }
579
646
  }
580
- return { ok: true }
581
- })
582
- if (!url) return
583
- toast('正在从远端拉取…', 'info')
584
- let r = null
585
- try {
586
- r = await Promise.resolve(G.adoptRepo({ remoteUrl: url, branch: 'main' }))
587
- } catch (e) {
588
- r = { ok: false, message: (e && e.message) || String(e) }
589
- }
590
- if (r && r.ok) {
591
- toast('已从远端加载完成', 'ok')
592
- try { if (G.rediscover) G.rediscover() } catch (e) {}
593
- try { if (win.__zgpRefresh) win.__zgpRefresh() } catch (e) {}
594
- render()
595
- setTimeout(function () {
596
- try { if (win.__ZNOTES_REFRESH) win.__ZNOTES_REFRESH() } catch (e) {}
597
- }, 600)
598
- } else {
599
- toast('加载失败:' + ((r && r.message) || '未知原因'), 'err')
647
+ zgsPrompt('填写远程仓库地址', 'https://github.com/you/repo.git', '', async function (url) {
648
+ const u = String(url || '').trim()
649
+ if (!u) return { ok: false, message: '请填写远程仓库地址' }
650
+ // 先免授权探一下:公开仓库直接过,私有仓库会返回 auth 类错误
651
+ const probe = await G.probeRemote(u)
652
+ if (probe && probe.ok) return bindRemote(u)
653
+ const msg = String((probe && probe.message) || '')
654
+ // 注意:GitHub/Gitee 对「私有仓库无权限」和「仓库不存在」都返回 not found,
655
+ // 所以这里把 not found 也算进去 —— 否则私有仓库会直接走到绑定、只丢一句英文报错。
656
+ if (/auth|permission|denied|403|401|username|password|credentials|not found|does not exist/i.test(msg)) {
657
+ // 需要授权:关掉地址输入,接着弹令牌输入
658
+ askToken(u)
659
+ return { ok: true }
660
+ }
661
+ return bindRemote(u)
662
+ })
663
+ return
664
+ }
665
+ if (act === 'push' || act === 'pull') {
666
+ b.disabled = true
667
+ const r = act === 'push' ? await G.push() : await G.pull()
668
+ b.disabled = false
669
+ if (r && r.ok) { toast(act === 'push' ? '推送完成' : '拉取完成'); return }
670
+ const failMsg = (r && r.message) || (act === 'push' ? '推送失败' : '拉取失败')
671
+ if (isAuthError(failMsg)) { showAuthError(failMsg); return }
672
+ toast(failMsg, 'err')
673
+ return
674
+ }
675
+ if (act === 'clear-token') {
676
+ const ok = await zgsConfirm('清除本机保存的授权凭据后,推送需要重新授权;远端绑定关系保留。', '清除')
677
+ if (!ok) return
678
+ try { if (win.__CMARK_REMOTE && win.__CMARK_REMOTE.clear) win.__CMARK_REMOTE.clear() } catch (e) {}
679
+ toast('已清除本机保存的授权凭据')
680
+ afterGitChange()
681
+ return
682
+ }
683
+ if (act === 'unbind') {
684
+ const ok = await zgsConfirm('移除远端绑定后,笔记只保留在本机。', '移除')
685
+ if (!ok) return
686
+ const r = await G.setRemote('')
687
+ if (r && r.ok) { toast('已移除远端'); afterGitChange() } else toast((r && r.message) || '移除失败', 'err')
688
+ }
600
689
  }
601
690
  }
602
691
  }
603
692
 
604
- // 「将本地仓库关联到远程」:弹窗填地址 → 校验通过才关窗并写入 remote
605
- const linkBtn = $('s-vc-link-btn')
606
- if (linkBtn) {
607
- linkBtn.onclick = async function () {
608
- let remoteBranches = null
609
- const url = await zgsPrompt(
610
- '将本地仓库关联到远程\n\n输入远端仓库地址。点「检测并继续」会先确认仓库可访问,通过后才写入配置。',
611
- 'https://gitee.com/账号/仓库.git', '',
612
- async function (v) {
613
- const chk = await Promise.resolve(
614
- G && typeof G.checkRemote === 'function' ? G.checkRemote(v) : { ok: true })
615
- if (!chk || !chk.ok) {
616
- return { ok: false, message: '仓库地址不可用:' + ((chk && chk.message) || '无法连接') }
617
- }
618
- remoteBranches = (chk.branches || [])
619
- return { ok: true }
620
- })
621
- if (!url) return
622
- let r = null
623
- try { r = await Promise.resolve(G.setRemote(url)) } catch (e) { r = { ok: false, message: (e && e.message) || '' } }
624
- if (!r || !r.ok) {
625
- toast('关联失败:' + ((r && r.message) || '未知原因'), 'err')
626
- return
627
- }
628
- try { if (win.__zgpRefresh) win.__zgpRefresh() } catch (e) {}
629
- // 远端是空仓库:光写 remote 等于什么也没同步,顺手把本地历史推上去,
630
- // 否则用户会以为"关联成功"但远端正本还是空的。
631
- if (remoteBranches && remoteBranches.length === 0) {
632
- toast('远端是空仓库,正在推送本地历史…', 'info')
633
- let pr = null
634
- try { pr = await Promise.resolve(G.push()) } catch (e) { pr = { ok: false, message: (e && e.message) || '' } }
635
- if (pr && pr.ok) toast('已关联到远程并推送完成', 'ok')
636
- else toast('已关联,但推送失败:' + ((pr && pr.message) || '未知原因'), 'err')
637
- } else {
638
- toast('已关联到远程', 'ok')
639
- }
640
- render()
693
+ if (gitSwitch) {
694
+ gitSwitch.onchange = function () {
695
+ writeGitFlag(!!gitSwitch.checked)
696
+ if (gitDrawer) gitDrawer.classList.toggle('open', !!gitSwitch.checked)
697
+ setSectionsDisabled(!gitSwitch.checked)
698
+ // 通知侧边栏操作栏与编辑区云端按钮同步置灰/隐藏
699
+ try { win.dispatchEvent(new CustomEvent('cmark-git-changed')) } catch (e) {}
700
+ refreshGitSection().catch(function () {})
641
701
  }
642
702
  }
703
+ refreshGitSection().catch(function () {})
704
+
705
+ // ---------- 工作区(仓库根)/ 笔记目录 ----------
706
+ const fillDirs = function () {
707
+ const api = win.__CMARK || {}
708
+ const ws = win.__ZNOTES_WS || ''
709
+ let root = ''
710
+ try { root = api.repoDir ? api.repoDir() : '' } catch (e) {}
711
+ if (!root) root = ws
712
+ const rootEl = $('s-repo-dir')
713
+ if (rootEl) rootEl.textContent = root || '未设置'
714
+ const wsEl = $('s-ws')
715
+ if (wsEl) wsEl.textContent = ws || '未设置'
716
+ }
717
+ fillDirs()
643
718
 
644
- const saveRemote = async function (url, btn) {
645
- if (!G || typeof G.setRemote !== 'function') { toast('git 服务不可用', 'err'); return }
646
- let old = ''
647
- try {
648
- const ri = await Promise.resolve(G.remoteInfo())
649
- if (ri && ri.ok && ri.data && ri.data.hasRemote) old = ri.data.url
650
- } catch (e) {}
651
- if (url && old && url !== old) {
652
- const ok = await zgsConfirm('把远端地址\n\n' + old + '\n\n改为\n\n' + url + '\n\n之后推送会进入新仓库。', '修改')
653
- if (!ok) return
654
- }
655
- if (!url) {
656
- const ok = await zgsConfirm('移除远端后,笔记只在本机提交,不再上传。', '移除')
657
- if (!ok) return
658
- }
659
- if (btn) btn.disabled = true
719
+ const repoNames = { pick: '修改工作区', notes: '选择笔记目录' }
720
+ const runDirPick = function (kind) {
721
+ const api = win.__CMARK || {}
722
+ const fn = kind === 'pick' ? api.pickRepoDir : api.pickNotesDir
723
+ if (typeof fn !== 'function') return toast(repoNames[kind] + '功能不可用', 'err')
660
724
  let r = null
661
- try { r = await G.setRemote(url) } catch (e) { r = { ok: false, message: (e && e.message) || '' } }
662
- if (btn) btn.disabled = false
663
- if (r && r.ok) {
664
- toast(url ? '远程地址已保存' : '已移除远端', 'ok')
665
- // 状态卡要跟着变(已绑定 ↔ 未绑定),否则界面上还是旧地址
666
- render()
667
- refreshVc().catch(function () {}) // 局部刷新,避免整块重渲染冲掉其它输入
668
- } else {
669
- toast('操作失败:' + ((r && r.message) || '未知原因'), 'err')
670
- }
725
+ try { r = fn.call(api) } catch (e) { return toast('打开目录选择失败:' + (e && e.message), 'err') }
726
+ if (!r || r.canceled) return
727
+ if (!r.ok) return toast(r.message || '操作失败', 'err')
728
+ toast(kind === 'pick' ? '工作区已切换' : '笔记目录已切换')
729
+ if (r.needReload) setTimeout(function () { try { win.location.reload() } catch (e) {} }, 400)
671
730
  }
731
+ const repoBtn = $('s-repo-pick')
732
+ if (repoBtn) repoBtn.onclick = function () { runDirPick('pick') }
733
+ const notesBtn = $('s-notes-pick')
734
+ if (notesBtn) notesBtn.onclick = function () { runDirPick('notes') }
735
+
736
+
737
+ // 从远程仓库加载(要求空工作区):校验连通性 + 目录状态后才拉取
738
+ async function loadFromRemote () {
739
+ const url = await zgsPrompt(
740
+ '从远程仓库加载\n\n输入远端仓库地址。点「检测并继续」会先确认仓库可访问、当前工作区为空,通过后才开始拉取。',
741
+ 'https://gitee.com/账号/仓库.git', '',
742
+ async function (v) {
743
+ const chk = await Promise.resolve(G && typeof G.checkRemote === 'function' ? G.checkRemote(v) : { ok: true })
744
+ if (!chk || !chk.ok) return { ok: false, message: '仓库地址不可用:' + ((chk && chk.message) || '无法连接') }
745
+ const pre = G && typeof G.adoptPrecheck === 'function' ? G.adoptPrecheck() : null
746
+ if (pre && !pre.ok) {
747
+ const items = (pre.blocking && pre.blocking.length)
748
+ ? ':' + pre.blocking.slice(0, 3).join('、') + (pre.blocking.length > 3 ? ' 等' : '')
749
+ : ''
750
+ return { ok: false, message: '加载需要空工作区。' + (pre.reason || '') + items }
751
+ }
752
+ return { ok: true }
753
+ })
754
+ if (!url) return
755
+ toast('正在从远端拉取…', 'info')
756
+ let r = null
757
+ try { r = await Promise.resolve(G.adoptRepo({ remoteUrl: url, branch: 'main' })) } catch (e) { r = { ok: false, message: (e && e.message) || String(e) } }
758
+ if (r && r.ok) {
759
+ toast('已从远端加载完成', 'ok')
760
+ try { if (G.rediscover) G.rediscover() } catch (e) {}
761
+ refreshGitSection().catch(function () {})
672
762
 
673
- const remoteClearBtn = $('s-vc-remote-clear')
674
- if (remoteClearBtn) {
675
- remoteClearBtn.onclick = function () { saveRemote('', remoteClearBtn) }
676
- }
763
+ // 暴露给外部一次性监听器:每次 render 更新引用,避免重复注册 addEventListener
764
+ win.__zgsRefreshGitSection = refreshGitSection
677
765
 
678
- const idSaveBtn = $('s-vc-id-save')
679
- if (idSaveBtn) {
680
- idSaveBtn.onclick = async function () {
681
- const n = (($('s-vc-name2') || {}).value || '').trim()
682
- const e = (($('s-vc-email2') || {}).value || '').trim()
683
- idSaveBtn.disabled = true
684
- let r = null
685
- try { r = await G.setIdentity(n, e) } catch (err) { r = { ok: false, message: (err && err.message) || '' } }
686
- idSaveBtn.disabled = false
687
- if (r && r.ok) toast('身份已保存', 'ok')
688
- else toast('保存失败:' + ((r && r.message) || '未知原因'), 'err')
766
+ setTimeout(function () { try { if (win.__ZNOTES_REFRESH) win.__ZNOTES_REFRESH() } catch (e) {} }, 600)
767
+ } else {
768
+ toast('加载失败:' + ((r && r.message) || '未知原因'), 'err')
689
769
  }
690
770
  }
691
771
 
@@ -694,30 +774,11 @@ module.exports = function attachSettingsPanel (win) {
694
774
  autoSync: $('s-auto').checked,
695
775
  autoMode: $('s-mode') ? $('s-mode').value : 'interval',
696
776
  intervalMs: parseInt($('s-interval').value, 10),
697
- onStopEditMinutes: parseInt($('s-stop').value, 10),
698
- commitMessage: $('s-msg').value
777
+ onStopEditMinutes: parseInt($('s-stop').value, 10)
699
778
  })
700
779
  toast('设置已保存', 'ok')
701
780
  }
702
781
 
703
- // 自定义提交信息:开关控制抽屉展开,并决定用默认模板还是自定义模板
704
- const DEFAULT_MSG = 'notes: 同步 {{datetime}}'
705
- const msgCustom = $('s-msg-custom')
706
- const msgDrawer = $('s-msg-drawer')
707
- const msgInput = $('s-msg')
708
- if (msgCustom && msgDrawer) {
709
- const isCustom = !!(cfg.commitMessage && cfg.commitMessage !== DEFAULT_MSG)
710
- msgCustom.checked = isCustom
711
- msgDrawer.classList.toggle('open', isCustom)
712
- if (!isCustom && msgInput && !msgInput.value) msgInput.value = DEFAULT_MSG
713
- msgCustom.onchange = function () {
714
- const on = msgCustom.checked
715
- msgDrawer.classList.toggle('open', on)
716
- if (!on && msgInput) msgInput.value = DEFAULT_MSG
717
- save()
718
- }
719
- }
720
-
721
782
  const autoSw = $('s-auto')
722
783
  if (autoSw) {
723
784
  autoSw.onchange = function () {
@@ -741,7 +802,6 @@ module.exports = function attachSettingsPanel (win) {
741
802
  const el = $(id)
742
803
  if (el) el.onchange = save
743
804
  })
744
- if ($('s-msg')) $('s-msg').onblur = save
745
805
 
746
806
  // 仓库 / 状态信息(异步)
747
807
  let st = {}
@@ -751,34 +811,79 @@ module.exports = function attachSettingsPanel (win) {
751
811
  } catch (e) {}
752
812
  const repoEl = $('s-repo')
753
813
  if (repoEl) {
754
- // 状态也用键值对齐,与上方版本控制保持一致
814
+ const inspNow = (G && G.inspect) ? G.inspect() : { isRepo: false }
815
+ let remoteNow = { hasRemote: false, url: '' }
816
+ try { const rr = await G.remoteInfo(); if (rr && rr.ok) remoteNow = rr.data } catch (e) {}
817
+ let lastRun = 0
818
+ try { const cc = G.config(); if (cc && cc.ok) lastRun = cc.data.lastRunAt || 0 } catch (e) {}
819
+ const fmtTime = function (t) {
820
+ if (!t) return ''
821
+ try {
822
+ const diff = Date.now() - t
823
+ if (diff < 0) return ''
824
+ if (diff < 60000) return '刚刚'
825
+ if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前'
826
+ if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前'
827
+ const d = new Date(t)
828
+ const p2 = function (n) { return n < 10 ? '0' + n : String(n) }
829
+ return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate())
830
+ } catch (e) { return '' }
831
+ }
832
+ const c2 = (G.config() || {}).data || {}
755
833
  let rows = ''
834
+ rows += '<dt>本地仓库</dt><dd>' + (inspNow.isRepo ? '已绑定' + (st.branch ? ' · ' + esc(st.branch) : '') : '未绑定') + '</dd>'
835
+ rows += '<dt>远程仓库</dt><dd>' + (remoteNow.hasRemote ? '已绑定' : '未绑定') + '</dd>'
836
+ if (inspNow.isRepo) {
837
+ rows += '<dt>上次同步</dt><dd>' + (c2.lastRunAt ? new Date(c2.lastRunAt).toLocaleString('zh-CN') : '从未执行') + '</dd>'
838
+ if (c2.lastAutoResult) {
839
+ const ar = c2.lastAutoResult
840
+ rows += '<dt>最近结果</dt><dd>' + (ar.ok ? '成功' : '失败') + (ar.message ? ' · ' + esc(ar.message) : '') + '</dd>'
841
+ if (ar.error) rows += '<dt>错误</dt><dd>' + esc(ar.error) + '</dd>'
842
+ }
843
+ }
844
+ if (typeof st.dirty === 'number') rows += '<dt>未提交</dt><dd>' + st.dirty + ' 个文件</dd>'
756
845
  if (G.scopeRel) rows += '<dt>笔记子目录</dt><dd class="mono">' + esc(G.scopeRel) + '</dd>'
757
- if (st.branch) rows += '<dt>当前分支</dt><dd class="mono">' + esc(st.branch) + '</dd>'
758
- if (typeof st.dirty === 'number') rows += '<dt>未提交改动</dt><dd>' + st.dirty + ' 个文件</dd>'
759
846
  repoEl.innerHTML = '<dl class="zgs-kv">' + rows + '</dl>'
760
847
  }
761
- const c2 = (G.config() || {}).data || {}
762
- let irows = '<dt>上次同步</dt><dd>' + (c2.lastRunAt ? new Date(c2.lastRunAt).toLocaleString('zh-CN') : '从未执行') + '</dd>'
763
- if (c2.lastAutoResult) {
764
- const ar = c2.lastAutoResult
765
- irows += '<dt>最近结果</dt><dd>' + (ar.ok ? '成功' : '失败') + (ar.message ? ' · ' + esc(ar.message) : '') + '</dd>'
766
- if (ar.error) irows += '<dt>错误</dt><dd>' + esc(ar.error) + '</dd>'
767
- if (ar.at) irows += '<dt>发生时间</dt><dd>' + new Date(ar.at).toLocaleString('zh-CN') + '</dd>'
768
- }
769
- const infoEl = $('s-info')
770
- if (infoEl) infoEl.innerHTML = '<dl class="zgs-kv">' + irows + '</dl>'
771
848
  }
772
849
 
773
850
  // ---------- 开关 ----------
851
+ // ZTools 的窗口策略是失焦即隐藏:打开资源管理器 / 外部授权页 / 原生目录选择框之后,
852
+ // 插件窗口会被宿主收起来。这里用 sessionStorage 记住面板是否开着,
853
+ // 窗口重新可见(或插件被重新呼出)时自动恢复,用户不用每次都重新点一遍。
854
+ const OPEN_KEY = 'cmark-settings-open'
855
+ function readOpenFlag () {
856
+ try { return win.sessionStorage.getItem(OPEN_KEY) === '1' } catch (e) { return false }
857
+ }
858
+ function writeOpenFlag (on) {
859
+ try { win.sessionStorage.setItem(OPEN_KEY, on ? '1' : '0') } catch (e) {}
860
+ }
861
+
774
862
  function open () {
775
863
  if (root.classList.contains('open')) return
776
864
  root.classList.add('open')
865
+ writeOpenFlag(true)
777
866
  render()
778
867
  }
779
868
  function close () {
780
869
  root.classList.remove('open')
870
+ writeOpenFlag(false)
781
871
  }
872
+
873
+ // 窗口从隐藏变回可见时恢复面板
874
+ try {
875
+ doc.addEventListener('visibilitychange', function () {
876
+ if (doc.visibilityState === 'visible' && readOpenFlag() && !root.classList.contains('open')) open()
877
+ })
878
+ } catch (e) {}
879
+ // 补一条:宿主重新进入插件时也恢复(部分场景不触发 visibilitychange)
880
+ try {
881
+ if (win.ztools && typeof win.ztools.onPluginEnter === 'function') {
882
+ win.ztools.onPluginEnter(function () { if (readOpenFlag()) open() })
883
+ }
884
+ } catch (e) {}
885
+ // 首次挂载时若上次会话面板是开的,直接打开
886
+ if (readOpenFlag()) setTimeout(function () { try { open() } catch (e) {} }, 80)
782
887
  function toggle () {
783
888
  if (root.classList.contains('open')) close()
784
889
  else open()
@@ -842,6 +947,13 @@ module.exports = function attachSettingsPanel (win) {
842
947
  win.__zgsOpen = open
843
948
  win.__zgsClose = close
844
949
  win.__zgsIsOpen = function () { return root.classList.contains('open') }
950
+ // 建仓向导完成 / 开关变更时重算仓库状态(只注册一次;render 会被反复调用)
951
+ try {
952
+ win.addEventListener('cmark-git-changed', function () {
953
+ try { if (typeof win.__zgsRefreshGitSection === 'function') win.__zgsRefreshGitSection() } catch (e) {}
954
+ })
955
+ } catch (e) {}
956
+
845
957
  win.__zgsRender = render
846
958
  logBoot('done, btn=' + !!doc.getElementById('znotes-sbtn'))
847
959
  }