zcode-pro 0.16.0__py3-none-any.whl
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.
- zcode_pro/__init__.py +3 -0
- zcode_pro/__main__.py +4 -0
- zcode_pro/app/cli.mjs +9 -0
- zcode_pro/app/dist/inject.js +5821 -0
- zcode_pro/app/src/host/cdp.mjs +126 -0
- zcode_pro/app/src/host/helper.mjs +2134 -0
- zcode_pro/app/src/host/main.mjs +262 -0
- zcode_pro/app/src/host/paths.mjs +73 -0
- zcode_pro/app/src/host/pickFolder.mjs +152 -0
- zcode_pro/app/src/host/settings.mjs +61 -0
- zcode_pro/app/src/host/taskIndex.mjs +255 -0
- zcode_pro/app/src/host/taskOrder.mjs +117 -0
- zcode_pro/app/src/host/webdav.mjs +105 -0
- zcode_pro/app/src/inject/core.js +751 -0
- zcode_pro/app/src/inject/features/alias.js +211 -0
- zcode_pro/app/src/inject/features/copy-path.js +78 -0
- zcode_pro/app/src/inject/features/file-menu.js +147 -0
- zcode_pro/app/src/inject/features/idle-reclaim.js +179 -0
- zcode_pro/app/src/inject/features/image-menu.js +215 -0
- zcode_pro/app/src/inject/features/open-folder.js +43 -0
- zcode_pro/app/src/inject/features/pinned-collapse.js +132 -0
- zcode_pro/app/src/inject/features/pinned-expand.js +142 -0
- zcode_pro/app/src/inject/features/plugin-updates.js +28 -0
- zcode_pro/app/src/inject/features/project-menu.js +58 -0
- zcode_pro/app/src/inject/features/relocate-dialog.js +156 -0
- zcode_pro/app/src/inject/features/session-switch.js +244 -0
- zcode_pro/app/src/inject/features/settings-dialog.js +1503 -0
- zcode_pro/app/src/inject/features/settings-entry.js +21 -0
- zcode_pro/app/src/inject/features/styles.js +311 -0
- zcode_pro/app/src/inject/features/task-order.js +78 -0
- zcode_pro/app/src/inject/features/title-lock.js +242 -0
- zcode_pro/app/src/inject/features/toolbar-icons.js +243 -0
- zcode_pro/app/src/inject/features/ws-running.js +472 -0
- zcode_pro/app/src/inject/index.js +55 -0
- zcode_pro/app/src/inject/ui.js +734 -0
- zcode_pro/cli.py +71 -0
- zcode_pro-0.16.0.dist-info/METADATA +45 -0
- zcode_pro-0.16.0.dist-info/RECORD +41 -0
- zcode_pro-0.16.0.dist-info/WHEEL +4 -0
- zcode_pro-0.16.0.dist-info/entry_points.txt +2 -0
- zcode_pro-0.16.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// 图片右键“复制图片”:覆盖会话内容列([data-v4-timeline-content-column])里的图片与
|
|
2
|
+
// 点击放大的预览弹窗(role=dialog 内的大图),复制到系统剪贴板。
|
|
3
|
+
// 按渲染尺寸过滤行内技术图标(material-icons 约 16px);blob:/data:/file: 源经 fetch 取 blob,
|
|
4
|
+
// 取不到回退 canvas,非 PNG 一律经 canvas 转 PNG(剪贴板只可靠支持 image/png)。
|
|
5
|
+
import { h, t, getConfig } from '../core.js';
|
|
6
|
+
import { showToast } from '../ui.js';
|
|
7
|
+
|
|
8
|
+
const MIN_SIZE = 48; // 过滤行内小图标(真实截图/照片远大于此值)
|
|
9
|
+
|
|
10
|
+
// 复制目标判定:target 是会话内容列或弹窗内的 <img> 且渲染尺寸达标。
|
|
11
|
+
// 预览大图上方常有透明交互层(缩放/拖拽捕获),右键的 target 不是 img 本身,
|
|
12
|
+
// 此时在弹窗内按坐标找被命中的图片(取面积最大者,避免命中角落装饰图)。
|
|
13
|
+
function copyTargetOf(el, x, y) {
|
|
14
|
+
const sizeOk = (img) => {
|
|
15
|
+
const r = img.getBoundingClientRect();
|
|
16
|
+
return Math.min(r.width, r.height) >= MIN_SIZE;
|
|
17
|
+
};
|
|
18
|
+
if (el instanceof HTMLImageElement) {
|
|
19
|
+
if (!el.closest('[data-v4-timeline-content-column]') && !el.closest('[role="dialog"]')) return null;
|
|
20
|
+
return sizeOk(el) ? el : null;
|
|
21
|
+
}
|
|
22
|
+
if (!(el instanceof Element)) return null;
|
|
23
|
+
const dialog = el.closest('[role="dialog"]');
|
|
24
|
+
if (!dialog) return null;
|
|
25
|
+
let best = null;
|
|
26
|
+
for (const img of dialog.querySelectorAll('img')) {
|
|
27
|
+
const r = img.getBoundingClientRect();
|
|
28
|
+
if (x < r.left || x > r.right || y < r.top || y > r.bottom) continue;
|
|
29
|
+
if (!sizeOk(img)) continue;
|
|
30
|
+
const area = r.width * r.height;
|
|
31
|
+
if (!best || area > best.area) best = { img, area };
|
|
32
|
+
}
|
|
33
|
+
return best ? best.img : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let menuEl = null;
|
|
37
|
+
let hideMenu = () => {};
|
|
38
|
+
|
|
39
|
+
function showMenu(x, y, img) {
|
|
40
|
+
hideMenu();
|
|
41
|
+
const L = t();
|
|
42
|
+
// 菜单打开时就预取图片数据:点击复制时若图片已被卸载(如弹窗关闭)仍可完成
|
|
43
|
+
const preBlob = fetchImageBlob(img);
|
|
44
|
+
menuEl = h('div', {
|
|
45
|
+
role: 'menu',
|
|
46
|
+
class: 'fixed rounded-xl border border-border bg-menu p-1 text-ui-sm text-foreground',
|
|
47
|
+
style: 'z-index:2147483000;min-width:9rem;box-shadow:0 10px 30px rgba(0,0,0,.18);pointer-events:auto',
|
|
48
|
+
},
|
|
49
|
+
h('div', {
|
|
50
|
+
role: 'menuitem',
|
|
51
|
+
tabindex: '0',
|
|
52
|
+
class: 'zcodepro-imgmenu-item flex cursor-pointer select-none items-center gap-2 rounded-lg px-2.5 py-1.5',
|
|
53
|
+
onClick: () => {
|
|
54
|
+
hideMenu();
|
|
55
|
+
void copyImage(img, preBlob);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
hCopyIcon(),
|
|
59
|
+
document.createTextNode(L.imageCopy)));
|
|
60
|
+
document.body.append(menuEl);
|
|
61
|
+
const r = menuEl.getBoundingClientRect();
|
|
62
|
+
menuEl.style.left = Math.max(8, Math.min(x, window.innerWidth - r.width - 8)) + 'px';
|
|
63
|
+
menuEl.style.top = Math.max(8, Math.min(y, window.innerHeight - r.height - 8)) + 'px';
|
|
64
|
+
|
|
65
|
+
const onPointerDown = (e) => {
|
|
66
|
+
if (menuEl && menuEl.contains(e.target)) {
|
|
67
|
+
// 菜单内的按下交给菜单项自身 click 处理;stopPropagation 防止预览弹窗
|
|
68
|
+
// (Radix“点击外部关闭”)连带关闭、图片被卸载导致复制落空
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
hideMenu();
|
|
75
|
+
};
|
|
76
|
+
const onKeyDown = (e) => {
|
|
77
|
+
if (e.key !== 'Escape') return;
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
hideMenu(); // 只负责收起菜单;预览弹窗自身的 Esc 关闭不受影响
|
|
80
|
+
};
|
|
81
|
+
hideMenu = () => {
|
|
82
|
+
if (menuEl) { menuEl.remove(); menuEl = null; }
|
|
83
|
+
window.removeEventListener('blur', hideMenu);
|
|
84
|
+
window.removeEventListener('resize', hideMenu);
|
|
85
|
+
document.removeEventListener('scroll', hideMenu, true);
|
|
86
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
87
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
88
|
+
hideMenu = () => {};
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener('blur', hideMenu);
|
|
91
|
+
window.addEventListener('resize', hideMenu);
|
|
92
|
+
document.addEventListener('scroll', hideMenu, true);
|
|
93
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
94
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 取图片二进制:blob:/data:/file: 源走 fetch,失败或非图片时回退 canvas
|
|
98
|
+
async function fetchImageBlob(img) {
|
|
99
|
+
const src = img.currentSrc || img.src || '';
|
|
100
|
+
if (src) {
|
|
101
|
+
try {
|
|
102
|
+
const resp = await fetch(src);
|
|
103
|
+
if (resp.ok) {
|
|
104
|
+
const blob = await resp.blob();
|
|
105
|
+
if (/^image\//.test(blob.type)) return blob;
|
|
106
|
+
}
|
|
107
|
+
} catch { /* 跨域等场景回退 canvas */ }
|
|
108
|
+
}
|
|
109
|
+
return await fromCanvas(img);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function copyImage(img, preBlob) {
|
|
113
|
+
const L = t();
|
|
114
|
+
try {
|
|
115
|
+
let blob = await (preBlob || fetchImageBlob(img));
|
|
116
|
+
if (!blob) throw new Error('empty image');
|
|
117
|
+
if (blob.type !== 'image/png') blob = await toPng(blob);
|
|
118
|
+
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
|
119
|
+
showToast(L.imageCopied);
|
|
120
|
+
} catch {
|
|
121
|
+
// 部分 Electron/Linux 组合上 navigator.clipboard 写图片报 DataError(写文本正常),
|
|
122
|
+
// 回退 execCommand:选中 <img> 后复制,Chromium 会把图片本体写入剪贴板
|
|
123
|
+
if (copyViaSelection(img)) showToast(L.imageCopied);
|
|
124
|
+
else showToast(L.imageCopyFailed, 'error');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function copyViaSelection(img) {
|
|
129
|
+
try {
|
|
130
|
+
// 预览大图带 select-none(user-select:none),复制期间临时改为可选中
|
|
131
|
+
const prevSelect = img.style.userSelect;
|
|
132
|
+
img.style.userSelect = 'text';
|
|
133
|
+
const range = document.createRange();
|
|
134
|
+
range.selectNode(img);
|
|
135
|
+
const sel = window.getSelection();
|
|
136
|
+
const saved = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
137
|
+
sel.removeAllRanges();
|
|
138
|
+
sel.addRange(range);
|
|
139
|
+
const ok = document.execCommand('copy');
|
|
140
|
+
sel.removeAllRanges();
|
|
141
|
+
if (saved) sel.addRange(saved);
|
|
142
|
+
img.style.userSelect = prevSelect;
|
|
143
|
+
return ok;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 从已渲染的 img 直接画 canvas(同源/blob/data 图片可用;跨域图片会因画布污染失败)
|
|
150
|
+
function fromCanvas(img) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
try {
|
|
153
|
+
const c = document.createElement('canvas');
|
|
154
|
+
c.width = img.naturalWidth || 0;
|
|
155
|
+
c.height = img.naturalHeight || 0;
|
|
156
|
+
if (!c.width || !c.height) { resolve(null); return; }
|
|
157
|
+
c.getContext('2d').drawImage(img, 0, 0);
|
|
158
|
+
c.toBlob((b) => resolve(b), 'image/png');
|
|
159
|
+
} catch {
|
|
160
|
+
resolve(null);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function toPng(blob) {
|
|
166
|
+
const bmp = await createImageBitmap(blob);
|
|
167
|
+
const c = document.createElement('canvas');
|
|
168
|
+
c.width = bmp.width;
|
|
169
|
+
c.height = bmp.height;
|
|
170
|
+
c.getContext('2d').drawImage(bmp, 0, 0);
|
|
171
|
+
const png = await new Promise((resolve) => c.toBlob(resolve, 'image/png'));
|
|
172
|
+
if (!png) throw new Error('png convert failed');
|
|
173
|
+
return png;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hCopyIcon() {
|
|
177
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
178
|
+
const svg = document.createElementNS(ns, 'svg');
|
|
179
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
180
|
+
svg.setAttribute('fill', 'none');
|
|
181
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
182
|
+
svg.setAttribute('stroke-width', '2');
|
|
183
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
184
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
185
|
+
svg.setAttribute('class', 'size-4 shrink-0 text-foreground-subtle');
|
|
186
|
+
const rect = document.createElementNS(ns, 'rect');
|
|
187
|
+
rect.setAttribute('width', '14');
|
|
188
|
+
rect.setAttribute('height', '14');
|
|
189
|
+
rect.setAttribute('x', '8');
|
|
190
|
+
rect.setAttribute('y', '8');
|
|
191
|
+
rect.setAttribute('rx', '2');
|
|
192
|
+
rect.setAttribute('ry', '2');
|
|
193
|
+
svg.append(rect);
|
|
194
|
+
const p = document.createElementNS(ns, 'path');
|
|
195
|
+
p.setAttribute('d', 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2');
|
|
196
|
+
svg.append(p);
|
|
197
|
+
return svg;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function startImageMenu() {
|
|
201
|
+
if (typeof window === 'undefined') return;
|
|
202
|
+
if (typeof navigator.clipboard?.write !== 'function' || typeof window.ClipboardItem === 'undefined') return;
|
|
203
|
+
// preventDefault 必须同步做,功能开关用最近一次配置结果判断(右键时顺带刷新)
|
|
204
|
+
let enabled = true;
|
|
205
|
+
const refresh = () => { void getConfig().then((c) => { enabled = !(c.features && c.features.imageCopy === false); }); };
|
|
206
|
+
refresh();
|
|
207
|
+
document.addEventListener('contextmenu', (e) => {
|
|
208
|
+
if (e.defaultPrevented) return;
|
|
209
|
+
const img = copyTargetOf(e.target, e.clientX, e.clientY);
|
|
210
|
+
if (!img) return;
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
if (enabled) showMenu(e.clientX, e.clientY, img);
|
|
213
|
+
refresh();
|
|
214
|
+
}, true);
|
|
215
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// “打开文件夹”:项目“更多”菜单中在系统文件管理器里打开项目目录。
|
|
2
|
+
// 走 helper /open-folder:三平台行为一致(宿主桥 openInFileManager 在 macOS 是定位而非打开)。
|
|
3
|
+
import { h, t, rpc, closeRadixMenu } from '../core.js';
|
|
4
|
+
|
|
5
|
+
export function appendOpenFolderItem(menu, anchorItem, project) {
|
|
6
|
+
if (menu.querySelector('[data-zcodepro-item="open-folder"]')) return;
|
|
7
|
+
const L = t();
|
|
8
|
+
const item = anchorItem.cloneNode(true);
|
|
9
|
+
item.removeAttribute('data-testid');
|
|
10
|
+
item.removeAttribute('data-highlighted');
|
|
11
|
+
item.setAttribute('data-zcodepro-item', 'open-folder');
|
|
12
|
+
for (const child of [...item.childNodes]) child.remove();
|
|
13
|
+
const origIcon = anchorItem.querySelector('svg');
|
|
14
|
+
const iconClass = origIcon ? origIcon.getAttribute('class') : 'h-3.5 w-3.5';
|
|
15
|
+
item.append(hFolderIcon(iconClass), document.createTextNode(L.openFolderItem));
|
|
16
|
+
item.addEventListener('mouseenter', () => {
|
|
17
|
+
for (const el of menu.querySelectorAll('[role="menuitem"]')) el.removeAttribute('data-highlighted');
|
|
18
|
+
item.setAttribute('data-highlighted', '');
|
|
19
|
+
});
|
|
20
|
+
item.addEventListener('mouseleave', () => item.removeAttribute('data-highlighted'));
|
|
21
|
+
item.addEventListener('click', () => {
|
|
22
|
+
closeRadixMenu(menu);
|
|
23
|
+
try { void rpc('/open-folder', { method: 'POST', body: { path: project.path } }); } catch { /* ignore */ }
|
|
24
|
+
});
|
|
25
|
+
anchorItem.before(item);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hFolderIcon(cls) {
|
|
29
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
30
|
+
const svg = document.createElementNS(ns, 'svg');
|
|
31
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
32
|
+
svg.setAttribute('fill', 'none');
|
|
33
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
34
|
+
svg.setAttribute('stroke-width', '2');
|
|
35
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
36
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
37
|
+
svg.setAttribute('class', cls);
|
|
38
|
+
// lucide folder
|
|
39
|
+
const p = document.createElementNS(ns, 'path');
|
|
40
|
+
p.setAttribute('d', 'M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 0-1.69.9l-.58.87A2 2 0 0 1 8.93 8H4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2Z');
|
|
41
|
+
svg.append(p);
|
|
42
|
+
return svg;
|
|
43
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// 已置顶分区可折叠:原生侧栏“项目”“分组”的分区头是可折叠按钮(悬停显现
|
|
2
|
+
// chevron),“已置顶”只是静态标题。本功能把该标题变成同样的折叠开关:
|
|
3
|
+
// 标题内追加自有 chevron 按钮、整行可点,折叠时藏起分区里的任务列表;
|
|
4
|
+
// 状态记在 localStorage,界面重渲染后由观察器自动补回。不增删应用节点
|
|
5
|
+
// 结构(往标题里插外来子节点与顶栏图标按钮同理,界面框架不排斥)。
|
|
6
|
+
import { h, t, getConfig } from '../core.js';
|
|
7
|
+
import { ensureStyle } from '../ui.js';
|
|
8
|
+
|
|
9
|
+
const COLLAPSED_CLASS = 'zcodepro-pin-collapsed';
|
|
10
|
+
const HEAD_CLASS = 'zcodepro-pin-head';
|
|
11
|
+
const STORE_KEY = 'zcodepro:pinned-collapsed';
|
|
12
|
+
const SYNC_DEBOUNCE_MS = 300; // 相关 DOM 变动后的去抖
|
|
13
|
+
const SCAN_FALLBACK_MS = 5000; // 兜底轮询周期
|
|
14
|
+
|
|
15
|
+
let enabled = true; // 配置开关(features.pinnedCollapse !== false)
|
|
16
|
+
let syncTimer = 0;
|
|
17
|
+
|
|
18
|
+
// 已置顶标题的识别不依赖文案(跟随界面语言):h3 的下一个兄弟是含任务行的列表
|
|
19
|
+
function findPinnedHeader() {
|
|
20
|
+
for (const h3 of document.querySelectorAll('h3')) {
|
|
21
|
+
const ul = h3.nextElementSibling;
|
|
22
|
+
if (ul && ul.tagName === 'UL' && ul.querySelector('li[data-task-item-key]')) return h3;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function chevronSvg() {
|
|
28
|
+
// 优先克隆应用“项目”分区头的 chevron(应用改图标自动跟随),取不到再内建
|
|
29
|
+
const proto = document.querySelector('svg[data-purpose-section-chevron]');
|
|
30
|
+
if (proto) {
|
|
31
|
+
const clone = proto.cloneNode(true);
|
|
32
|
+
clone.removeAttribute('class');
|
|
33
|
+
clone.removeAttribute('data-purpose-section-chevron');
|
|
34
|
+
return clone;
|
|
35
|
+
}
|
|
36
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
37
|
+
const svg = document.createElementNS(ns, 'svg');
|
|
38
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
39
|
+
svg.setAttribute('fill', 'none');
|
|
40
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
41
|
+
svg.setAttribute('stroke-width', '1.5');
|
|
42
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
43
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
44
|
+
const path = document.createElementNS(ns, 'path');
|
|
45
|
+
path.setAttribute('d', 'm6 9 6 6 6-6');
|
|
46
|
+
svg.append(path);
|
|
47
|
+
return svg;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isCollapsed() {
|
|
51
|
+
try { return localStorage.getItem(STORE_KEY) === '1'; } catch { return false; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toggle() {
|
|
55
|
+
try { localStorage.setItem(STORE_KEY, isCollapsed() ? '0' : '1'); } catch { /* ignore */ }
|
|
56
|
+
apply();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function apply() {
|
|
60
|
+
const h3 = findPinnedHeader();
|
|
61
|
+
if (!h3) return false;
|
|
62
|
+
const sect = h3.parentElement;
|
|
63
|
+
const collapsed = isCollapsed();
|
|
64
|
+
sect.classList.toggle(COLLAPSED_CLASS, collapsed);
|
|
65
|
+
const existing = h3.querySelector('.zcodepro-pin-chevron');
|
|
66
|
+
if (h3.classList.contains(HEAD_CLASS) && existing) {
|
|
67
|
+
existing.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
h3.classList.add(HEAD_CLASS);
|
|
71
|
+
// 整行可点(与“项目”分区头一致);开关禁用后旧监听变空转,无需摘除
|
|
72
|
+
h3.addEventListener('click', () => { if (enabled) toggle(); });
|
|
73
|
+
const btn = h('button', {
|
|
74
|
+
type: 'button',
|
|
75
|
+
class: 'zcodepro-pin-chevron',
|
|
76
|
+
'aria-label': t().pinnedToggleAria,
|
|
77
|
+
'aria-expanded': collapsed ? 'false' : 'true',
|
|
78
|
+
onClick: (e) => { e.stopPropagation(); if (enabled) toggle(); },
|
|
79
|
+
});
|
|
80
|
+
btn.append(chevronSvg());
|
|
81
|
+
h3.append(btn);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sync() {
|
|
86
|
+
if (enabled) apply();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scheduleSync() {
|
|
90
|
+
if (syncTimer) return;
|
|
91
|
+
syncTimer = setTimeout(() => { syncTimer = 0; sync(); }, SYNC_DEBOUNCE_MS);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 侧栏相关变动才重扫:分区/任务行的新增删除(重渲染、切分组/项目视图)
|
|
95
|
+
function relevantMutations(muts) {
|
|
96
|
+
for (const m of muts) {
|
|
97
|
+
for (const list of [m.addedNodes, m.removedNodes]) {
|
|
98
|
+
for (const n of list) {
|
|
99
|
+
if (!(n instanceof Element)) continue;
|
|
100
|
+
if (n.tagName === 'H3' || n.matches('li[data-task-item-key]')
|
|
101
|
+
|| n.querySelector('h3, li[data-task-item-key]')) return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function teardown() {
|
|
109
|
+
for (const h3 of document.querySelectorAll('.' + HEAD_CLASS)) {
|
|
110
|
+
h3.classList.remove(HEAD_CLASS);
|
|
111
|
+
h3.querySelector('.zcodepro-pin-chevron')?.remove();
|
|
112
|
+
h3.parentElement?.classList.remove(COLLAPSED_CLASS);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function refreshConfig() {
|
|
117
|
+
// 强制拉取:配置可能经设置弹窗以外的途径改动(缓存 3 秒内会是旧值)
|
|
118
|
+
const cfg = await getConfig(true).catch(() => null);
|
|
119
|
+
enabled = !cfg || !cfg.features || cfg.features.pinnedCollapse !== false;
|
|
120
|
+
if (enabled) sync();
|
|
121
|
+
else teardown();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function startPinnedCollapse() {
|
|
125
|
+
ensureStyle();
|
|
126
|
+
window.addEventListener('zcodepro:config-changed', () => { void refreshConfig(); });
|
|
127
|
+
void refreshConfig();
|
|
128
|
+
new MutationObserver((muts) => { if (relevantMutations(muts)) scheduleSync(); })
|
|
129
|
+
.observe(document.body, { childList: true, subtree: true });
|
|
130
|
+
setInterval(() => { if (!document.hidden) sync(); }, SCAN_FALLBACK_MS);
|
|
131
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) sync(); });
|
|
132
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// 置顶会话展开抑制(v4,实验性):
|
|
2
|
+
// 已知缺陷:无法真正拦截展开(store 在应用闭包内不可达,渲染层无拦截点),
|
|
3
|
+
// 只能在展开发生后收起,因此视觉上“先展开再缩起”,大会话期间可能闪烁。
|
|
4
|
+
|
|
5
|
+
// 点击折叠项目的置顶会话时,应用展开该项目;且会话内容加载完成(正文渲染)后还会再展开一次。
|
|
6
|
+
// 固定延时不可靠(会话有大有小:空白时就收起会被“内容出现后的二次展开”覆盖)。
|
|
7
|
+
// v4 改为事件驱动:点击后开始监听该项目的 DOM 变化,
|
|
8
|
+
// 以“会话区渲染出实质内容(文本节点增多/任务行出现)且短暂稳定”作为加载完成信号,
|
|
9
|
+
// 之后若项目仍展开则收起;最长等待 15s 兜底。
|
|
10
|
+
// 开关:helper pinnedKeepCollapsed(默认 true = 保持折叠,增强)。
|
|
11
|
+
import { rpc } from '../core.js';
|
|
12
|
+
|
|
13
|
+
let installed = false;
|
|
14
|
+
let keepCollapsed = false; // 默认关闭:实现为“先展开再缩起”,有闪烁(应用无拦截点,详见文件头)
|
|
15
|
+
|
|
16
|
+
const CONFIG_POLL_MIN_MS = 5000; // 配置未变化时逐次拉长轮询间隔,减轻常驻开销
|
|
17
|
+
const CONFIG_POLL_MAX_MS = 60000;
|
|
18
|
+
let pollMs = CONFIG_POLL_MIN_MS;
|
|
19
|
+
|
|
20
|
+
// 返回本次请求是否改变了开关值(用于轮询间隔回弹)
|
|
21
|
+
async function refreshConfig() {
|
|
22
|
+
try {
|
|
23
|
+
const res = await rpc('/config');
|
|
24
|
+
if (res && res.ok && res.config && res.config.features) {
|
|
25
|
+
const next = res.config.features.pinnedKeepCollapsed !== false;
|
|
26
|
+
if (next !== keepCollapsed) {
|
|
27
|
+
keepCollapsed = next;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch { /* helper 未就绪时保持默认 */ }
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 自适应轮询:值未变化间隔 ×1.5 直至上限,变化即回到最短;页面隐藏时跳过请求
|
|
36
|
+
function pollConfig() {
|
|
37
|
+
setTimeout(async () => {
|
|
38
|
+
if (document.hidden) pollMs = CONFIG_POLL_MAX_MS;
|
|
39
|
+
else pollMs = (await refreshConfig()) ? CONFIG_POLL_MIN_MS : Math.min(Math.round(pollMs * 1.5), CONFIG_POLL_MAX_MS);
|
|
40
|
+
pollConfig();
|
|
41
|
+
}, pollMs);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function startPinnedExpandSuppression() {
|
|
45
|
+
if (installed || typeof document === 'undefined') return;
|
|
46
|
+
installed = true;
|
|
47
|
+
void refreshConfig();
|
|
48
|
+
pollConfig();
|
|
49
|
+
// 回到前台立即补一次,避免长退避期间拿不到刚改的配置
|
|
50
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) void refreshConfig(); });
|
|
51
|
+
|
|
52
|
+
document.addEventListener('click', (e) => {
|
|
53
|
+
if (!keepCollapsed) return;
|
|
54
|
+
const taskRow = e.target.closest && e.target.closest('[data-testid^="task-item-"]');
|
|
55
|
+
if (!taskRow) return;
|
|
56
|
+
const key = taskRow.getAttribute('data-task-item-key') || '';
|
|
57
|
+
const sep = key.lastIndexOf(':');
|
|
58
|
+
if (sep <= 0) return;
|
|
59
|
+
const wsKey = key.slice(0, sep);
|
|
60
|
+
const wsRow = document.querySelector(`[data-testid="workspace-item-${cssEscape(wsKey)}"]`);
|
|
61
|
+
if (!wsRow || wsRow.getAttribute('aria-expanded') !== 'false') return;
|
|
62
|
+
|
|
63
|
+
const head = wsRow.matches('[aria-expanded]') ? wsRow : wsRow.querySelector('[aria-expanded]');
|
|
64
|
+
if (!head) return;
|
|
65
|
+
// 等会话加载完成(内容渲染且稳定)后再收起
|
|
66
|
+
collapseAfterContentLoaded(wsRow);
|
|
67
|
+
}, true);
|
|
68
|
+
|
|
69
|
+
// 用户点项目行头部 = 主动展开,永不干预(事件捕获,先于应用处理)
|
|
70
|
+
document.addEventListener('click', (e) => {
|
|
71
|
+
const row = e.target.closest && e.target.closest('[data-testid^="workspace-item-"]');
|
|
72
|
+
if (row) row.__zcodeproUserTouched = Date.now();
|
|
73
|
+
}, true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 收起用的点击必须点回当前会话:应用把项目行点击当作“切换到该项目”,只点项目行
|
|
77
|
+
// 会把当前会话取消选中、主视图跳进新建任务。还原行必须在点项目行之前就记住——
|
|
78
|
+
// 点击会立刻取消选中(3.14.4 实测),事后再按选中标记找就找不到了,回点落空
|
|
79
|
+
// 会把视图留在该项目的新会话页;元素被 React 重挂载时按记住的会话键重找。
|
|
80
|
+
function currentTaskRow() {
|
|
81
|
+
for (const el of document.querySelectorAll('[data-task-item-key]')) {
|
|
82
|
+
if ((el.className + '').includes('bg-selected')) return el;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function collapseNavSafe(head) {
|
|
87
|
+
const cur = currentTaskRow();
|
|
88
|
+
const backKey = cur ? cur.getAttribute('data-task-item-key') : '';
|
|
89
|
+
head.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
90
|
+
if (!backKey) return;
|
|
91
|
+
const back = (cur && cur.isConnected)
|
|
92
|
+
? cur
|
|
93
|
+
: document.querySelector(`[data-task-item-key="${cssEscape(backKey)}"]`);
|
|
94
|
+
if (back) back.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 实测时序(探针 v2):点击后 ~6s 出现第一次自动展开并被收起;
|
|
98
|
+
// 正文文字在点击后 ~8-14s 才开始增长,期间应用还会再次展开项目。
|
|
99
|
+
// 因此收起不能是单次动作:在 25s 监听期内持续压制(发现展开即收起),
|
|
100
|
+
// 直到“正文出现增长且稳定 1s”(会话真正加载完成)后再做最后一次收起并结束。
|
|
101
|
+
function collapseAfterContentLoaded(wsRow) {
|
|
102
|
+
const deadline = Date.now() + 25000;
|
|
103
|
+
let lastLen = (document.querySelector('main') || document.body).innerText.length;
|
|
104
|
+
let grewAt = 0;
|
|
105
|
+
let settledAt = 0;
|
|
106
|
+
|
|
107
|
+
const headOf = () => {
|
|
108
|
+
const h = wsRow.matches('[aria-expanded]') ? wsRow : wsRow.querySelector('[aria-expanded]');
|
|
109
|
+
return h || null;
|
|
110
|
+
};
|
|
111
|
+
const userTouched = () => wsRow.__zcodeproUserTouched && Date.now() - wsRow.__zcodeproUserTouched < 800;
|
|
112
|
+
|
|
113
|
+
const tick = () => {
|
|
114
|
+
if (!keepCollapsed || Date.now() > deadline || !wsRow.isConnected) return done();
|
|
115
|
+
const len = (document.querySelector('main') || document.body).innerText.length;
|
|
116
|
+
if (len > lastLen) grewAt = Date.now(); // 正文开始/持续增长
|
|
117
|
+
if (grewAt && Date.now() - grewAt >= 1000) settledAt = settledAt || Date.now(); // 增长后稳定 1s
|
|
118
|
+
lastLen = len;
|
|
119
|
+
|
|
120
|
+
const head = headOf();
|
|
121
|
+
// 持续压制:监听期内任何展开都收起(用户刚点过头部则跳过)
|
|
122
|
+
if (head && head.getAttribute('aria-expanded') === 'true' && !userTouched()) {
|
|
123
|
+
collapseNavSafe(head);
|
|
124
|
+
}
|
|
125
|
+
// 结束条件:正文已增长且稳定 1s(此时收起已完成)——再做最后一次确认收起
|
|
126
|
+
if (settledAt && Date.now() - settledAt >= 1000) {
|
|
127
|
+
const h2 = headOf();
|
|
128
|
+
if (h2 && h2.getAttribute('aria-expanded') === 'true' && !userTouched()) {
|
|
129
|
+
collapseNavSafe(h2);
|
|
130
|
+
}
|
|
131
|
+
return done();
|
|
132
|
+
}
|
|
133
|
+
setTimeout(tick, 200);
|
|
134
|
+
};
|
|
135
|
+
const done = () => { wsRow.__zcodeproSuppending = false; };
|
|
136
|
+
wsRow.__zcodeproSuppending = true;
|
|
137
|
+
setTimeout(tick, 400);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cssEscape(s) {
|
|
141
|
+
return String(s).replace(/(["\\\]])/g, '\\$1');
|
|
142
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// 启动时检查 duanluan-zcode-plugins 市场的插件更新:
|
|
2
|
+
// 有新版本 → 开了“自动更新插件”直接更新,否则 toast 提醒一次(同一批更新只提醒一次)。
|
|
3
|
+
// 实际的 marketplace 同步与安装/更新都由 helper 的 /plugins/* 端点执行。
|
|
4
|
+
import { rpc, getConfig, t } from '../core.js';
|
|
5
|
+
import { showToast } from '../ui.js';
|
|
6
|
+
|
|
7
|
+
const NOTIFIED_KEY = 'zcodepro-plugin-updates-notified';
|
|
8
|
+
|
|
9
|
+
export async function startPluginUpdateCheck() {
|
|
10
|
+
const status = await rpc('/plugins/status');
|
|
11
|
+
if (!status.ok || !Array.isArray(status.updates)) return;
|
|
12
|
+
const L = t();
|
|
13
|
+
if (status.updates.length === 0) return;
|
|
14
|
+
|
|
15
|
+
const config = await getConfig();
|
|
16
|
+
if ((config.features || {}).autoUpdatePlugins === true) {
|
|
17
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: { installMissing: true } });
|
|
18
|
+
if (res.ok) showToast(L.pluginsAutoUpdated, 'success');
|
|
19
|
+
else showToast(L.pluginsUpdateFailed + ': ' + (res.error || ''), 'error');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const sig = status.updates.map((u) => `${u.name}:${u.installed}>${u.latest}`).join(',');
|
|
23
|
+
let seen = '';
|
|
24
|
+
try { seen = window.localStorage.getItem(NOTIFIED_KEY) || ''; } catch { /* ignore */ }
|
|
25
|
+
if (seen === sig) return;
|
|
26
|
+
try { window.localStorage.setItem(NOTIFIED_KEY, sig); } catch { /* ignore */ }
|
|
27
|
+
showToast(L.pluginsUpdatesAvailable.replaceAll('{n}', String(status.updates.length)), 'info');
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// 功能二:项目“更多”下拉菜单中新增“自定义别名”。
|
|
2
|
+
// 识别方式:菜单内存在带 data-testid(值中含项目绝对路径)且文案为“移除/Remove”的菜单项。
|
|
3
|
+
// 项目路径优先从 data-testid 提取;提取失败时回退为按项目名匹配 helper 返回的最近项目列表。
|
|
4
|
+
import { rpc, getConfig, itemsOf, itemText } from '../core.js';
|
|
5
|
+
import { appendAliasItem } from './alias.js';
|
|
6
|
+
import { appendRelocateItem } from './relocate-dialog.js';
|
|
7
|
+
import { appendOpenFolderItem } from './open-folder.js';
|
|
8
|
+
import { appendCopyPathItem } from './copy-path.js';
|
|
9
|
+
|
|
10
|
+
const REMOVE_TEXTS = ['移除', 'Remove'];
|
|
11
|
+
|
|
12
|
+
// data-testid 实测格式为 "<前缀>-<绝对路径>"(如 workspace-close-/home/x/y),
|
|
13
|
+
// 也兼容 ":"、"=" 等分隔符与 Windows 盘符路径。
|
|
14
|
+
function extractPathFromTestId(testid) {
|
|
15
|
+
let m = testid.match(/[-:=,|](\/.+)$/); // Unix 绝对路径
|
|
16
|
+
if (m) return m[1];
|
|
17
|
+
m = testid.match(/[-:=,|]([A-Za-z]:\\.*)$/); // Windows 盘符路径
|
|
18
|
+
return m ? m[1] : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function basenameOf(p) {
|
|
22
|
+
return String(p || '').replace(/[\\/]+$/, '').split(/[\\/]/).pop() || '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function resolveProjectPath(removeItem) {
|
|
26
|
+
const testid = removeItem.getAttribute('data-testid') || '';
|
|
27
|
+
const extracted = extractPathFromTestId(testid);
|
|
28
|
+
if (!extracted) return null;
|
|
29
|
+
// 与 helper 的最近项目列表核对(仅用于确认路径有效;任务派生的项目可能不在列表中)
|
|
30
|
+
const res = await rpc('/projects');
|
|
31
|
+
if (res.ok && Array.isArray(res.projects)) {
|
|
32
|
+
const norm = (p) => String(p).replace(/[\\/]+$/, '');
|
|
33
|
+
const hit = res.projects.find((p) => norm(p.path) === norm(extracted));
|
|
34
|
+
if (hit) return hit;
|
|
35
|
+
}
|
|
36
|
+
return { path: extracted, name: basenameOf(extracted) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function handleProjectMenu(content) {
|
|
40
|
+
if (content.querySelector('[data-zcodepro-item="alias"]')) return;
|
|
41
|
+
const items = itemsOf(content);
|
|
42
|
+
const removeItem = items.find(
|
|
43
|
+
(el) => el.hasAttribute('data-testid') && REMOVE_TEXTS.some((x) => itemText(el).startsWith(x))
|
|
44
|
+
);
|
|
45
|
+
if (!removeItem) return;
|
|
46
|
+
void (async () => {
|
|
47
|
+
const config = await getConfig();
|
|
48
|
+
// 一组开关控制整组菜单项(别名 / 切换文件夹 / 打开文件夹 / 复制路径)
|
|
49
|
+
if (config.features && config.features.projectMenu === false) return;
|
|
50
|
+
const project = await resolveProjectPath(removeItem);
|
|
51
|
+
if (!project) return;
|
|
52
|
+
// 菜单顺序:自定义别名 → 切换文件夹 → 打开文件夹 → 复制路径 → 移除(均插在“移除”之前)
|
|
53
|
+
appendAliasItem(content, removeItem, project);
|
|
54
|
+
appendRelocateItem(content, removeItem, project);
|
|
55
|
+
appendOpenFolderItem(content, removeItem, project);
|
|
56
|
+
appendCopyPathItem(content, removeItem, project);
|
|
57
|
+
})();
|
|
58
|
+
}
|