@lancc/cmark 0.0.0-stage → 1.1.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +185 -2
- package/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/assets/abnfDiagram-WEYOIA3C-BgXsTF7a.js +1 -0
- package/assets/architectureDiagram-ZHVYKT53-BsCWa_qC.js +36 -0
- package/assets/blockDiagram-RJAE7QA3-wbIxEiYN.js +140 -0
- package/assets/c4Diagram-XDPPPE54-5zdyxFvA.js +38 -0
- package/assets/chunk-2E7WHVMD-F85Lca-i.js +88 -0
- package/assets/chunk-2Q5K7J3B-CjpAZ0OI.js +1 -0
- package/assets/chunk-4EMNKR2T-C8Kbh83c.js +1 -0
- package/assets/chunk-55XCU76K-BINcFiLN.js +1 -0
- package/assets/chunk-5VM5RSS4-Diy5h2LS.js +15 -0
- package/assets/chunk-JWPE2WC7-BXnjDdh1.js +1 -0
- package/assets/chunk-VQ5W62QI-BM7ZZo1I.js +1 -0
- package/assets/chunk-XXDRQBXY-CW8CjB1w.js +1 -0
- package/assets/chunk-ZCHMQSOR-C7ATz1L1.js +2 -0
- package/assets/classDiagram-v2-4MYWR3UA-CMoqNk-b.js +217 -0
- package/assets/cose-bilkent-JH36ORCC-6rqR5iJk.js +1 -0
- package/assets/cynefin-EF2NZ3EQ-DypLIJxl.js +141 -0
- package/assets/cynefinDiagram-WF24UDRP-8b2CEBlR.js +62 -0
- package/assets/cytoscape.esm-CjX6Hyah.js +331 -0
- package/assets/dagre-NK4GIZVX-Dv6vuUbR.js +4 -0
- package/assets/diagram-CBK3MWTC-DJgMDeYd.js +24 -0
- package/assets/diagram-I5XIIV2D-BB9cMoRW.js +24 -0
- package/assets/diagram-ODIL2UCW-BVcRtsdy.js +30 -0
- package/assets/diagram-PFBEJUXE-BbGSOtJx.js +3 -0
- package/assets/diagram-RJA3I4VV-CzkY_8Fo.js +41 -0
- package/assets/diagram-TYUNFSNR-YTl5QdfL.js +200 -0
- package/assets/ebnfDiagram-V4P3TTPT-COVjPAjE.js +1 -0
- package/assets/echarts-BAPDkhe3.js +61 -0
- package/assets/elk-QONTBCTQ-DHsjY8Z_.js +27 -0
- package/assets/erDiagram-KYQIGQAA-CUAbTUl1.js +99 -0
- package/assets/flowDiagram-IUGLLWBU-CWsiqTQC.js +213 -0
- package/assets/ganttDiagram-R7GPNTJY-DetBW0Wm.js +292 -0
- package/assets/gitGraphDiagram-PDYL7XHY-ByrdgJmw.js +106 -0
- package/assets/graph-D8UmR1vK.js +1 -0
- package/assets/index-BLONSItV.css +1 -0
- package/assets/index-Cz6qaZbV.js +744 -0
- package/assets/infoDiagram-MVDRKAX5-BM48fjhU.js +2 -0
- package/assets/ishikawaDiagram-HPEHV2GE-DtHYywHZ.js +70 -0
- package/assets/journeyDiagram-BOSHW7O7-DJ1LiRh3.js +139 -0
- package/assets/kanban-definition-FNSA4HC5-DPPt8pwJ.js +89 -0
- package/assets/katex-CQL9J12G.js +257 -0
- package/assets/markdown-DVehI1Sg.js +314 -0
- package/assets/mermaid-C_q9pug0.js +312 -0
- package/assets/mindmap-definition-6OY57UG3-zbqYOEY6.js +98 -0
- package/assets/pegDiagram-ECIUJNBX-e7QG7TMq.js +1 -0
- package/assets/pieDiagram-GNMPY74T-Duf6YP0N.js +39 -0
- package/assets/quadrantDiagram-D57K4MC3-B2eYoUUy.js +7 -0
- package/assets/railroadDiagram-Y72NRBL7-BWCzPcI_.js +1 -0
- package/assets/requirementDiagram-EQYSW5RA-HhPMEqB1.js +84 -0
- package/assets/sankeyDiagram-SY24JHO5-CMUTwFBB.js +40 -0
- package/assets/sequenceDiagram-BLEHNHQU-C9wR2ll7.js +169 -0
- package/assets/sizeCapture-INFHLROL-DXfz37S6.js +1 -0
- package/assets/stateDiagram-v2-A6IESKWZ-D_gtonhE.js +291 -0
- package/assets/swimlanes-B3ENRKFQ-DdJOuuMV.js +1 -0
- package/assets/swimlanesDiagram-AYQRV4AK-BWt3HxHV.js +8 -0
- package/assets/timeline-definition-JXYKGLGB-DQsdwJe-.js +120 -0
- package/assets/usecaseDiagram-WPOLBNNX-DTRyFq7d.js +370 -0
- package/assets/vennDiagram-Q3Y3IDCW-dihYszm0.js +34 -0
- package/assets/wardleyDiagram-IVAIDKWA-BoPpgYfK.js +78 -0
- package/assets/xychartDiagram-4URNHDU4-DoLjYxeB.js +7 -0
- package/export-file/github-markdown.css +550 -0
- package/export-file/index.html +33 -0
- package/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/fonts/KaTeX_Main-Bold.woff +0 -0
- package/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/fonts/KaTeX_Main-Italic.woff +0 -0
- package/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/fonts/KaTeX_Main-Regular.woff +0 -0
- package/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/fonts/KaTeX_Math-Italic.woff +0 -0
- package/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/fonts/KaTeX_Script-Regular.woff +0 -0
- package/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/index.html +15 -0
- package/katex.min.css +1 -0
- package/logo.png +0 -0
- package/package.json +31 -4
- package/plugin.json +241 -0
- package/preload/branch-bar.js +159 -0
- package/preload/git-panel.js +1311 -0
- package/preload/git-service.js +1403 -0
- package/preload/ndb.js +1407 -0
- package/preload/package.json +3 -0
- package/preload/rename-note.js +151 -0
- package/preload/services.js +432 -0
- package/preload/settings-panel.js +847 -0
- package/preload.js +1615 -0
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-panel.js —— Git 面板(参照 Obsidian Git 的信息架构,纯 DOM 实现)
|
|
3
|
+
*
|
|
4
|
+
* 结构:顶部状态条 + 三个页签(改动 / 历史 / 设置)+ 底部动作区
|
|
5
|
+
* 特性:
|
|
6
|
+
* - 深浅色自适应(prefers-color-scheme)
|
|
7
|
+
* - 改动列表只区分「未提交」文件,按类型着色,可丢弃 / 看差异(不暴露暂存区概念)
|
|
8
|
+
* - 历史列表可展开查看该提交的文件,点文件看该次改动
|
|
9
|
+
* - diff 视图按行着色(+绿 -红 @@蓝)
|
|
10
|
+
* - 当前打开的笔记在列表中高亮
|
|
11
|
+
*/
|
|
12
|
+
module.exports = function installGitPanel(win, WS) {
|
|
13
|
+
const doc = win.document
|
|
14
|
+
if (!doc) return
|
|
15
|
+
if (doc.readyState === 'loading' || !doc.head || !doc.body) {
|
|
16
|
+
doc.addEventListener('DOMContentLoaded', function () { installGitPanel(win, WS) })
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
// 结构自检:面板可能由旧版本创建(缺少新容器),这种情况直接重建
|
|
20
|
+
{
|
|
21
|
+
const oldRoot = doc.getElementById('zgp-root')
|
|
22
|
+
if (oldRoot) {
|
|
23
|
+
const ok = oldRoot.querySelector('#zgp-chips') && oldRoot.querySelector('#zgp-fileact')
|
|
24
|
+
if (ok) return
|
|
25
|
+
try { oldRoot.remove() } catch (e) {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const G = win.__znotesGit
|
|
30
|
+
if (!G) return
|
|
31
|
+
|
|
32
|
+
// ============ 样式 ============
|
|
33
|
+
const css = `
|
|
34
|
+
:root{
|
|
35
|
+
--zgp-bg:#ffffff; --zgp-bg2:#f7f8fa; --zgp-bg3:#eef0f4;
|
|
36
|
+
--zgp-fg:#1f2328; --zgp-fg2:#57606a; --zgp-fg3:#8b949e;
|
|
37
|
+
--zgp-bd:#e3e6ea; --zgp-bd2:#d0d7de;
|
|
38
|
+
--zgp-acc:#2563eb; --zgp-acc-bg:#eff5ff;
|
|
39
|
+
--zgp-ok:#1a7f37; --zgp-warn:#bf8700; --zgp-err:#cf222e;
|
|
40
|
+
--zgp-add:#e6ffec; --zgp-add-fg:#1a7f37;
|
|
41
|
+
--zgp-del:#ffebe9; --zgp-del-fg:#cf222e;
|
|
42
|
+
--zgp-hunk:#ddf4ff; --zgp-hunk-fg:#0969da;
|
|
43
|
+
--zgp-shadow:0 8px 32px rgba(31,35,40,.14),0 2px 8px rgba(31,35,40,.08);
|
|
44
|
+
}
|
|
45
|
+
@media (prefers-color-scheme:dark){
|
|
46
|
+
:root{
|
|
47
|
+
--zgp-bg:#161b22; --zgp-bg2:#1c2128; --zgp-bg3:#22272e;
|
|
48
|
+
--zgp-fg:#e6edf3; --zgp-fg2:#9aa4b2; --zgp-fg3:#6e7681;
|
|
49
|
+
--zgp-bd:#30363d; --zgp-bd2:#3d444d;
|
|
50
|
+
--zgp-acc:#58a6ff; --zgp-acc-bg:#12233d;
|
|
51
|
+
--zgp-ok:#3fb950; --zgp-warn:#d29922; --zgp-err:#f85149;
|
|
52
|
+
--zgp-add:#0f2b1a; --zgp-add-fg:#3fb950;
|
|
53
|
+
--zgp-del:#2d1214; --zgp-del-fg:#f85149;
|
|
54
|
+
--zgp-hunk:#0c2d4a; --zgp-hunk-fg:#58a6ff;
|
|
55
|
+
--zgp-shadow:0 8px 32px rgba(0,0,0,.6),0 2px 8px rgba(0,0,0,.4);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
#zgp-btn{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:500;padding:3px 10px;
|
|
59
|
+
border:1px solid var(--zgp-bd2);border-radius:6px;background:var(--zgp-bg);color:var(--zgp-fg2);
|
|
60
|
+
cursor:pointer;margin-right:6px;transition:all .15s;line-height:20px;}
|
|
61
|
+
#zgp-btn:hover{background:var(--zgp-bg3);color:var(--zgp-fg);border-color:var(--zgp-acc);}
|
|
62
|
+
#zgp-btn .zgp-dot{width:6px;height:6px;border-radius:50%;background:var(--zgp-warn);display:none;}
|
|
63
|
+
#zgp-btn.zgp-dirty .zgp-dot{display:block;animation:zgp-pulse 1.8s ease-in-out infinite;}
|
|
64
|
+
@keyframes zgp-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
65
|
+
|
|
66
|
+
#zgp-root{position:fixed;top:0;right:0;width:520px;max-width:96vw;height:100%;z-index:99998;
|
|
67
|
+
display:none;flex-direction:column;background:var(--zgp-bg);color:var(--zgp-fg);
|
|
68
|
+
box-shadow:var(--zgp-shadow);font-size:13px;line-height:1.5;
|
|
69
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",system-ui,sans-serif;}
|
|
70
|
+
#zgp-root.open{display:flex;animation:zgp-in .22s cubic-bezier(.2,.8,.2,1);}
|
|
71
|
+
@keyframes zgp-in{from{transform:translateX(24px);opacity:0}to{transform:none;opacity:1}}
|
|
72
|
+
|
|
73
|
+
.zgp-head{padding:14px 16px 10px;border-bottom:1px solid var(--zgp-bd);}
|
|
74
|
+
.zgp-head-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;}
|
|
75
|
+
.zgp-title{font-size:14px;font-weight:600;letter-spacing:.2px;display:flex;align-items:center;gap:7px;white-space:nowrap;flex:1;min-width:0;overflow:hidden;}
|
|
76
|
+
.zgp-title > svg{width:16px;height:16px;flex:0 0 16px;fill:currentColor;}
|
|
77
|
+
.zgp-title > span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
78
|
+
.zgp-close{border:none;background:none;color:var(--zgp-fg3);cursor:pointer;font-size:20px;line-height:1;flex:0 0 auto;
|
|
79
|
+
padding:2px 6px;border-radius:6px;}
|
|
80
|
+
.zgp-close:hover{background:var(--zgp-bg3);color:var(--zgp-fg);}
|
|
81
|
+
.zgp-status{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:11px;}
|
|
82
|
+
.zgp-chips{display:flex;flex-wrap:wrap;gap:6px;min-width:0;flex:1 1 auto;}
|
|
83
|
+
.zgp-acts{display:flex;align-items:center;gap:6px;flex:0 0 auto;}
|
|
84
|
+
.zgp-fileact{display:flex;align-items:center;gap:6px;flex:0 0 auto;}
|
|
85
|
+
.zgp-fileact .zgp-b{padding:3px 9px;font-size:11px;}
|
|
86
|
+
.zgp-fileact .zgp-count{font-size:11px;color:var(--zgp-fg2);white-space:nowrap;}
|
|
87
|
+
.zgp-acts .zgp-b{padding:3px 9px;font-size:11px;}
|
|
88
|
+
.zgp-chip{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:999px;
|
|
89
|
+
background:var(--zgp-bg2);border:1px solid var(--zgp-bd);color:var(--zgp-fg2);white-space:nowrap;}
|
|
90
|
+
.zgp-chip b{font-weight:600;color:var(--zgp-fg);}
|
|
91
|
+
.zgp-chip.ok{border-color:var(--zgp-ok);color:var(--zgp-ok);}
|
|
92
|
+
.zgp-chip.warn{border-color:var(--zgp-warn);color:var(--zgp-warn);}
|
|
93
|
+
.zgp-chip.err{border-color:var(--zgp-err);color:var(--zgp-err);}
|
|
94
|
+
.zgp-chip.acc{border-color:var(--zgp-acc);color:var(--zgp-acc);}
|
|
95
|
+
.zgp-spin{width:9px;height:9px;border:1.5px solid currentColor;border-right-color:transparent;
|
|
96
|
+
border-radius:50%;display:inline-block;animation:zgp-rot .7s linear infinite;}
|
|
97
|
+
@keyframes zgp-rot{to{transform:rotate(360deg)}}
|
|
98
|
+
|
|
99
|
+
.zgp-tabs{display:flex;gap:2px;padding:8px 12px 0;border-bottom:1px solid var(--zgp-bd);}
|
|
100
|
+
.zgp-tab{padding:7px 14px 9px;font-size:12.5px;font-weight:500;color:var(--zgp-fg2);cursor:pointer;
|
|
101
|
+
border:none;background:none;border-bottom:2px solid transparent;margin-bottom:-1px;transition:all .15s;
|
|
102
|
+
display:flex;align-items:center;gap:6px;}
|
|
103
|
+
.zgp-tab:hover{color:var(--zgp-fg);}
|
|
104
|
+
.zgp-tab.on{color:var(--zgp-acc);border-bottom-color:var(--zgp-acc);}
|
|
105
|
+
.zgp-tab .zgp-badge{min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--zgp-bg3);
|
|
106
|
+
color:var(--zgp-fg2);font-size:10px;display:none;align-items:center;justify-content:center;font-weight:600;}
|
|
107
|
+
.zgp-tab .zgp-badge.on{display:inline-flex;}
|
|
108
|
+
.zgp-tab.on .zgp-badge{background:var(--zgp-acc-bg);color:var(--zgp-acc);}
|
|
109
|
+
|
|
110
|
+
.zgp-body{flex:1;overflow:auto;overscroll-behavior:contain;}
|
|
111
|
+
.zgp-pane{display:none;padding:0;}
|
|
112
|
+
.zgp-pane.on{display:block;}
|
|
113
|
+
.zgp-sec{padding:12px 16px;border-bottom:1px solid var(--zgp-bd);}
|
|
114
|
+
.zgp-sec:last-child{border-bottom:none;}
|
|
115
|
+
.zgp-sec h5{margin:0 0 8px;font-size:11px;font-weight:600;letter-spacing:.6px;text-transform:uppercase;color:var(--zgp-fg3);}
|
|
116
|
+
|
|
117
|
+
.zgp-autoline{display:flex;align-items:center;justify-content:space-between;gap:8px;
|
|
118
|
+
margin-top:7px;font-size:11.5px;color:var(--zgp-fg2);}
|
|
119
|
+
.zgp-autoline .zgp-b{padding:2px 8px;font-size:11px;}
|
|
120
|
+
.zgp-file{display:flex;align-items:center;gap:8px;padding:7px 16px;cursor:default;
|
|
121
|
+
border-bottom:1px solid var(--zgp-bd);transition:background .12s;}
|
|
122
|
+
.zgp-file:hover{background:var(--zgp-bg2);}
|
|
123
|
+
.zgp-file.cur{background:var(--zgp-acc-bg);box-shadow:inset 3px 0 0 var(--zgp-acc);}
|
|
124
|
+
.zgp-st{width:18px;height:18px;flex:0 0 18px;border-radius:4px;display:flex;align-items:center;
|
|
125
|
+
justify-content:center;font-size:10px;font-weight:700;font-family:Consolas,Menlo,monospace;}
|
|
126
|
+
.zgp-st.M{background:#fff4ce;color:#9a6700;}
|
|
127
|
+
.zgp-st.A{background:var(--zgp-add);color:var(--zgp-add-fg);}
|
|
128
|
+
.zgp-st.D{background:var(--zgp-del);color:var(--zgp-del-fg);}
|
|
129
|
+
.zgp-st.Q{background:var(--zgp-bg3);color:var(--zgp-fg3);}
|
|
130
|
+
.zgp-st.U{background:var(--zgp-del);color:var(--zgp-del-fg);}
|
|
131
|
+
@media (prefers-color-scheme:dark){.zgp-st.M{background:#3a2d00;color:#d29922;}}
|
|
132
|
+
.zgp-fp{flex:1;min-width:0;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
133
|
+
font-family:Consolas,Menlo,monospace;}
|
|
134
|
+
.zgp-fp span{unicode-bidi:normal;}
|
|
135
|
+
.zgp-ops{display:flex;gap:3px;opacity:0;transition:opacity .12s;}
|
|
136
|
+
.zgp-file:hover .zgp-ops{opacity:1;}
|
|
137
|
+
.zgp-ic{border:1px solid var(--zgp-bd2);background:var(--zgp-bg);color:var(--zgp-fg2);cursor:pointer;
|
|
138
|
+
width:24px;height:24px;border-radius:5px;display:flex;align-items:center;justify-content:center;padding:0;}
|
|
139
|
+
.zgp-ic:hover{background:var(--zgp-acc-bg);border-color:var(--zgp-acc);color:var(--zgp-acc);}
|
|
140
|
+
.zgp-ic.danger:hover{background:var(--zgp-del);border-color:var(--zgp-err);color:var(--zgp-err);}
|
|
141
|
+
.zgp-ic svg{width:13px;height:13px;fill:currentColor;}
|
|
142
|
+
|
|
143
|
+
.zgp-cm{padding:9px 16px;border-bottom:1px solid var(--zgp-bd);cursor:pointer;transition:background .12s;}
|
|
144
|
+
.zgp-cm:hover{background:var(--zgp-bg2);}
|
|
145
|
+
.zgp-cm.open{background:var(--zgp-bg2);}
|
|
146
|
+
.zgp-cm1{display:flex;align-items:baseline;gap:8px;}
|
|
147
|
+
.zgp-cm-sub{flex:1;min-width:0;font-size:12.5px;font-weight:500;overflow:hidden;text-overflow:ellipsis;
|
|
148
|
+
white-space:nowrap;color:var(--zgp-fg);}
|
|
149
|
+
.zgp-cm-meta{font-size:11px;color:var(--zgp-fg3);margin-top:3px;font-family:Consolas,Menlo,monospace;}
|
|
150
|
+
.zgp-hash{font-size:10.5px;font-family:Consolas,Menlo,monospace;padding:1px 5px;border-radius:4px;
|
|
151
|
+
background:var(--zgp-bg3);color:var(--zgp-fg2);flex:0 0 auto;}
|
|
152
|
+
.zgp-cmbody{display:none;margin-top:7px;padding-left:4px;border-left:2px solid var(--zgp-bd2);}
|
|
153
|
+
.zgp-cm.open .zgp-cmbody{display:block;}
|
|
154
|
+
.zgp-cmf{display:flex;align-items:center;gap:7px;padding:3px 0 3px 8px;font-size:11.5px;
|
|
155
|
+
font-family:Consolas,Menlo,monospace;color:var(--zgp-fg2);cursor:pointer;border-radius:4px;}
|
|
156
|
+
.zgp-cmf:hover{background:var(--zgp-bg3);color:var(--zgp-fg);}
|
|
157
|
+
.zgp-cmf .zgp-fs{font-weight:700;width:12px;flex:0 0 12px;}
|
|
158
|
+
.zgp-cmf .zgp-fs.A{color:var(--zgp-add-fg);} .zgp-cmf .zgp-fs.D{color:var(--zgp-del-fg);}
|
|
159
|
+
.zgp-cmf .zgp-fs.M{color:var(--zgp-warn);}
|
|
160
|
+
|
|
161
|
+
.zgp-field{margin-bottom:13px;}
|
|
162
|
+
.zgp-field:last-child{margin-bottom:0;}
|
|
163
|
+
.zgp-lab{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:5px;}
|
|
164
|
+
.zgp-lab span{font-size:12.5px;color:var(--zgp-fg);}
|
|
165
|
+
.zgp-desc{font-size:11px;color:var(--zgp-fg3);margin-top:4px;line-height:1.5;}
|
|
166
|
+
.zgp-in,.zgp-sel{width:100%;box-sizing:border-box;padding:6px 9px;font-size:12px;border-radius:6px;
|
|
167
|
+
border:1px solid var(--zgp-bd2);background:var(--zgp-bg);color:var(--zgp-fg);outline:none;
|
|
168
|
+
font-family:inherit;transition:border-color .15s;}
|
|
169
|
+
.zgp-in:focus,.zgp-sel:focus{border-color:var(--zgp-acc);box-shadow:0 0 0 3px var(--zgp-acc-bg);}
|
|
170
|
+
.zgp-drawer{overflow:hidden;max-height:0;opacity:0;
|
|
171
|
+
transition:max-height .26s ease,opacity .2s ease;}
|
|
172
|
+
.zgp-drawer.open{max-height:520px;opacity:1;}
|
|
173
|
+
.zgp-sw{position:relative;width:36px;height:20px;flex:0 0 36px;}
|
|
174
|
+
.zgp-sw input{opacity:0;width:0;height:0;position:absolute;}
|
|
175
|
+
.zgp-sw i{position:absolute;inset:0;background:var(--zgp-bd2);border-radius:999px;transition:.18s;cursor:pointer;}
|
|
176
|
+
.zgp-sw i::after{content:"";position:absolute;width:16px;height:16px;left:2px;top:2px;background:#fff;
|
|
177
|
+
border-radius:50%;transition:.18s;box-shadow:0 1px 3px rgba(0,0,0,.25);}
|
|
178
|
+
.zgp-sw input:checked + i{background:var(--zgp-acc);}
|
|
179
|
+
.zgp-sw input:checked + i::after{transform:translateX(16px);}
|
|
180
|
+
|
|
181
|
+
.zgp-b{padding:6px 14px;font-size:12.5px;font-weight:500;border-radius:6px;cursor:pointer;
|
|
182
|
+
border:1px solid var(--zgp-bd2);background:var(--zgp-bg);color:var(--zgp-fg);transition:all .15s;
|
|
183
|
+
display:inline-flex;align-items:center;gap:6px;}
|
|
184
|
+
.zgp-b:hover{background:var(--zgp-bg3);}
|
|
185
|
+
.zgp-b:disabled{opacity:.5;cursor:not-allowed;}
|
|
186
|
+
.zgp-b.primary{background:var(--zgp-acc);border-color:var(--zgp-acc);color:#fff;}
|
|
187
|
+
.zgp-b.primary:hover{filter:brightness(1.08);}
|
|
188
|
+
.zgp-b svg{width:13px;height:13px;fill:currentColor;}
|
|
189
|
+
|
|
190
|
+
.zgp-radio{display:flex;align-items:center;gap:8px;padding:6px 0;font-size:12.5px;cursor:pointer;}
|
|
191
|
+
.zgp-radio input{width:14px;height:14px;accent-color:var(--zgp-acc);cursor:pointer;}
|
|
192
|
+
.zgp-empty{padding:34px 20px;text-align:center;color:var(--zgp-fg3);font-size:12.5px;line-height:1.9;}
|
|
193
|
+
.zgp-empty b{display:block;color:var(--zgp-fg2);font-size:13px;margin-bottom:4px;font-weight:500;}
|
|
194
|
+
|
|
195
|
+
#zgp-view{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:100001;display:none;
|
|
196
|
+
align-items:center;justify-content:center;padding:24px;backdrop-filter:blur(2px);}
|
|
197
|
+
#zgp-view.on{display:flex;}
|
|
198
|
+
.zgp-vbox{background:var(--zgp-bg);color:var(--zgp-fg);width:min(1000px,96vw);height:min(82vh,860px);
|
|
199
|
+
border-radius:12px;display:flex;flex-direction:column;overflow:hidden;box-shadow:var(--zgp-shadow);}
|
|
200
|
+
.zgp-vhead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;
|
|
201
|
+
border-bottom:1px solid var(--zgp-bd);background:var(--zgp-bg2);}
|
|
202
|
+
.zgp-vtitle{font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
203
|
+
font-family:Consolas,Menlo,monospace;}
|
|
204
|
+
.zgp-vbody{flex:1;overflow:auto;margin:0;font-family:Consolas,"Cascadia Mono",Menlo,monospace;
|
|
205
|
+
font-size:12px;line-height:1.6;}
|
|
206
|
+
.zgp-dl{display:block;padding:0 14px;white-space:pre-wrap;word-break:break-word;}
|
|
207
|
+
.zgp-dl.add{background:var(--zgp-add);color:var(--zgp-add-fg);}
|
|
208
|
+
.zgp-dl.del{background:var(--zgp-del);color:var(--zgp-del-fg);}
|
|
209
|
+
.zgp-dl.hunk{background:var(--zgp-hunk);color:var(--zgp-hunk-fg);font-weight:600;}
|
|
210
|
+
.zgp-dl.meta{color:var(--zgp-fg3);}
|
|
211
|
+
.zgp-dl{padding:0 8px 0 0;}
|
|
212
|
+
.zgp-ln{display:inline-block;width:42px;padding-right:10px;text-align:right;
|
|
213
|
+
color:var(--zgp-fg3);opacity:.7;user-select:none;font-variant-numeric:tabular-nums;}
|
|
214
|
+
.zgp-dl.add .zgp-ln{color:var(--zgp-add-fg);opacity:.85;}
|
|
215
|
+
.zgp-dl.del .zgp-ln{color:var(--zgp-del-fg);opacity:.85;}
|
|
216
|
+
.zgp-dl.oldno .zgp-ln{opacity:.45;font-style:italic;}
|
|
217
|
+
.zgp-jumpbar{position:sticky;top:0;z-index:3;display:flex;justify-content:flex-end;
|
|
218
|
+
padding:4px 8px 3px;background:var(--zgp-bg);}
|
|
219
|
+
.zgp-jumpbar button{padding:3px 10px;font-size:11.5px;border-radius:6px;cursor:pointer;
|
|
220
|
+
border:1px solid var(--zgp-bd2);background:var(--zgp-bg);color:var(--zgp-fg2);}
|
|
221
|
+
.zgp-jumpbar button:hover{background:var(--zgp-acc-bg);border-color:var(--zgp-acc);color:var(--zgp-acc);}
|
|
222
|
+
.zgp-dl.zgp-flash{outline:2px solid var(--zgp-acc);outline-offset:-2px;}
|
|
223
|
+
|
|
224
|
+
.zgp-cf-ov{position:absolute;inset:0;background:rgba(0,0,0,.38);display:flex;align-items:center;
|
|
225
|
+
justify-content:center;z-index:20;padding:20px;}
|
|
226
|
+
.zgp-cf-card{background:var(--zgp-bg);border:1px solid var(--zgp-bd);border-radius:10px;
|
|
227
|
+
padding:16px 16px 12px;width:100%;max-width:340px;box-shadow:var(--zgp-shadow);}
|
|
228
|
+
.zgp-cf-msg{font-size:12.5px;line-height:1.75;white-space:pre-wrap;color:var(--zgp-fg);
|
|
229
|
+
margin-bottom:14px;word-break:break-word;}
|
|
230
|
+
.zgp-cf-btns{display:flex;justify-content:flex-end;gap:8px;}
|
|
231
|
+
.zgp-b.danger{background:var(--zgp-err);border-color:var(--zgp-err);color:#fff;}
|
|
232
|
+
#zgp-toast{position:fixed;left:50%;bottom:36px;transform:translateX(-50%) translateY(12px);
|
|
233
|
+
background:#1f2328;color:#fff;padding:9px 18px;border-radius:8px;font-size:12.5px;z-index:100002;
|
|
234
|
+
opacity:0;pointer-events:none;transition:all .22s;box-shadow:0 6px 24px rgba(0,0,0,.3);max-width:70vw;}
|
|
235
|
+
#zgp-toast.on{opacity:1;transform:translateX(-50%) translateY(0);}
|
|
236
|
+
#zgp-toast.err{background:var(--zgp-err);}
|
|
237
|
+
#zgp-toast.ok{background:var(--zgp-ok);}
|
|
238
|
+
`
|
|
239
|
+
|
|
240
|
+
const style = doc.createElement('style')
|
|
241
|
+
style.textContent = css
|
|
242
|
+
doc.head.appendChild(style)
|
|
243
|
+
|
|
244
|
+
// ============ 图标 ============
|
|
245
|
+
const ICON = {
|
|
246
|
+
// 与工作区比对:文件 + 对勾箭头(该提交 → 现在的差异)
|
|
247
|
+
cmpWork: '<svg viewBox="0 0 16 16"><path d="M2 1.75A.75.75 0 0 1 2.75 1h5.19c.2 0 .39.08.53.22l3.31 3.31a.75.75 0 0 1 .22.53v2.13a5.5 5.5 0 0 0-1.5-.44V5.5H7.75A.75.75 0 0 1 7 4.75V2.5H2.75v11h3.44c.28.55.66 1.04 1.11 1.46H2.75A.75.75 0 0 1 2 14.25V1.75z"/><path d="M11.75 8a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5zm1.72 2.78l-2.22 2.22a.75.75 0 0 1-1.06 0l-.97-.97a.75.75 0 1 1 1.06-1.06l.44.44 1.69-1.69a.75.75 0 1 1 1.06 1.06z"/></svg>',
|
|
248
|
+
// 查看该版本:眼睛
|
|
249
|
+
eye: '<svg viewBox="0 0 16 16"><path d="M8 3C4.5 3 1.7 5.3.5 8c1.2 2.7 4 5 7.5 5s6.3-2.3 7.5-5C14.3 5.3 11.5 3 8 3zm0 8.25a3.25 3.25 0 1 1 0-6.5 3.25 3.25 0 0 1 0 6.5z"/><circle cx="8" cy="8" r="1.6"/></svg>',
|
|
250
|
+
// 与提交前比对:时钟回溯(那次提交本身改了什么)
|
|
251
|
+
cmpPrev: '<svg viewBox="0 0 16 16"><path d="M8 1.5a6.5 6.5 0 1 1-6.4 7.65h1.53A5 5 0 1 0 8 3v1.75L4.75 2.5 8-.25V1.5z" transform="translate(0,1)"/><path d="M7.25 5h1.5v3.4l2.2 1.3-.75 1.27-2.95-1.74V5z"/></svg>',
|
|
252
|
+
git: '<svg viewBox="0 0 1024 1024"><path d="M512 64c-16.128 0-31.872 5.888-44 18.016l-90.016 90.976c-4.864 2.624-8.96 6.4-11.968 11.008l-284.032 284a62.304 62.304 0 0 0 0 88l386.016 386.016a62.56 62.56 0 0 0 88 0l386.016-386.016a62.304 62.304 0 0 0 0-88L555.968 81.984A61.536 61.536 0 0 0 512 64z m0 64.992L895.008 512 512 895.008 128.992 512l265.024-264.992 56 56A63.36 63.36 0 0 0 448 320c0 23.616 12.864 43.872 32 55.008v273.984c-19.136 11.136-32 31.36-32 55.008a63.968 63.968 0 1 0 128 0c0-23.616-12.864-43.872-32-55.008v-250.976l98.016 97.984A63.968 63.968 0 0 0 704 576c35.328 0 63.968-28.64 63.968-64a63.968 63.968 0 0 0-80-62.016L573.984 336A63.968 63.968 0 0 0 512 256a63.36 63.36 0 0 0-16.96 2.016l-56-56z"/></svg>',
|
|
253
|
+
plus: '<svg viewBox="0 0 16 16"><path d="M7.25 1.75a.75.75 0 0 1 1.5 0V7.25h5.5a.75.75 0 0 1 0 1.5h-5.5v5.5a.75.75 0 0 1-1.5 0v-5.5h-5.5a.75.75 0 0 1 0-1.5h5.5z"/></svg>',
|
|
254
|
+
undo: '<svg viewBox="0 0 16 16"><path d="M1.5 8a6.5 6.5 0 1 1 1.9 4.6.75.75 0 1 1 1.08-1.04A5 5 0 1 0 3.5 8v1.25a.75.75 0 0 1-1.5 0V8z"/><path d="M1 4.5a.75.75 0 0 1 1.5 0V6h1.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 6.75V4.5z"/></svg>',
|
|
255
|
+
diff: '<svg viewBox="0 0 16 16"><path d="M1.75 1a.75.75 0 0 0 0 1.5h12.5a.75.75 0 0 0 0-1.5H1.75zM1.75 5a.75.75 0 0 0 0 1.5h5.5a.75.75 0 0 0 0-1.5h-5.5zM1.75 9a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5zM1.75 13a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5z"/></svg>',
|
|
256
|
+
sync: '<svg viewBox="0 0 16 16"><path d="M1.75 8a6.25 6.25 0 0 1 10.6-4.46l1.2-1.2a.5.5 0 0 1 .85.35v3.5a.5.5 0 0 1-.5.5h-3.5a.5.5 0 0 1-.35-.85l1.24-1.24A4.75 4.75 0 0 0 3.25 8a.75.75 0 0 1-1.5 0zm12.5 0a.75.75 0 0 1-1.5 0A4.75 4.75 0 0 0 4.06 12.2l1.24-1.24a.5.5 0 0 1 .85.35v3.5a.5.5 0 0 1-.5.5h-3.5a.5.5 0 0 1-.35-.85l1.2-1.2A6.25 6.25 0 0 1 1.75 8z"/></svg>',
|
|
257
|
+
up: '<svg viewBox="0 0 16 16"><path d="M8 1.5l5 5-.9.9L8.5 3.8V15h-1V3.8L3.9 7.4 3 6.5z"/></svg>',
|
|
258
|
+
down: '<svg viewBox="0 0 16 16"><path d="M8 14.5l-5-5 .9-.9L7.5 12.2V1h1v11.2l3.6-3.6.9.9z"/></svg>'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============ DOM ============
|
|
262
|
+
const root = doc.createElement('div')
|
|
263
|
+
root.id = 'zgp-root'
|
|
264
|
+
root.innerHTML = `
|
|
265
|
+
<div class="zgp-head">
|
|
266
|
+
<div class="zgp-head-row">
|
|
267
|
+
<div class="zgp-title"><span id="zgp-title-text" style="white-space:nowrap">工作区变更记录</span></div>
|
|
268
|
+
<button class="zgp-close" id="zgp-close" title="关闭 (Esc)">×</button>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="zgp-status" id="zgp-status">
|
|
271
|
+
<div class="zgp-chips" id="zgp-chips"></div>
|
|
272
|
+
<div class="zgp-acts">
|
|
273
|
+
<button class="zgp-b" id="zgp-pull" title="从远端拉取提交">${ICON.down}<span>拉取</span></button>
|
|
274
|
+
<button class="zgp-b" id="zgp-push" title="把本地提交推送到远端">${ICON.up}<span>推送</span></button>
|
|
275
|
+
<button class="zgp-b primary" id="zgp-sync" title="提交本地改动并推送">${ICON.sync}<span>同步</span></button>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="zgp-fileact" id="zgp-fileact"></div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="zgp-tabs">
|
|
281
|
+
<button class="zgp-tab on" data-tab="changes">改动<span class="zgp-badge" id="zgp-b1"></span></button>
|
|
282
|
+
<button class="zgp-tab" data-tab="history">历史</button>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="zgp-body">
|
|
285
|
+
<div class="zgp-pane on" id="zgp-pane-changes"></div>
|
|
286
|
+
<div class="zgp-pane" id="zgp-pane-history"></div>
|
|
287
|
+
</div>
|
|
288
|
+
`
|
|
289
|
+
doc.body.appendChild(root)
|
|
290
|
+
|
|
291
|
+
const view = doc.createElement('div')
|
|
292
|
+
view.id = 'zgp-view'
|
|
293
|
+
view.innerHTML = `
|
|
294
|
+
<div class="zgp-vbox">
|
|
295
|
+
<div class="zgp-vhead">
|
|
296
|
+
<div class="zgp-vtitle" id="zgp-vtitle"></div>
|
|
297
|
+
<button class="zgp-b" id="zgp-vclose">关闭</button>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="zgp-vbody" id="zgp-vbody"></div>
|
|
300
|
+
</div>
|
|
301
|
+
`
|
|
302
|
+
doc.body.appendChild(view)
|
|
303
|
+
|
|
304
|
+
const toastEl = doc.createElement('div')
|
|
305
|
+
toastEl.id = 'zgp-toast'
|
|
306
|
+
doc.body.appendChild(toastEl)
|
|
307
|
+
|
|
308
|
+
const $ = (id) => doc.getElementById(id)
|
|
309
|
+
|
|
310
|
+
// ============ 小工具 ============
|
|
311
|
+
let toastTimer = null
|
|
312
|
+
function toast(text, kind) {
|
|
313
|
+
toastEl.textContent = text || ''
|
|
314
|
+
toastEl.className = 'on ' + (kind || '')
|
|
315
|
+
clearTimeout(toastTimer)
|
|
316
|
+
toastTimer = setTimeout(() => { toastEl.className = '' }, 3600)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 把服务层的错误类型翻译成"原因 + 下一步建议"
|
|
320
|
+
function hintOf(r) {
|
|
321
|
+
const map = {
|
|
322
|
+
network: '网络不通或远端不可达,检查网络后重试',
|
|
323
|
+
auth: '认证失败,检查 SSH key 或 git 凭据',
|
|
324
|
+
rejected: '远端有新提交,先点「拉取」再推送',
|
|
325
|
+
diverged: '本地与远端已分叉,先推送本地提交或手动合并',
|
|
326
|
+
conflict: '存在冲突,请先处理冲突文件',
|
|
327
|
+
lock: '有 git 操作正在进行,稍后重试',
|
|
328
|
+
detached: 'HEAD 处于游离状态,先切回分支',
|
|
329
|
+
noUpstream: '当前分支没有上游分支,首次推送会自动设置',
|
|
330
|
+
perm: '文件被占用或权限不足,可能被杀毒软件锁定',
|
|
331
|
+
backup: '备份失败,操作已中止(你的文件没有被改动)',
|
|
332
|
+
timeout: '命令超时,检查网络或仓库大小',
|
|
333
|
+
busy: '上一个 git 操作还在进行中,稍等再试'
|
|
334
|
+
}
|
|
335
|
+
const msg = r.message || ''
|
|
336
|
+
const hint = map[r.kind]
|
|
337
|
+
return hint ? (hint + (msg && msg.indexOf(hint) < 0 ? '(' + msg.slice(0, 100) + ')' : '')) : (msg || r.kind || '未知原因')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 面板内确认层:不用原生 confirm —— 它会抢焦点,导致宿主把插件窗口隐藏掉
|
|
341
|
+
function askConfirm(message, okText) {
|
|
342
|
+
return new Promise(function (resolve) {
|
|
343
|
+
var ov = doc.createElement('div')
|
|
344
|
+
ov.className = 'zgp-cf-ov'
|
|
345
|
+
var card = doc.createElement('div')
|
|
346
|
+
card.className = 'zgp-cf-card'
|
|
347
|
+
var msg = doc.createElement('div')
|
|
348
|
+
msg.className = 'zgp-cf-msg'
|
|
349
|
+
msg.textContent = message
|
|
350
|
+
var row = doc.createElement('div')
|
|
351
|
+
row.className = 'zgp-cf-btns'
|
|
352
|
+
var no = doc.createElement('button')
|
|
353
|
+
no.className = 'zgp-b'
|
|
354
|
+
no.textContent = '取消'
|
|
355
|
+
var yes = doc.createElement('button')
|
|
356
|
+
yes.className = 'zgp-b primary'
|
|
357
|
+
yes.textContent = okText || '确定'
|
|
358
|
+
row.appendChild(no)
|
|
359
|
+
row.appendChild(yes)
|
|
360
|
+
card.appendChild(msg)
|
|
361
|
+
card.appendChild(row)
|
|
362
|
+
ov.appendChild(card)
|
|
363
|
+
root.appendChild(ov)
|
|
364
|
+
var done = function (v) {
|
|
365
|
+
try { ov.remove() } catch (e) {}
|
|
366
|
+
try { doc.removeEventListener('keydown', onKey) } catch (e) {}
|
|
367
|
+
resolve(v)
|
|
368
|
+
}
|
|
369
|
+
var onKey = function (e) {
|
|
370
|
+
if (e.key === 'Escape') { e.stopPropagation(); done(false) }
|
|
371
|
+
}
|
|
372
|
+
no.onclick = function () { done(false) }
|
|
373
|
+
yes.onclick = function () { done(true) }
|
|
374
|
+
ov.onclick = function (e) { if (e.target === ov) done(false) }
|
|
375
|
+
doc.addEventListener('keydown', onKey)
|
|
376
|
+
setTimeout(function () { try { yes.focus() } catch (e) {} }, 30)
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 还原到最近一次提交:把该文件的未提交改动丢掉(服务层会同时清掉暂存区)
|
|
381
|
+
async function doRestoreHead(rel) {
|
|
382
|
+
let r = await G.discard(rel)
|
|
383
|
+
if (!r.ok && r.kind === 'confirm-needed') {
|
|
384
|
+
const ok = await askConfirm('把「' + rel + '」还原到最近一次提交?\n\n未提交的改动会丢失(不会被同步到 git)。', '还原')
|
|
385
|
+
if (!ok) return { ok: false, kind: 'cancelled' }
|
|
386
|
+
r = await G.discard(rel, { force: true })
|
|
387
|
+
}
|
|
388
|
+
return r
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 回溯:先请求,服务层发现有未提交改动会返回 confirm-needed,确认后带 force 重试
|
|
392
|
+
async function doRestore(commit, rel, label) {
|
|
393
|
+
let r = await G.restoreTo(commit, rel)
|
|
394
|
+
if (!r.ok && r.kind === 'confirm-needed') {
|
|
395
|
+
const ok = await askConfirm('「' + rel + '」有未提交的改动。\n\n回溯到 ' + (label || commit.slice(0, 7)) + ' 会直接覆盖这些改动,且无法恢复。\n\n确定继续?')
|
|
396
|
+
if (!ok) return { ok: false, kind: 'cancelled' }
|
|
397
|
+
r = await G.restoreTo(commit, rel, { force: true })
|
|
398
|
+
}
|
|
399
|
+
return r
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 撤销未提交改动:同样先确认(未跟踪的新文件服务层会自行留底)
|
|
403
|
+
async function doDiscard(rel) {
|
|
404
|
+
let r = await G.discard(rel)
|
|
405
|
+
if (!r.ok && r.kind === 'confirm-needed') {
|
|
406
|
+
const ok = await askConfirm('「' + rel + '」的未提交改动将被丢弃,且无法恢复。\n\n确定继续?')
|
|
407
|
+
if (!ok) return { ok: false, kind: 'cancelled' }
|
|
408
|
+
r = await G.discard(rel, { force: true })
|
|
409
|
+
}
|
|
410
|
+
return r
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function esc(s) {
|
|
414
|
+
return String(s == null ? '' : s)
|
|
415
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
416
|
+
.replace(/"/g, '"')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 显示用路径:去掉笔记目录这一层(git 仓库根通常在其上一级,目录树里并不显示这一层)
|
|
420
|
+
function displayPath(p) {
|
|
421
|
+
const s = String(p || '')
|
|
422
|
+
const sc = (G && G.scopeRel) || ''
|
|
423
|
+
if (sc && s.indexOf(sc + '/') === 0) return s.slice(sc.length + 1)
|
|
424
|
+
return s
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function shortPath(p) {
|
|
428
|
+
const seg = String(p).split('/')
|
|
429
|
+
if (seg.length <= 2) return p
|
|
430
|
+
// 保留首段与文件名:Notes/…/City.md
|
|
431
|
+
return seg[0] + '/…/' + seg.slice(-1)[0]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 长文件快速定位:在 diff 上方挂一个「跳到改动」按钮,循环跳 add/del 行
|
|
435
|
+
function mountJumpBar (body) {
|
|
436
|
+
if (!body) return
|
|
437
|
+
const host = body.parentElement || body
|
|
438
|
+
const prev = host.querySelector('.zgp-jumpbar')
|
|
439
|
+
if (prev) prev.remove()
|
|
440
|
+
const marks = body.querySelectorAll('.zgp-dl.add, .zgp-dl.del')
|
|
441
|
+
if (!marks.length) return
|
|
442
|
+
const bar = doc.createElement('div')
|
|
443
|
+
bar.className = 'zgp-jumpbar'
|
|
444
|
+
const btn = doc.createElement('button')
|
|
445
|
+
btn.type = 'button'
|
|
446
|
+
btn.textContent = '跳到改动'
|
|
447
|
+
btn.title = '跳到下一处改动(共 ' + marks.length + ' 行改动)'
|
|
448
|
+
bar.appendChild(btn)
|
|
449
|
+
host.insertBefore(bar, body)
|
|
450
|
+
let idx = -1
|
|
451
|
+
btn.addEventListener('click', function (ev) {
|
|
452
|
+
ev.stopPropagation()
|
|
453
|
+
idx = (idx + 1) % marks.length
|
|
454
|
+
const el = marks[idx]
|
|
455
|
+
try { el.scrollIntoView({ block: 'center', behavior: 'smooth' }) } catch (e) { el.scrollIntoView() }
|
|
456
|
+
el.classList.add('zgp-flash')
|
|
457
|
+
setTimeout(function () { el.classList.remove('zgp-flash') }, 900)
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// diff 渲染:带行号
|
|
462
|
+
function renderDiffLines (lines) {
|
|
463
|
+
let oldNo = 0, newNo = 0
|
|
464
|
+
const out = []
|
|
465
|
+
for (let i = 0; i < lines.length; i++) {
|
|
466
|
+
const ln = lines[i]
|
|
467
|
+
let cls = 'meta', o = '', n = ''
|
|
468
|
+
if (ln.startsWith('@@')) {
|
|
469
|
+
const m = /@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(ln)
|
|
470
|
+
if (m) { oldNo = parseInt(m[1], 10); newNo = parseInt(m[2], 10) }
|
|
471
|
+
continue // hunk 头不显示
|
|
472
|
+
} else if (ln.startsWith('+')) { cls = 'add'; n = String(newNo++) }
|
|
473
|
+
else if (ln.startsWith('-')) { cls = 'del'; o = String(oldNo++) }
|
|
474
|
+
else if (ln.charCodeAt(0) === 92) { continue } // "" 不显示
|
|
475
|
+
else { cls = 'ctx'; o = String(oldNo++); n = String(newNo++) }
|
|
476
|
+
// 单列行号:改动后的行号;删除行用旧行号并淡显
|
|
477
|
+
const lnNo = n || o
|
|
478
|
+
out.push('<span class="zgp-dl ' + cls + (n ? '' : ' oldno') + '">' +
|
|
479
|
+
'<span class="zgp-ln">' + lnNo + '</span>' +
|
|
480
|
+
(esc(ln) || ' ') + '</span>')
|
|
481
|
+
}
|
|
482
|
+
return out.join('')
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 去掉 diff 的头部元信息(diff --git / index / --- / +++ 等),只留 hunk 内容
|
|
486
|
+
function stripDiffHeader (text) {
|
|
487
|
+
const lines = String(text == null ? '' : text).split(String.fromCharCode(10))
|
|
488
|
+
const out = []
|
|
489
|
+
let seenHunk = false
|
|
490
|
+
for (let i = 0; i < lines.length; i++) {
|
|
491
|
+
const ln = lines[i]
|
|
492
|
+
if (!seenHunk) {
|
|
493
|
+
if (ln.startsWith('@@')) { seenHunk = true; out.push(ln); continue }
|
|
494
|
+
if (/^(diff --git |index |--- |\+\+\+ |new file mode|deleted file mode|old mode |new mode |similarity index|rename from |rename to |Binary files )/.test(ln)) continue
|
|
495
|
+
if (ln.trim() === '') continue
|
|
496
|
+
out.push(ln)
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
out.push(ln)
|
|
500
|
+
}
|
|
501
|
+
return out
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function openView(title, content, isDiff) {
|
|
505
|
+
$('zgp-vtitle').textContent = title
|
|
506
|
+
const body = $('zgp-vbody')
|
|
507
|
+
if (isDiff) {
|
|
508
|
+
const lines = stripDiffHeader(content)
|
|
509
|
+
body.innerHTML = renderDiffLines(lines)
|
|
510
|
+
mountJumpBar(body)
|
|
511
|
+
} else {
|
|
512
|
+
body.innerHTML = '<span class="zgp-dl">' + esc(content || '(空)') + '</span>'
|
|
513
|
+
}
|
|
514
|
+
view.classList.add('on')
|
|
515
|
+
body.scrollTop = 0
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ============ 当前笔记 ============
|
|
519
|
+
function currentRel() {
|
|
520
|
+
try {
|
|
521
|
+
const inst = win.__znotesFindInstance && win.__znotesFindInstance('main')
|
|
522
|
+
const wn = inst && inst.state && inst.state.workNote
|
|
523
|
+
if (!wn || !wn._id || wn._id.indexOf('note/') !== 0) return null
|
|
524
|
+
const rel = wn._id.slice(5)
|
|
525
|
+
return G.relOf(WS + '/' + rel.split('/').join(require('path').sep))
|
|
526
|
+
} catch (e) { return null }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ============ 状态条 ============
|
|
530
|
+
function renderStatus(st) {
|
|
531
|
+
ensureStatusContainers()
|
|
532
|
+
const box = $('zgp-status')
|
|
533
|
+
if (!st || !st.isRepo) {
|
|
534
|
+
box.innerHTML = '<span class="zgp-chip warn">尚未初始化 git 仓库</span>'
|
|
535
|
+
const foot0 = doc.querySelector('.zgp-acts')
|
|
536
|
+
const oldE = doc.getElementById('zgp-foot-extra')
|
|
537
|
+
if (oldE) oldE.remove()
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
const parts = []
|
|
541
|
+
parts.push('<span class="zgp-chip acc">分支 <b>' + esc(st.detached ? '游离 HEAD' : (st.branch || '-')) + '</b></span>')
|
|
542
|
+
if (st.ahead) parts.push('<span class="zgp-chip">待推送 <b>' + st.ahead + '</b></span>')
|
|
543
|
+
if (st.behind) parts.push('<span class="zgp-chip warn">待拉取 <b>' + st.behind + '</b></span>')
|
|
544
|
+
if (st.dirty) parts.push('<span class="zgp-chip warn"><b>' + st.dirty + '</b> 项改动</span>')
|
|
545
|
+
else parts.push('<span class="zgp-chip ok">工作区干净</span>')
|
|
546
|
+
if (st.locked) parts.push('<span class="zgp-chip err">仓库被占用</span>')
|
|
547
|
+
if (st.inProgress) parts.push('<span class="zgp-chip err">' + esc(st.inProgress) + ' 进行中</span>')
|
|
548
|
+
if (st.busy) parts.push('<span class="zgp-chip"><span class="zgp-spin"></span>' + esc(st.currentOp || '执行中') + '</span>')
|
|
549
|
+
if (st._err) parts.push('<span class="zgp-chip err">状态读取失败:' + esc(String(st._err).slice(0, 60)) + '</span>')
|
|
550
|
+
if (st.failCount >= 3) parts.push('<span class="zgp-chip err">自动同步连续失败 ' + st.failCount + ' 次</span>')
|
|
551
|
+
let chipsBox = $('zgp-chips')
|
|
552
|
+
if (!chipsBox) {
|
|
553
|
+
chipsBox = doc.createElement('div')
|
|
554
|
+
chipsBox.className = 'zgp-chips'
|
|
555
|
+
chipsBox.id = 'zgp-chips'
|
|
556
|
+
box.insertBefore(chipsBox, box.firstChild)
|
|
557
|
+
}
|
|
558
|
+
chipsBox.innerHTML = parts.join('')
|
|
559
|
+
|
|
560
|
+
// 中断状态 / 陈旧锁:给出可操作按钮(不再让用户去终端)
|
|
561
|
+
const foot = doc.querySelector('.zgp-acts')
|
|
562
|
+
const oldExtra = doc.getElementById('zgp-foot-extra')
|
|
563
|
+
if (oldExtra) oldExtra.remove()
|
|
564
|
+
if (st.inProgress || (st.locked && st.lockAgeMs > 600000)) {
|
|
565
|
+
const wrap = doc.createElement('span')
|
|
566
|
+
wrap.id = 'zgp-foot-extra'
|
|
567
|
+
wrap.style.cssText = 'display:inline-flex;gap:6px;margin-left:6px'
|
|
568
|
+
if (st.inProgress) {
|
|
569
|
+
const b = doc.createElement('button')
|
|
570
|
+
b.className = 'zgp-b'
|
|
571
|
+
b.textContent = '中止 ' + st.inProgress
|
|
572
|
+
b.onclick = async () => {
|
|
573
|
+
const ok = await askConfirm('中止当前的 ' + st.inProgress + ' 操作并恢复到之前的状态?')
|
|
574
|
+
if (!ok) return
|
|
575
|
+
const r = await G.abortInProgress()
|
|
576
|
+
toast(r.ok ? ('已中止 ' + st.inProgress) : ('中止失败:' + r.message), r.ok ? 'ok' : 'err')
|
|
577
|
+
refresh()
|
|
578
|
+
}
|
|
579
|
+
wrap.appendChild(b)
|
|
580
|
+
}
|
|
581
|
+
if (st.locked && st.lockAgeMs > 600000) {
|
|
582
|
+
const b2 = doc.createElement('button')
|
|
583
|
+
b2.className = 'zgp-b'
|
|
584
|
+
b2.textContent = '清理陈旧锁'
|
|
585
|
+
b2.onclick = async () => {
|
|
586
|
+
const r = await G.clearStaleLock()
|
|
587
|
+
toast(r.ok ? '已清理陈旧 index.lock' : ('清理失败:' + r.message), r.ok ? 'ok' : 'err')
|
|
588
|
+
refresh()
|
|
589
|
+
}
|
|
590
|
+
wrap.appendChild(b2)
|
|
591
|
+
}
|
|
592
|
+
if (foot) foot.appendChild(wrap)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const badge = $('zgp-b1')
|
|
596
|
+
if (badge) {
|
|
597
|
+
if (st.dirty) { badge.textContent = String(st.dirty); badge.className = 'zgp-badge on' }
|
|
598
|
+
else { badge.textContent = ''; badge.className = 'zgp-badge' }
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ============ 改动页 ============
|
|
603
|
+
// 只表达"这个文件改了什么":新增 / 修改 / 删除 / 冲突
|
|
604
|
+
function stLetter(f) {
|
|
605
|
+
if (f.kind === 'untracked') return 'A'
|
|
606
|
+
if (f.kind === 'conflict' || f.x === 'U' || f.y === 'U') return 'U'
|
|
607
|
+
if (f.x === 'D' || f.y === 'D') return 'D'
|
|
608
|
+
if (f.x === '?' || f.y === '?') return 'A'
|
|
609
|
+
if (f.x === 'A' || f.y === 'A') return 'A'
|
|
610
|
+
return 'M'
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 未初始化仓库时的提示:配置统一在设置面板,这里只做跳转(不再有第二套表单)
|
|
614
|
+
function renderInit (pane) {
|
|
615
|
+
pane.innerHTML = `
|
|
616
|
+
<div class="zgp-sec" style="padding-bottom:8px">
|
|
617
|
+
<div style="font-size:12.5px;color:var(--zgp-fg2);line-height:1.8">
|
|
618
|
+
当前笔记目录还没有启用版本控制。<br>
|
|
619
|
+
<span style="color:var(--zgp-fg3);font-size:11.5px">目录:${esc(WS)}</span>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
<div class="zgp-sec">
|
|
623
|
+
<div class="zgp-row">
|
|
624
|
+
<button class="zgp-b primary" id="zgp-goto-settings">打开设置 · 版本控制</button>
|
|
625
|
+
</div>
|
|
626
|
+
<div class="zgp-hint">
|
|
627
|
+
在设置里可以选「仅本地记录历史」或「绑定远程仓库并同步」,初始化不会改动任何笔记内容。
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
`
|
|
631
|
+
const go = doc.getElementById('zgp-goto-settings')
|
|
632
|
+
if (go) {
|
|
633
|
+
go.onclick = () => {
|
|
634
|
+
try {
|
|
635
|
+
// 先收起 git 面板,避免两个 520px 面板叠在一起
|
|
636
|
+
try { root.classList.remove('open') } catch (e) {}
|
|
637
|
+
if (win.__zgsOpen) win.__zgsOpen()
|
|
638
|
+
else toast('设置面板不可用', 'err')
|
|
639
|
+
} catch (e) {
|
|
640
|
+
toast('打开设置失败:' + (e && e.message), 'err')
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// 读取全局 git 身份,仅用于提示
|
|
647
|
+
function globalIdentityHint() {
|
|
648
|
+
try {
|
|
649
|
+
const cp = require('child_process')
|
|
650
|
+
const n = (cp.execFileSync('git', ['config', '--global', 'user.name'], { encoding: 'utf8', windowsHide: true }) || '').trim()
|
|
651
|
+
const e = (cp.execFileSync('git', ['config', '--global', 'user.email'], { encoding: 'utf8', windowsHide: true }) || '').trim()
|
|
652
|
+
if (!n && !e) return '未配置'
|
|
653
|
+
return [n, e].filter(Boolean).join(' / ')
|
|
654
|
+
} catch (err) { return '未配置' }
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ===== 单文件模式:只显示当前笔记的改动与历史 =====
|
|
658
|
+
function renderFileMode(st, raw) {
|
|
659
|
+
// 单文件模式没有全局倒计时行,清掉上一次的残留(否则会留下看不见但仍可点的按钮)
|
|
660
|
+
const _c = doc.getElementById('zgp-countdown')
|
|
661
|
+
if (_c) { const _p = _c.closest('.zgp-autoline'); if (_p) _p.remove() }
|
|
662
|
+
const box = $('zgp-status')
|
|
663
|
+
const rel = currentRel()
|
|
664
|
+
if (!rel) {
|
|
665
|
+
const chips0 = box.querySelector('#zgp-chips')
|
|
666
|
+
if (chips0) chips0.innerHTML = '<span class="zgp-chip warn">没有打开的笔记</span>'
|
|
667
|
+
else box.innerHTML = '<span class="zgp-chip warn">没有打开的笔记</span>'
|
|
668
|
+
const pane = $('zgp-pane-changes')
|
|
669
|
+
if (pane) pane.innerHTML = '<div class="zgp-empty"><b>没有打开的笔记</b>打开一篇笔记后,这里显示它的改动与历史</div>'
|
|
670
|
+
const hp = $('zgp-pane-history')
|
|
671
|
+
if (hp) hp.innerHTML = '<div class="zgp-empty">打开一篇笔记后可以看到它的修改历史</div>'
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
const files = (st && st.files) || []
|
|
675
|
+
const file = files.find((f) => f.path === rel)
|
|
676
|
+
const readErr = raw && !raw.ok ? ('状态读取失败:' + (raw.message || '')) : ''
|
|
677
|
+
const chipsHtml = '<span class="zgp-chip acc">' + esc(displayPath(rel)) + '</span>'
|
|
678
|
+
+ (file ? '<span class="zgp-chip warn">有未提交改动</span>' : '<span class="zgp-chip ok">与最近提交一致</span>')
|
|
679
|
+
+ (readErr ? '<span class="zgp-chip err">' + esc(readErr.slice(0, 40)) + '</span>' : '')
|
|
680
|
+
// 只更新 chips,不要覆盖整个状态条(否则会删掉右侧操作区与容器)
|
|
681
|
+
const chipsBoxF = box.querySelector('#zgp-chips')
|
|
682
|
+
if (chipsBoxF) chipsBoxF.innerHTML = chipsHtml
|
|
683
|
+
else box.innerHTML = chipsHtml
|
|
684
|
+
renderFileChanges(rel, file)
|
|
685
|
+
if (activeTab === 'history') renderHistory(true)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// 状态条右侧:倒计时 + 立即推送 + 撤销改动(仅单文件模式、且有未提交改动时)
|
|
689
|
+
function renderFileActions(rel, hasChanges) {
|
|
690
|
+
const box = root.querySelector('#zgp-fileact')
|
|
691
|
+
if (!box) return
|
|
692
|
+
// 单文件模式:显示专属动作区,隐藏全局三按钮(不依赖 applyMode 的时序)
|
|
693
|
+
const _acts = root.querySelector('.zgp-acts')
|
|
694
|
+
if (_acts) _acts.style.display = 'none'
|
|
695
|
+
box.style.display = 'flex'
|
|
696
|
+
if (!hasChanges) { box.innerHTML = ''; return }
|
|
697
|
+
let cfg = {}
|
|
698
|
+
try { cfg = (G.config() || {}).data || {} } catch (e) {}
|
|
699
|
+
let html = ''
|
|
700
|
+
if (cfg.autoSync) {
|
|
701
|
+
let nx = null
|
|
702
|
+
try { nx = G.nextAutoAt ? G.nextAutoAt() : null } catch (e) {}
|
|
703
|
+
// 没有有效的下次时间(已触发过 / 等待下次编辑)就不显示倒计时,避免出现 "0 秒"
|
|
704
|
+
if (nx) {
|
|
705
|
+
html += '<span class="zgp-count" id="zgp-fcount">'
|
|
706
|
+
+ Math.max(0, Math.round((nx.at - Date.now()) / 1000)) + ' 秒后自动同步</span>'
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
html += '<button class="zgp-b primary" id="zgp-f-pushnow" title="提交这篇笔记的改动,然后拉取并推送">立即同步</button>'
|
|
710
|
+
html += '<button class="zgp-b" id="zgp-f-undo" title="丢弃此文件在工作区的改动">撤销改动</button>'
|
|
711
|
+
box.innerHTML = html
|
|
712
|
+
|
|
713
|
+
const pushBtn = $('zgp-f-pushnow')
|
|
714
|
+
if (pushBtn) {
|
|
715
|
+
pushBtn.onclick = async () => {
|
|
716
|
+
pushBtn.disabled = true
|
|
717
|
+
toast('正在推送…')
|
|
718
|
+
const r = await G.sync(null)
|
|
719
|
+
pushBtn.disabled = false
|
|
720
|
+
if (r.ok) {
|
|
721
|
+
toast('已推送:' + (r.data.steps.join(' · ') || '无改动'), 'ok')
|
|
722
|
+
logCache = null
|
|
723
|
+
refresh()
|
|
724
|
+
} else toast('推送失败:' + hintOf(r), 'err')
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const undoBtn = $('zgp-f-undo')
|
|
728
|
+
if (undoBtn) {
|
|
729
|
+
undoBtn.onclick = async () => {
|
|
730
|
+
const rr = await doRestoreHead(rel)
|
|
731
|
+
if (rr.ok) {
|
|
732
|
+
toast('已还原到最近一次提交', 'ok')
|
|
733
|
+
logCache = null
|
|
734
|
+
try { if (win.__znotesReloadNote) win.__znotesReloadNote(rel) } catch (e) {}
|
|
735
|
+
refresh()
|
|
736
|
+
} else if (rr.kind !== 'cancelled') {
|
|
737
|
+
toast('还原失败:' + hintOf(rr), 'err')
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function renderFileChanges(rel, file) {
|
|
744
|
+
const pane = $('zgp-pane-changes')
|
|
745
|
+
if (!file) {
|
|
746
|
+
pane.innerHTML = '<div class="zgp-empty"><b>没有未提交改动</b>这篇笔记与最近一次提交的内容一致</div>'
|
|
747
|
+
renderFileActions(rel, false)
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
// 已有 diff 内容时不先清空,避免每次刷新闪一下「读取差异…」
|
|
751
|
+
const existedBody = pane.querySelector('#zgp-f-diffbody')
|
|
752
|
+
if (!existedBody) pane.innerHTML = '<div class="zgp-empty">读取差异…</div>'
|
|
753
|
+
const r = await G.diff(null, rel)
|
|
754
|
+
const diff = r.ok ? (r.data || '(无差异)') : ('读取失败:' + r.message)
|
|
755
|
+
pane.innerHTML = '<div class="zgp-sec" style="padding:0"><div id="zgp-f-diffbody"></div></div>'
|
|
756
|
+
renderFileActions(rel, true)
|
|
757
|
+
const body = $('zgp-f-diffbody')
|
|
758
|
+
const lines = stripDiffHeader(diff)
|
|
759
|
+
body.innerHTML = renderDiffLines(lines)
|
|
760
|
+
mountJumpBar(body)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function renderChanges(st) {
|
|
764
|
+
const pane = $('zgp-pane-changes')
|
|
765
|
+
// 全局模式:显示三按钮,隐藏单文件动作区
|
|
766
|
+
const _fAct = root.querySelector('#zgp-fileact')
|
|
767
|
+
const _acts = root.querySelector('.zgp-acts')
|
|
768
|
+
if (_fAct) _fAct.style.display = 'none'
|
|
769
|
+
if (_acts) _acts.style.display = 'flex'
|
|
770
|
+
if (!st || !st.isRepo) {
|
|
771
|
+
renderInit(pane)
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
if (!st.files.length) {
|
|
775
|
+
pane.innerHTML = '<div class="zgp-empty"><b>工作区干净</b>没有未提交的改动</div>'
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
const cur = currentRel()
|
|
779
|
+
const rows = st.files.map((f, i) => {
|
|
780
|
+
const L = stLetter(f)
|
|
781
|
+
const isCur = cur && f.path === cur
|
|
782
|
+
return '<div class="zgp-file' + (isCur ? ' cur' : '') + '" data-i="' + i + '">'
|
|
783
|
+
+ '<span class="zgp-st ' + L + '">' + L + '</span>'
|
|
784
|
+
+ '<span class="zgp-fp" title="' + esc(displayPath(f.path)) + '"><span>' + esc(shortPath(displayPath(f.path))) + '</span></span>'
|
|
785
|
+
+ '<span class="zgp-ops">'
|
|
786
|
+
+ '<button class="zgp-ic" data-act="diff" data-i="' + i + '" title="查看差异">' + ICON.diff + '</button>'
|
|
787
|
+
+ '<button class="zgp-ic danger" data-act="discard" data-i="' + i + '" title="丢弃此文件在工作区的改动">' + ICON.undo + '</button>'
|
|
788
|
+
+ '</span></div>'
|
|
789
|
+
}).join('')
|
|
790
|
+
pane.innerHTML = rows
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
pane.querySelectorAll('[data-act]').forEach((el) => {
|
|
794
|
+
el.onclick = async (ev) => {
|
|
795
|
+
ev.stopPropagation()
|
|
796
|
+
const i = parseInt(el.getAttribute('data-i'), 10)
|
|
797
|
+
const f = st.files[i]
|
|
798
|
+
const act = el.getAttribute('data-act')
|
|
799
|
+
if (act === 'diff') {
|
|
800
|
+
const r = await G.diff(null, f.path)
|
|
801
|
+
openView('未提交差异 · ' + f.path, r.ok ? r.data : ('读取失败:' + r.message), true)
|
|
802
|
+
} else if (act === 'discard') {
|
|
803
|
+
const r = await doRestoreHead(f.path)
|
|
804
|
+
if (r.ok) {
|
|
805
|
+
toast('已还原到最近一次提交', 'ok')
|
|
806
|
+
// 编辑器内部可能残留旧内容并触发自动保存,把还原结果又写回磁盘;
|
|
807
|
+
// 整页刷新是唯一可靠的做法(还原属低频操作)
|
|
808
|
+
try { if (win.__znotesReloadNote) win.__znotesReloadNote(f.path) } catch (err) {}
|
|
809
|
+
refresh()
|
|
810
|
+
} else if (r.kind !== 'cancelled') {
|
|
811
|
+
toast('还原失败:' + hintOf(r), 'err')
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ============ 历史页 ============
|
|
819
|
+
let logCache = null
|
|
820
|
+
let filePage = 0 // 单文件历史的当前页(0 起)
|
|
821
|
+
let fileLogRel = null // 当前分页对应的笔记,换笔记时重置页码
|
|
822
|
+
const FILE_PAGE_SIZE = 10
|
|
823
|
+
const REPO_PAGE_SIZE = 20
|
|
824
|
+
let repoPage = 0
|
|
825
|
+
let lastFileSig = ''
|
|
826
|
+
async function renderHistory(force) {
|
|
827
|
+
const pane = $('zgp-pane-history')
|
|
828
|
+
if (!pane) return
|
|
829
|
+
// 单文件模式:只列这篇笔记自己的提交历史
|
|
830
|
+
if (panelMode === 'file') {
|
|
831
|
+
const rel = currentRel()
|
|
832
|
+
if (!rel) {
|
|
833
|
+
pane.innerHTML = '<div class="zgp-empty">没有打开的笔记</div>'
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
// 换笔记时把页码归零
|
|
837
|
+
if (rel !== fileLogRel) { fileLogRel = rel; filePage = 0; logCache = null }
|
|
838
|
+
const cacheKey = rel + '#' + filePage
|
|
839
|
+
if (!force && logCache && logCache._key === cacheKey) {
|
|
840
|
+
renderFileLogPane(pane, rel, logCache._list, logCache._total)
|
|
841
|
+
return
|
|
842
|
+
}
|
|
843
|
+
pane.innerHTML = '<div class="zgp-empty">读取修订记录…</div>'
|
|
844
|
+
const cntR = await G.logCount(rel)
|
|
845
|
+
const total = cntR.ok ? cntR.data : 0
|
|
846
|
+
const rf = await G.log(rel, FILE_PAGE_SIZE, filePage * FILE_PAGE_SIZE)
|
|
847
|
+
if (!rf.ok) {
|
|
848
|
+
pane.innerHTML = '<div class="zgp-empty"><b>读取失败</b>' + esc(rf.message) + '</div>'
|
|
849
|
+
return
|
|
850
|
+
}
|
|
851
|
+
logCache = rf.data
|
|
852
|
+
logCache._key = cacheKey
|
|
853
|
+
logCache._list = rf.data
|
|
854
|
+
logCache._total = total
|
|
855
|
+
renderFileLogPane(pane, rel, rf.data, total)
|
|
856
|
+
return
|
|
857
|
+
}
|
|
858
|
+
if (!force && logCache && !logCache._rel) { renderRepoLogPane(pane, logCache, logCache._total || logCache.length); return }
|
|
859
|
+
pane.innerHTML = '<div class="zgp-empty">读取提交历史…</div>'
|
|
860
|
+
const cntR = await G.repoLogCount()
|
|
861
|
+
const totalRepo = cntR.ok ? cntR.data : 0
|
|
862
|
+
const r = await G.repoLog(REPO_PAGE_SIZE, repoPage * REPO_PAGE_SIZE)
|
|
863
|
+
if (!r.ok) {
|
|
864
|
+
pane.innerHTML = '<div class="zgp-empty"><b>读取失败</b>' + esc(r.message) + '</div>'
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
logCache = r.data
|
|
868
|
+
logCache._total = totalRepo
|
|
869
|
+
renderRepoLogPane(pane, logCache, totalRepo)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// 单文件历史:标题 + 列表 + 分页
|
|
873
|
+
// 全局历史:列表 + 分页(按钮在上,统计在下)
|
|
874
|
+
function renderRepoLogPane(pane, list, total) {
|
|
875
|
+
const pages = Math.max(1, Math.ceil(total / REPO_PAGE_SIZE))
|
|
876
|
+
// 没有记录时不显示分页统计
|
|
877
|
+
const pager = total > 0
|
|
878
|
+
? ('<div class="zgp-sec" style="display:flex;flex-direction:column;align-items:center;gap:7px;'
|
|
879
|
+
+ 'padding:10px 16px;border-top:1px solid var(--zgp-bd)">'
|
|
880
|
+
+ (pages > 1
|
|
881
|
+
? ('<div style="display:flex;align-items:center;gap:10px">'
|
|
882
|
+
+ '<button class="zgp-b" data-rpg="prev"' + (repoPage <= 0 ? ' disabled' : '') + '>上一页</button>'
|
|
883
|
+
+ '<span style="font-size:12px;color:var(--zgp-fg2);white-space:nowrap">第 ' + (repoPage + 1) + ' / ' + pages + ' 页</span>'
|
|
884
|
+
+ '<button class="zgp-b" data-rpg="next"' + (repoPage >= pages - 1 ? ' disabled' : '') + '>下一页</button>'
|
|
885
|
+
+ '</div>')
|
|
886
|
+
: '')
|
|
887
|
+
+ '<span style="font-size:12px;color:var(--zgp-fg3)">共有 ' + total + ' 条提交记录</span>'
|
|
888
|
+
+ '</div>')
|
|
889
|
+
: ''
|
|
890
|
+
pane.innerHTML = (list && list.length)
|
|
891
|
+
? '<div id="zgp-repolog"></div>' + pager
|
|
892
|
+
: '<div class="zgp-empty">暂无提交记录</div>' + pager
|
|
893
|
+
const body = doc.getElementById('zgp-repolog')
|
|
894
|
+
if (body) paintLog(body, list)
|
|
895
|
+
const prev = pane.querySelector('[data-rpg="prev"]')
|
|
896
|
+
const next = pane.querySelector('[data-rpg="next"]')
|
|
897
|
+
if (prev) prev.onclick = () => { if (repoPage > 0) { repoPage--; logCache = null; renderHistory(true) } }
|
|
898
|
+
if (next) next.onclick = () => { if (repoPage < pages - 1) { repoPage++; logCache = null; renderHistory(true) } }
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function renderFileLogPane(pane, rel, list, total) {
|
|
902
|
+
const pages = Math.max(1, Math.ceil(total / FILE_PAGE_SIZE))
|
|
903
|
+
// 分页按钮在上,统计信息在下;没有记录时整块不显示
|
|
904
|
+
const pager = total > 0
|
|
905
|
+
? ('<div class="zgp-sec" style="display:flex;flex-direction:column;align-items:center;gap:7px;'
|
|
906
|
+
+ 'padding:10px 16px;border-top:1px solid var(--zgp-bd)">'
|
|
907
|
+
+ (pages > 1
|
|
908
|
+
? ('<div style="display:flex;align-items:center;gap:10px">'
|
|
909
|
+
+ '<button class="zgp-b" data-pg="prev"' + (filePage <= 0 ? ' disabled' : '') + '>上一页</button>'
|
|
910
|
+
+ '<span style="font-size:12px;color:var(--zgp-fg2);white-space:nowrap">第 ' + (filePage + 1) + ' / ' + pages + ' 页</span>'
|
|
911
|
+
+ '<button class="zgp-b" data-pg="next"' + (filePage >= pages - 1 ? ' disabled' : '') + '>下一页</button>'
|
|
912
|
+
+ '</div>')
|
|
913
|
+
: '')
|
|
914
|
+
+ '<span style="font-size:12px;color:var(--zgp-fg3)">共有 ' + total + ' 条修改记录</span>'
|
|
915
|
+
+ '</div>')
|
|
916
|
+
: ''
|
|
917
|
+
pane.innerHTML = (list && list.length)
|
|
918
|
+
? '<div id="zgp-filelog"></div>' + pager
|
|
919
|
+
: '<div class="zgp-empty">暂无修改记录</div>' + pager
|
|
920
|
+
const body = doc.getElementById('zgp-filelog')
|
|
921
|
+
if (body) paintLog(body, list, rel)
|
|
922
|
+
const prev = pane.querySelector('[data-pg="prev"]')
|
|
923
|
+
const next = pane.querySelector('[data-pg="next"]')
|
|
924
|
+
if (prev) prev.onclick = () => { if (filePage > 0) { filePage--; logCache = null; renderHistory(true) } }
|
|
925
|
+
if (next) next.onclick = () => { if (filePage < pages - 1) { filePage++; logCache = null; renderHistory(true) } }
|
|
926
|
+
}
|
|
927
|
+
function paintLog(pane, list, onlyFile) {
|
|
928
|
+
if (!list.length) {
|
|
929
|
+
pane.innerHTML = '<div class="zgp-empty"><b>还没有提交记录</b>做一次同步就会产生第一条提交</div>'
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
const cur = currentRel()
|
|
933
|
+
pane.innerHTML = list.map((c, i) => {
|
|
934
|
+
// 单文件模式:不再展开文件清单,直接给该版本的操作
|
|
935
|
+
const ops = onlyFile
|
|
936
|
+
? '<div class="zgp-ops" style="margin-top:7px;opacity:1;align-items:center">'
|
|
937
|
+
+ '<button class="zgp-ic" data-cmpwork="' + i + '" title="与工作区比对:这次提交之后到现在,这篇笔记改了什么">' + ICON.cmpWork + '</button>'
|
|
938
|
+
+ '<button class="zgp-ic" data-cmpprev="' + i + '" title="与提交前比对:这一次提交本身对这篇笔记做了什么改动">' + ICON.cmpPrev + '</button>'
|
|
939
|
+
+ '<button class="zgp-ic" data-view="' + i + '" title="查看该版本:显示这次提交时这篇笔记的完整内容">' + ICON.eye + '</button>'
|
|
940
|
+
+ '<span style="flex:1"></span>'
|
|
941
|
+
+ '<button class="zgp-ic danger" data-back="' + i + '" title="回溯到此版本:把这篇笔记恢复成这个版本的内容(改动前自动备份)">' + ICON.undo + '</button>'
|
|
942
|
+
+ '</div>'
|
|
943
|
+
: ''
|
|
944
|
+
return '<div class="zgp-cm" data-c="' + i + '">'
|
|
945
|
+
+ '<div class="zgp-cm1">'
|
|
946
|
+
+ '<span class="zgp-cm-sub">' + esc(c.subject || '(无提交信息)') + '</span>'
|
|
947
|
+
+ '<span class="zgp-hash">' + esc(c.short) + '</span>'
|
|
948
|
+
+ '</div>'
|
|
949
|
+
+ '<div class="zgp-cm-meta">' + esc(c.date) + ' · ' + esc(c.author) + '</div>'
|
|
950
|
+
+ ops
|
|
951
|
+
+ (onlyFile ? '' : '<div class="zgp-cmbody" data-body="' + i + '"></div>')
|
|
952
|
+
+ '</div>'
|
|
953
|
+
}).join('')
|
|
954
|
+
|
|
955
|
+
// 单文件模式:按钮直接响应,不再走展开逻辑
|
|
956
|
+
if (onlyFile) {
|
|
957
|
+
pane.querySelectorAll('[data-cmpwork]').forEach((b) => {
|
|
958
|
+
b.onclick = async (ev) => {
|
|
959
|
+
ev.stopPropagation()
|
|
960
|
+
const c = list[parseInt(b.getAttribute('data-cmpwork'), 10)]
|
|
961
|
+
const r = await G.diffWork(c.hash, onlyFile)
|
|
962
|
+
openView('与工作区比对 · ' + c.short + ' → 当前文件', r.ok ? (r.data || '(无差异)') : ('读取失败:' + r.message), true)
|
|
963
|
+
}
|
|
964
|
+
})
|
|
965
|
+
pane.querySelectorAll('[data-cmpprev]').forEach((b) => {
|
|
966
|
+
b.onclick = async (ev) => {
|
|
967
|
+
ev.stopPropagation()
|
|
968
|
+
const c = list[parseInt(b.getAttribute('data-cmpprev'), 10)]
|
|
969
|
+
const r = await G.diff(c.hash, onlyFile)
|
|
970
|
+
openView('与提交前比对 · ' + c.short + ' 这次提交的改动', r.ok ? (r.data || '(无差异)') : ('读取失败:' + r.message), true)
|
|
971
|
+
}
|
|
972
|
+
})
|
|
973
|
+
pane.querySelectorAll('[data-view]').forEach((b) => {
|
|
974
|
+
b.onclick = async (ev) => {
|
|
975
|
+
ev.stopPropagation()
|
|
976
|
+
const c = list[parseInt(b.getAttribute('data-view'), 10)]
|
|
977
|
+
const r = await G.show(c.hash, onlyFile)
|
|
978
|
+
openView('查看 ' + c.short + ' · ' + c.date, r.ok ? r.data : ('读取失败:' + r.message), false)
|
|
979
|
+
}
|
|
980
|
+
})
|
|
981
|
+
pane.querySelectorAll('[data-back]').forEach((b) => {
|
|
982
|
+
b.onclick = async (ev) => {
|
|
983
|
+
ev.stopPropagation()
|
|
984
|
+
const c = list[parseInt(b.getAttribute('data-back'), 10)]
|
|
985
|
+
const r = await doRestore(c.hash, onlyFile, c.short + '(' + c.date + ')')
|
|
986
|
+
if (r.ok) {
|
|
987
|
+
toast('已回溯到 ' + c.short, 'ok')
|
|
988
|
+
logCache = null
|
|
989
|
+
try { if (win.__znotesReloadNote) win.__znotesReloadNote(onlyFile) } catch (e) {}
|
|
990
|
+
refresh()
|
|
991
|
+
} else if (r.kind !== 'cancelled') {
|
|
992
|
+
toast('回溯失败:' + hintOf(r), 'err')
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
pane.querySelectorAll('.zgp-cm').forEach((el) => {
|
|
999
|
+
el.onclick = async (ev) => {
|
|
1000
|
+
if (onlyFile) return
|
|
1001
|
+
if (ev.target.closest('.zgp-cmf')) return
|
|
1002
|
+
const i = parseInt(el.getAttribute('data-c'), 10)
|
|
1003
|
+
const c = list[i]
|
|
1004
|
+
const body = el.querySelector('[data-body]')
|
|
1005
|
+
if (el.classList.contains('open')) { el.classList.remove('open'); return }
|
|
1006
|
+
el.classList.add('open')
|
|
1007
|
+
if (body.dataset.loaded) return
|
|
1008
|
+
body.innerHTML = '<div style="padding:4px 8px;font-size:11.5px;color:var(--zgp-fg3)">读取文件列表…</div>'
|
|
1009
|
+
const r = await G.commitFiles(c.hash)
|
|
1010
|
+
if (!r.ok) { body.innerHTML = '<div style="padding:4px 8px;color:var(--zgp-err);font-size:11.5px">' + esc(r.message) + '</div>'; return }
|
|
1011
|
+
const files = r.data
|
|
1012
|
+
if (!files.length) {
|
|
1013
|
+
body.innerHTML = '<div style="padding:4px 8px;font-size:11.5px;color:var(--zgp-fg3)">该提交没有文件改动</div>'
|
|
1014
|
+
} else {
|
|
1015
|
+
body.innerHTML = files.map((f) => {
|
|
1016
|
+
const isCur = cur && f.path === cur
|
|
1017
|
+
return '<div class="zgp-cmf" data-h="' + esc(c.hash) + '" data-p="' + esc(f.path) + '"'
|
|
1018
|
+
+ (isCur ? ' style="background:var(--zgp-acc-bg);color:var(--zgp-acc)"' : '') + '>'
|
|
1019
|
+
+ '<span class="zgp-fs ' + (f.status || '').charAt(0) + '">' + (f.status || '').charAt(0) + '</span>'
|
|
1020
|
+
+ '<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(f.path) + '">' + esc(f.path) + '</span>'
|
|
1021
|
+
+ '<button class="zgp-ic" title="查看该次改动">' + ICON.diff + '</button>'
|
|
1022
|
+
+ '<button class="zgp-ic danger" data-restore="1" title="回溯到该版本(先备份)">' + ICON.undo + '</button>'
|
|
1023
|
+
+ '</div>'
|
|
1024
|
+
}).join('')
|
|
1025
|
+
}
|
|
1026
|
+
body.dataset.loaded = '1'
|
|
1027
|
+
|
|
1028
|
+
body.querySelectorAll('.zgp-cmf').forEach((row) => {
|
|
1029
|
+
const hash = row.getAttribute('data-h')
|
|
1030
|
+
const p = row.getAttribute('data-p')
|
|
1031
|
+
const iDiff = row.querySelector('.zgp-ic:not([data-restore])')
|
|
1032
|
+
const iRest = row.querySelector('[data-restore]')
|
|
1033
|
+
if (iDiff) iDiff.onclick = async (e) => {
|
|
1034
|
+
e.stopPropagation()
|
|
1035
|
+
const r2 = await G.diff(hash, p)
|
|
1036
|
+
openView('改动 ' + hash.slice(0, 7) + ' · ' + p, r2.ok ? r2.data : ('读取失败:' + r2.message), true)
|
|
1037
|
+
}
|
|
1038
|
+
if (iRest) iRest.onclick = async (e) => {
|
|
1039
|
+
e.stopPropagation()
|
|
1040
|
+
const r3 = await doRestore(hash, p, hash.slice(0, 7))
|
|
1041
|
+
if (r3.ok) {
|
|
1042
|
+
toast('已回溯到 ' + hash.slice(0, 7), 'ok')
|
|
1043
|
+
logCache = null
|
|
1044
|
+
try { if (win.__znotesReloadNote) win.__znotesReloadNote(p) } catch (err) {}
|
|
1045
|
+
refresh()
|
|
1046
|
+
} else if (r3.kind !== 'cancelled') {
|
|
1047
|
+
toast('回溯失败:' + hintOf(r3), 'err')
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
})
|
|
1051
|
+
}
|
|
1052
|
+
})
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
// ============ 刷新 ============
|
|
1057
|
+
let refreshing = false
|
|
1058
|
+
let pendingForce = false
|
|
1059
|
+
let lastGoodState = null
|
|
1060
|
+
let lastSig = ''
|
|
1061
|
+
async function refresh(force) {
|
|
1062
|
+
// 轮询正在进行时,force 请求排队,等这一轮结束后立刻补跑
|
|
1063
|
+
if (refreshing) { if (force) pendingForce = true; return }
|
|
1064
|
+
refreshing = true
|
|
1065
|
+
try {
|
|
1066
|
+
const r = await G.state()
|
|
1067
|
+
// 状态签名去重:改动/分支/页签都没变时不重绘,避免闪烁
|
|
1068
|
+
if (r.ok && panelMode === 'global' && activeTab === 'changes') {
|
|
1069
|
+
const f = r.data.files || []
|
|
1070
|
+
const sig = r.data.branch + '|' + r.data.dirty + '|' + f.map((x) => x.path + x.x + x.y).join(',')
|
|
1071
|
+
if (!force && sig === lastSig) { renderStatus(r.data); return }
|
|
1072
|
+
lastSig = sig
|
|
1073
|
+
}
|
|
1074
|
+
if (panelMode === 'file') {
|
|
1075
|
+
const stAny = r.ok ? r.data : (lastGoodState || {})
|
|
1076
|
+
if (r.ok) lastGoodState = r.data
|
|
1077
|
+
// 签名去重:当前文件的改动状态没变就不重绘,避免每 3 秒闪一下
|
|
1078
|
+
const relF = currentRel() || ''
|
|
1079
|
+
const ff = ((r.ok ? (r.data.files || []) : []).find((x) => x.path === relF))
|
|
1080
|
+
const sigF = relF + '|' + (ff ? ff.x + ff.y : 'clean') + '|' + (r.ok ? 'ok' : 'err')
|
|
1081
|
+
if (!force && sigF === lastFileSig) return
|
|
1082
|
+
lastFileSig = sigF
|
|
1083
|
+
renderFileMode(stAny, r)
|
|
1084
|
+
return
|
|
1085
|
+
}
|
|
1086
|
+
if (r.ok) {
|
|
1087
|
+
lastGoodState = r.data
|
|
1088
|
+
renderStatus(r.data)
|
|
1089
|
+
renderChanges(r.data)
|
|
1090
|
+
} else {
|
|
1091
|
+
// 读不到状态时保留上一次的列表,只额外标出错误,避免显示"工作区干净"的假象
|
|
1092
|
+
const merged = Object.assign({}, lastGoodState || { isRepo: true }, { _err: r.message, _errKind: r.kind })
|
|
1093
|
+
renderStatus(merged)
|
|
1094
|
+
if (!lastGoodState) renderChanges({ isRepo: false })
|
|
1095
|
+
}
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
console.error('[zgp] refresh', e)
|
|
1098
|
+
} finally {
|
|
1099
|
+
refreshing = false
|
|
1100
|
+
if (pendingForce) { pendingForce = false; setTimeout(() => { try { refresh(true) } catch (e) {} }, 20) }
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// ============ 事件 ============
|
|
1105
|
+
let activeTab = 'changes'
|
|
1106
|
+
let panelMode = 'global' // global:全部改动/历史;file:只看当前笔记
|
|
1107
|
+
|
|
1108
|
+
function switchTab(name) {
|
|
1109
|
+
activeTab = name
|
|
1110
|
+
doc.querySelectorAll('.zgp-tab').forEach((x) => x.classList.toggle('on', x.getAttribute('data-tab') === name))
|
|
1111
|
+
doc.querySelectorAll('.zgp-pane').forEach((x) => x.classList.toggle('on', x.id === 'zgp-pane-' + name))
|
|
1112
|
+
// 切到改动页立即重新校验,不用等 3 秒轮询
|
|
1113
|
+
if (name === 'changes') refresh(true)
|
|
1114
|
+
// 切到历史页总是重新读取:提交/推送后必须看到最新记录,不能用旧缓存
|
|
1115
|
+
if (name === 'history') renderHistory(true)
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// 单文件模式下隐藏底部全局操作区(设置页已独立到侧边栏入口)
|
|
1119
|
+
function applyMode() {
|
|
1120
|
+
const isFile = panelMode === 'file'
|
|
1121
|
+
// 面板标题:外部(工作区变更为全局改动/历史)vs 内部(当前文件)
|
|
1122
|
+
const titleEl = doc.getElementById('zgp-title-text')
|
|
1123
|
+
if (titleEl) titleEl.textContent = isFile ? '文档变更记录' : '工作区变更记录'
|
|
1124
|
+
// 单文件模式不显示全仓改动数(与当前文件无关)
|
|
1125
|
+
if (isFile) {
|
|
1126
|
+
const badge = doc.getElementById('zgp-b1')
|
|
1127
|
+
if (badge) { badge.textContent = ''; badge.className = 'zgp-badge' }
|
|
1128
|
+
}
|
|
1129
|
+
const foot = root.querySelector('.zgp-acts')
|
|
1130
|
+
if (foot) foot.style.display = isFile ? 'none' : ''
|
|
1131
|
+
const fAct = root.querySelector('#zgp-fileact')
|
|
1132
|
+
if (fAct) fAct.style.display = isFile ? 'flex' : 'none'
|
|
1133
|
+
if (!isFile && fAct) fAct.innerHTML = ''
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
doc.querySelectorAll('.zgp-tab').forEach((t) => {
|
|
1137
|
+
t.onclick = () => switchTab(t.getAttribute('data-tab'))
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
$('zgp-close').onclick = () => root.classList.remove('open')
|
|
1141
|
+
$('zgp-vclose').onclick = () => view.classList.remove('on')
|
|
1142
|
+
view.onclick = (e) => { if (e.target === view) view.classList.remove('on') }
|
|
1143
|
+
// 焦点/鼠标回到笔记正文时自动收起面板(点面板内部不会触发)
|
|
1144
|
+
function autoHideIfEditor(target) {
|
|
1145
|
+
if (!root.classList.contains('open')) return
|
|
1146
|
+
// target 可能是文本节点,取最近的元素
|
|
1147
|
+
var el = target && target.nodeType === 1 ? target : (target && target.parentElement) || null
|
|
1148
|
+
if (!el) return
|
|
1149
|
+
// 面板自身、diff 大窗、toast 都算"面板内"
|
|
1150
|
+
if (el.closest('#zgp-root') || el.closest('#zgp-view') || el.closest('#zgp-toast')) return
|
|
1151
|
+
// 编辑器、可编辑区,以及"没有打开笔记"时的空白区(.worknote-empty,flex:1 撑满右侧)
|
|
1152
|
+
// 点击这些位置都收起面板
|
|
1153
|
+
if (el.closest('.editor-body') || el.closest('[contenteditable="true"]') || el.closest('.worknote-empty')) {
|
|
1154
|
+
root.classList.remove('open')
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
doc.addEventListener('focusin', (e) => autoHideIfEditor(e.target), true)
|
|
1159
|
+
doc.addEventListener('mousedown', (e) => autoHideIfEditor(e.target), true)
|
|
1160
|
+
|
|
1161
|
+
// 倒计时每秒刷新(不整块重绘,只改文本)
|
|
1162
|
+
setInterval(() => {
|
|
1163
|
+
if (!root.classList.contains('open')) return
|
|
1164
|
+
if (panelMode !== 'file' && activeTab !== 'changes') return
|
|
1165
|
+
// file 模式的倒计时
|
|
1166
|
+
const fEl = doc.getElementById('zgp-fcount')
|
|
1167
|
+
if (fEl) {
|
|
1168
|
+
try {
|
|
1169
|
+
const nxF = G.nextAutoAt ? G.nextAutoAt() : null
|
|
1170
|
+
if (nxF) {
|
|
1171
|
+
fEl.style.display = ''
|
|
1172
|
+
fEl.textContent = Math.max(0, Math.round((nxF.at - Date.now()) / 1000)) + ' 秒后自动同步'
|
|
1173
|
+
} else {
|
|
1174
|
+
fEl.style.display = 'none'
|
|
1175
|
+
}
|
|
1176
|
+
} catch (e) {}
|
|
1177
|
+
return
|
|
1178
|
+
}
|
|
1179
|
+
const el = doc.getElementById('zgp-countdown')
|
|
1180
|
+
if (!el) return
|
|
1181
|
+
try {
|
|
1182
|
+
const nx = G.nextAutoAt && G.nextAutoAt()
|
|
1183
|
+
if (!nx) return // 静态文案(停止编辑模式)保持不动
|
|
1184
|
+
el.textContent = Math.max(0, Math.round((nx.at - Date.now()) / 1000)) + ' 秒后自动同步'
|
|
1185
|
+
} catch (e) {}
|
|
1186
|
+
}, 1000)
|
|
1187
|
+
|
|
1188
|
+
doc.addEventListener('keydown', (e) => {
|
|
1189
|
+
if (e.key === 'Escape') {
|
|
1190
|
+
if (view.classList.contains('on')) { view.classList.remove('on'); return }
|
|
1191
|
+
if (root.classList.contains('open')) root.classList.remove('open')
|
|
1192
|
+
}
|
|
1193
|
+
})
|
|
1194
|
+
|
|
1195
|
+
function busyUI(on, label) {
|
|
1196
|
+
doc.querySelectorAll('.zgp-acts .zgp-b').forEach((b) => { b.disabled = !!on })
|
|
1197
|
+
if (on) toast((label || '同步') + '…')
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// 状态条三个操作按钮的绑定(容器可能被自愈重建,所以做成可重复调用)
|
|
1201
|
+
function bindStatusButtons() {
|
|
1202
|
+
const syncBtn = $('zgp-sync')
|
|
1203
|
+
if (syncBtn) syncBtn.onclick = async () => {
|
|
1204
|
+
busyUI(true, '同步中')
|
|
1205
|
+
const r = await G.sync(null, { pull: true, push: true })
|
|
1206
|
+
busyUI(false)
|
|
1207
|
+
if (r.ok) {
|
|
1208
|
+
toast('同步完成:' + (r.data.steps.length ? r.data.steps.join(' → ') : '无改动'), 'ok')
|
|
1209
|
+
logCache = null
|
|
1210
|
+
} else if (r.kind === 'nothing') {
|
|
1211
|
+
toast('没有需要提交的改动', 'ok')
|
|
1212
|
+
} else {
|
|
1213
|
+
toast('同步失败:' + hintOf(r), 'err')
|
|
1214
|
+
}
|
|
1215
|
+
refresh()
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const pushBtn = $('zgp-push')
|
|
1219
|
+
if (pushBtn) pushBtn.onclick = async () => {
|
|
1220
|
+
busyUI(true, '推送中')
|
|
1221
|
+
const r = await G.push()
|
|
1222
|
+
busyUI(false)
|
|
1223
|
+
toast(r.ok ? '已推送' : ('推送失败:' + hintOf(r)), r.ok ? 'ok' : 'err')
|
|
1224
|
+
refresh()
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const pullBtn = $('zgp-pull')
|
|
1228
|
+
if (pullBtn) pullBtn.onclick = async () => {
|
|
1229
|
+
busyUI(true, '拉取中')
|
|
1230
|
+
const r = await G.pull()
|
|
1231
|
+
busyUI(false)
|
|
1232
|
+
toast(r.ok ? '已拉取' : ('拉取失败:' + hintOf(r)), r.ok ? 'ok' : 'err')
|
|
1233
|
+
logCache = null
|
|
1234
|
+
refresh()
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
bindStatusButtons()
|
|
1238
|
+
|
|
1239
|
+
// 状态条容器自愈:面板可能由旧版本创建,缺 chips / acts / fileact 时补回来
|
|
1240
|
+
function ensureStatusContainers() {
|
|
1241
|
+
const status = $('zgp-status')
|
|
1242
|
+
if (!status) return
|
|
1243
|
+
let changed = false
|
|
1244
|
+
if (!$('zgp-chips')) {
|
|
1245
|
+
const d = doc.createElement('div')
|
|
1246
|
+
d.className = 'zgp-chips'
|
|
1247
|
+
d.id = 'zgp-chips'
|
|
1248
|
+
status.insertBefore(d, status.firstChild)
|
|
1249
|
+
changed = true
|
|
1250
|
+
}
|
|
1251
|
+
if (!status.querySelector('.zgp-acts')) {
|
|
1252
|
+
const d = doc.createElement('div')
|
|
1253
|
+
d.className = 'zgp-acts'
|
|
1254
|
+
d.innerHTML = '<button class="zgp-b" id="zgp-pull" title="从远端拉取提交">' + ICON.down + '<span>拉取</span></button>'
|
|
1255
|
+
+ '<button class="zgp-b" id="zgp-push" title="把本地提交推送到远端">' + ICON.up + '<span>推送</span></button>'
|
|
1256
|
+
+ '<button class="zgp-b primary" id="zgp-sync" title="提交本地改动并推送">' + ICON.sync + '<span>同步</span></button>'
|
|
1257
|
+
status.appendChild(d)
|
|
1258
|
+
changed = true
|
|
1259
|
+
}
|
|
1260
|
+
if (!$('zgp-fileact')) {
|
|
1261
|
+
const d = doc.createElement('div')
|
|
1262
|
+
d.className = 'zgp-fileact'
|
|
1263
|
+
d.id = 'zgp-fileact'
|
|
1264
|
+
status.appendChild(d)
|
|
1265
|
+
changed = true
|
|
1266
|
+
}
|
|
1267
|
+
if (changed) {
|
|
1268
|
+
try { bindStatusButtons() } catch (e) {}
|
|
1269
|
+
try { applyMode() } catch (e2) {}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
// ============ 由标题栏按钮调用(bundle 里注入的 Git 图标) ============
|
|
1275
|
+
win.__zgpToggle = function (mode) {
|
|
1276
|
+
const next = mode === 'global' ? 'global' : 'file'
|
|
1277
|
+
const opening = !root.classList.contains('open')
|
|
1278
|
+
const modeChanged = next !== panelMode
|
|
1279
|
+
panelMode = next
|
|
1280
|
+
// 切模式必须清掉签名缓存,否则 refresh() 会判定"状态没变"直接返回,
|
|
1281
|
+
// 面板里还留着上一个模式的 DOM(表现为外部面板显示成单文件详情)
|
|
1282
|
+
if (modeChanged) { lastSig = ''; lastFileSig = '' }
|
|
1283
|
+
root.classList.toggle('open')
|
|
1284
|
+
if (opening) {
|
|
1285
|
+
// 先把编辑器里待写盘的内容落盘,否则刚敲完就打开会看到旧状态
|
|
1286
|
+
try { if (win.__znotesFlushSave) win.__znotesFlushSave() } catch (e) {}
|
|
1287
|
+
applyMode()
|
|
1288
|
+
switchTab('changes')
|
|
1289
|
+
refresh(true)
|
|
1290
|
+
// 刚在编辑器里敲完就打开面板时,编辑器可能还没把内容写盘,
|
|
1291
|
+
// 这几次重试可以兜住写盘延迟(签名未变时不会重绘,无闪烁)
|
|
1292
|
+
;[250, 700, 1500, 2600, 4000].forEach(function (ms) {
|
|
1293
|
+
setTimeout(function () { try { refresh() } catch (e) {} }, ms)
|
|
1294
|
+
})
|
|
1295
|
+
} else if (modeChanged) {
|
|
1296
|
+
applyMode()
|
|
1297
|
+
refresh()
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
win.__zgpOpen = function (mode) { if (!root.classList.contains('open')) win.__zgpToggle(mode || 'global') }
|
|
1301
|
+
win.__zgpIsOpen = function () { return root.classList.contains('open') }
|
|
1302
|
+
|
|
1303
|
+
// 面板打开时定时刷新(3 秒);历史页不做轮询,避免反复重绘导致闪烁
|
|
1304
|
+
setInterval(() => {
|
|
1305
|
+
if (!root.classList.contains('open')) return
|
|
1306
|
+
if (activeTab === 'history') return
|
|
1307
|
+
refresh()
|
|
1308
|
+
}, 3000)
|
|
1309
|
+
|
|
1310
|
+
refresh()
|
|
1311
|
+
}
|