@aicupa/plugin-todo-dependency 1.0.4 → 1.0.6
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/package.json +1 -1
- package/service.js +27 -0
- package/view/index.html +91 -30
package/package.json
CHANGED
package/service.js
CHANGED
|
@@ -11,6 +11,7 @@ module.exports = (api) => {
|
|
|
11
11
|
id: node.todo.id,
|
|
12
12
|
content: node.todo.content,
|
|
13
13
|
done: node.todo.done,
|
|
14
|
+
focus: node.todo.focus || false,
|
|
14
15
|
level: node.todo.level,
|
|
15
16
|
depIds: node.todo.depIds || [],
|
|
16
17
|
})
|
|
@@ -83,6 +84,32 @@ module.exports = (api) => {
|
|
|
83
84
|
}
|
|
84
85
|
},
|
|
85
86
|
|
|
87
|
+
async toggleFocus({ todoId, focus, filePath }) {
|
|
88
|
+
try {
|
|
89
|
+
const content = await api.readFile(filePath)
|
|
90
|
+
const data = JSON.parse(content)
|
|
91
|
+
const todotree = data.todotree
|
|
92
|
+
|
|
93
|
+
function findAndUpdate(nodes) {
|
|
94
|
+
for (const node of nodes) {
|
|
95
|
+
if (node.todo && node.todo.id === todoId) {
|
|
96
|
+
node.todo.focus = focus
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
if (node.children?.length && findAndUpdate(node.children)) return true
|
|
100
|
+
}
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
findAndUpdate(todotree.tree)
|
|
105
|
+
await api.store('todotree', todotree, filePath)
|
|
106
|
+
await api.reload(filePath)
|
|
107
|
+
return { ok: true }
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return { ok: false, error: e.message }
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
86
113
|
async toggleDone({ todoId, done, filePath }) {
|
|
87
114
|
try {
|
|
88
115
|
const content = await api.readFile(filePath)
|
package/view/index.html
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
padding: 10px 12px; cursor: default;
|
|
53
53
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
|
54
54
|
transition: box-shadow 0.2s, transform 0.15s;
|
|
55
|
-
display: flex; align-items: flex-start; gap:
|
|
55
|
+
display: flex; align-items: flex-start; gap: 8px;
|
|
56
56
|
}
|
|
57
57
|
.node:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); transform: translateY(-1px); }
|
|
58
58
|
.dark .node { background: #2d2d2d; border-color: #3c3c3c; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
|
|
@@ -79,7 +79,22 @@
|
|
|
79
79
|
.dark .node-check.checked { border-color: #73d13d; background: #73d13d; }
|
|
80
80
|
.dark .node-check.checked::after { border-color: #2d2d2d; }
|
|
81
81
|
|
|
82
|
-
.node-body { flex: 1; min-width: 0; }
|
|
82
|
+
.node-body { flex: 1; min-width: 0; position: relative; }
|
|
83
|
+
|
|
84
|
+
.node-focus {
|
|
85
|
+
position: absolute; top: -2px; right: -4px;
|
|
86
|
+
width: 16px; height: 16px; cursor: pointer;
|
|
87
|
+
border: none; background: none; padding: 0;
|
|
88
|
+
color: #bbb; transition: color 0.15s;
|
|
89
|
+
display: flex; align-items: center; justify-content: center;
|
|
90
|
+
opacity: 0; transition: opacity 0.15s, color 0.15s;
|
|
91
|
+
}
|
|
92
|
+
.node:hover .node-focus { opacity: 1; }
|
|
93
|
+
.node-focus.active { opacity: 1; color: #1890ff; }
|
|
94
|
+
.node-focus:hover { color: #1890ff; }
|
|
95
|
+
.dark .node-focus { color: #555; }
|
|
96
|
+
.dark .node-focus:hover { color: #40a9ff; }
|
|
97
|
+
.dark .node-focus.active { color: #40a9ff; }
|
|
83
98
|
|
|
84
99
|
.node-content {
|
|
85
100
|
font-size: 12px; line-height: 1.5; word-break: break-word;
|
|
@@ -91,12 +106,14 @@
|
|
|
91
106
|
display: inline-block; font-size: 10px; padding: 1px 6px;
|
|
92
107
|
border-radius: 10px; margin-top: 5px;
|
|
93
108
|
}
|
|
94
|
-
.badge-pending
|
|
95
|
-
.badge-done
|
|
96
|
-
.badge-blocked
|
|
97
|
-
.
|
|
98
|
-
.dark .badge-
|
|
99
|
-
.dark .badge-
|
|
109
|
+
.badge-pending { background: #fff7e6; color: #d46b08; }
|
|
110
|
+
.badge-done { background: #f6ffed; color: #389e0d; }
|
|
111
|
+
.badge-blocked { background: #fff1f0; color: #cf1322; }
|
|
112
|
+
.badge-focus { background: #e6f7ff; color: #096dd9; }
|
|
113
|
+
.dark .badge-pending { background: #3a3000; color: #ffc53d; }
|
|
114
|
+
.dark .badge-done { background: #1e3a1e; color: #73d13d; }
|
|
115
|
+
.dark .badge-blocked { background: #3a1a1a; color: #ff4d4f; }
|
|
116
|
+
.dark .badge-focus { background: #111d2c; color: #40a9ff; }
|
|
100
117
|
|
|
101
118
|
.node.blocked { border-style: dashed; opacity: 0.7; }
|
|
102
119
|
.dark .node.blocked { opacity: 0.7; }
|
|
@@ -109,10 +126,16 @@
|
|
|
109
126
|
|
|
110
127
|
#edgeSvg { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
111
128
|
|
|
129
|
+
#graphInner { transition: opacity 0.35s ease; }
|
|
130
|
+
#graphInner.fade-out { opacity: 0; }
|
|
131
|
+
|
|
112
132
|
.empty-state {
|
|
113
133
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
114
|
-
|
|
134
|
+
position: absolute; inset: 0;
|
|
135
|
+
color: #aaa; padding: 40px 24px; text-align: center; user-select: none;
|
|
136
|
+
opacity: 0; pointer-events: none; transition: opacity 0.35s ease;
|
|
115
137
|
}
|
|
138
|
+
.empty-state.visible { opacity: 1; pointer-events: auto; }
|
|
116
139
|
.empty-icon { width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.35; }
|
|
117
140
|
.empty-title { font-size: 15px; font-weight: 500; margin-bottom: 10px; color: #888; }
|
|
118
141
|
.dark .empty-title { color: #777; }
|
|
@@ -128,20 +151,19 @@
|
|
|
128
151
|
</div>
|
|
129
152
|
<div id="warnings"></div>
|
|
130
153
|
<div id="graphArea">
|
|
131
|
-
<div id="graphContainer">
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</div>
|
|
154
|
+
<div id="graphContainer"></div>
|
|
155
|
+
<div class="empty-state visible" id="emptyState">
|
|
156
|
+
<svg class="empty-icon" viewBox="0 0 80 80" fill="none" stroke="currentColor" stroke-width="2">
|
|
157
|
+
<rect x="8" y="10" width="24" height="16" rx="4"/>
|
|
158
|
+
<rect x="48" y="10" width="24" height="16" rx="4"/>
|
|
159
|
+
<rect x="28" y="50" width="24" height="16" rx="4"/>
|
|
160
|
+
<line x1="26" y1="26" x2="36" y2="50" stroke-dasharray="3,2"/>
|
|
161
|
+
<line x1="54" y1="26" x2="44" y2="50" stroke-dasharray="3,2"/>
|
|
162
|
+
<polygon points="36,50 33,45 39,45" fill="currentColor" stroke="none"/>
|
|
163
|
+
<polygon points="44,50 41,45 47,45" fill="currentColor" stroke="none"/>
|
|
164
|
+
</svg>
|
|
165
|
+
<div class="empty-title" data-i18n="emptyTitle"></div>
|
|
166
|
+
<div class="empty-hint" data-i18n="emptyHint"></div>
|
|
145
167
|
</div>
|
|
146
168
|
</div>
|
|
147
169
|
</div>
|
|
@@ -153,14 +175,18 @@
|
|
|
153
175
|
title: '依赖图谱', refresh: '刷新',
|
|
154
176
|
emptyTitle: '暂无依赖关系',
|
|
155
177
|
emptyHint: '右键 Todo 节点,选择「设置依赖」<br>即可建立待办之间的依赖关系',
|
|
156
|
-
|
|
178
|
+
allDoneTitle: '所有依赖任务已完成 🎉',
|
|
179
|
+
allDoneHint: '当前依赖链路中的任务均已完成<br>如有新的依赖关系,请重新创建',
|
|
180
|
+
done: '已完成', pending: '待完成', blocked: '阻塞中', focus: '进行中', unfocus: '取消进行', notFound: '未找到该 Todo',
|
|
157
181
|
cycleWarning: '检测到循环依赖,部分节点的层级可能不准确',
|
|
158
182
|
},
|
|
159
183
|
en: {
|
|
160
184
|
title: 'Dependency Graph', refresh: 'Refresh',
|
|
161
185
|
emptyTitle: 'No dependencies yet',
|
|
162
186
|
emptyHint: 'Right-click a Todo and select "Set Dependencies"<br>to create dependency relationships',
|
|
163
|
-
|
|
187
|
+
allDoneTitle: 'All dependency tasks completed 🎉',
|
|
188
|
+
allDoneHint: 'All tasks in dependency chains are done<br>Create new dependencies if needed',
|
|
189
|
+
done: 'Done', pending: 'Pending', blocked: 'Blocked', focus: 'In Progress', unfocus: 'Cancel Focus', notFound: 'Todo not found',
|
|
164
190
|
cycleWarning: 'Cycle detected — some nodes may be positioned incorrectly',
|
|
165
191
|
},
|
|
166
192
|
}
|
|
@@ -243,6 +269,7 @@
|
|
|
243
269
|
if (!data) return
|
|
244
270
|
|
|
245
271
|
const { todos, edges: allEdges } = data
|
|
272
|
+
hideEmpty()
|
|
246
273
|
|
|
247
274
|
// Build adjacency (both directions) to find connected components
|
|
248
275
|
const allIds = new Set()
|
|
@@ -275,7 +302,7 @@
|
|
|
275
302
|
}
|
|
276
303
|
const edges = allEdges.filter(([from, to]) => visibleIds.has(from) && visibleIds.has(to))
|
|
277
304
|
const nodeIds = [...visibleIds]
|
|
278
|
-
if (!nodeIds.length) {
|
|
305
|
+
if (!nodeIds.length) { fadeToEmpty(allEdges.length > 0); return }
|
|
279
306
|
|
|
280
307
|
const { layers, hasCycle } = assignLayers(nodeIds, edges)
|
|
281
308
|
if (hasCycle) {
|
|
@@ -285,11 +312,23 @@
|
|
|
285
312
|
renderGraph(nodeIds, edges, layout.positions, todos, layout)
|
|
286
313
|
}
|
|
287
314
|
|
|
288
|
-
function showEmpty() {
|
|
289
|
-
|
|
315
|
+
function showEmpty(allDone) {
|
|
316
|
+
document.getElementById('graphContainer').innerHTML = ''
|
|
290
317
|
const e = document.getElementById('emptyState')
|
|
291
|
-
|
|
292
|
-
|
|
318
|
+
e.querySelector('.empty-title').innerHTML = allDone ? t.allDoneTitle : t.emptyTitle
|
|
319
|
+
e.querySelector('.empty-hint').innerHTML = allDone ? t.allDoneHint : t.emptyHint
|
|
320
|
+
e.classList.add('visible')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function hideEmpty() {
|
|
324
|
+
document.getElementById('emptyState').classList.remove('visible')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function fadeToEmpty(allDone) {
|
|
328
|
+
const inner = document.getElementById('graphInner')
|
|
329
|
+
if (!inner) { showEmpty(allDone); return }
|
|
330
|
+
inner.classList.add('fade-out')
|
|
331
|
+
inner.addEventListener('transitionend', () => showEmpty(allDone), { once: true })
|
|
293
332
|
}
|
|
294
333
|
|
|
295
334
|
// ── Layout algorithm ──
|
|
@@ -352,6 +391,13 @@
|
|
|
352
391
|
autoScan()
|
|
353
392
|
}
|
|
354
393
|
|
|
394
|
+
async function toggleTodoFocus(todoId, focus) {
|
|
395
|
+
try {
|
|
396
|
+
await callPlugin('toggleFocus', { todoId, focus, filePath: currentFilePath })
|
|
397
|
+
} catch {}
|
|
398
|
+
autoScan()
|
|
399
|
+
}
|
|
400
|
+
|
|
355
401
|
function renderGraph(nodeIds, edges, positions, todoData, layout) {
|
|
356
402
|
const container = document.getElementById('graphContainer')
|
|
357
403
|
container.innerHTML = ''
|
|
@@ -442,6 +488,18 @@
|
|
|
442
488
|
}
|
|
443
489
|
|
|
444
490
|
const body = document.createElement('div'); body.className = 'node-body'
|
|
491
|
+
|
|
492
|
+
if (todo && !todo.done && !blocked) {
|
|
493
|
+
const focusBtn = document.createElement('button')
|
|
494
|
+
focusBtn.className = 'node-focus' + (todo.focus ? ' active' : '')
|
|
495
|
+
focusBtn.title = todo.focus ? t.unfocus : t.focus
|
|
496
|
+
focusBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>'
|
|
497
|
+
focusBtn.addEventListener('click', e => {
|
|
498
|
+
e.stopPropagation()
|
|
499
|
+
toggleTodoFocus(id, !todo.focus)
|
|
500
|
+
})
|
|
501
|
+
body.appendChild(focusBtn)
|
|
502
|
+
}
|
|
445
503
|
const content = document.createElement('div'); content.className = 'node-content'
|
|
446
504
|
if (todo) {
|
|
447
505
|
content.textContent = todo.content
|
|
@@ -459,6 +517,9 @@
|
|
|
459
517
|
} else if (blocked) {
|
|
460
518
|
sb.className = 'node-badge badge-blocked'
|
|
461
519
|
sb.textContent = t.blocked
|
|
520
|
+
} else if (todo.focus) {
|
|
521
|
+
sb.className = 'node-badge badge-focus'
|
|
522
|
+
sb.textContent = t.focus
|
|
462
523
|
} else {
|
|
463
524
|
sb.className = 'node-badge badge-pending'
|
|
464
525
|
sb.textContent = t.pending
|