@aicupa/plugin-todo-dependency 1.0.2 → 1.0.4
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 +38 -2
- package/view/index.html +130 -58
package/package.json
CHANGED
package/service.js
CHANGED
|
@@ -54,11 +54,16 @@ module.exports = (api) => {
|
|
|
54
54
|
const data = JSON.parse(content)
|
|
55
55
|
const todotree = data.todotree
|
|
56
56
|
|
|
57
|
+
const allTodos = []
|
|
58
|
+
flattenTodos(todotree.tree, allTodos)
|
|
59
|
+
const existingIds = new Set(allTodos.map(t => t.id))
|
|
60
|
+
const validDepIds = depIds.filter(id => existingIds.has(id))
|
|
61
|
+
|
|
57
62
|
function findAndUpdate(nodes) {
|
|
58
63
|
for (const node of nodes) {
|
|
59
64
|
if (node.todo && node.todo.id === todoId) {
|
|
60
|
-
if (
|
|
61
|
-
node.todo.depIds =
|
|
65
|
+
if (validDepIds.length > 0) {
|
|
66
|
+
node.todo.depIds = validDepIds
|
|
62
67
|
} else {
|
|
63
68
|
delete node.todo.depIds
|
|
64
69
|
}
|
|
@@ -78,6 +83,37 @@ module.exports = (api) => {
|
|
|
78
83
|
}
|
|
79
84
|
},
|
|
80
85
|
|
|
86
|
+
async toggleDone({ todoId, done, filePath }) {
|
|
87
|
+
try {
|
|
88
|
+
const content = await api.readFile(filePath)
|
|
89
|
+
const data = JSON.parse(content)
|
|
90
|
+
const todotree = data.todotree
|
|
91
|
+
|
|
92
|
+
function findAndUpdate(nodes) {
|
|
93
|
+
for (const node of nodes) {
|
|
94
|
+
if (node.todo && node.todo.id === todoId) {
|
|
95
|
+
node.todo.done = done
|
|
96
|
+
if (done) {
|
|
97
|
+
node.todo.doneAt = Date.now()
|
|
98
|
+
} else {
|
|
99
|
+
delete node.todo.doneAt
|
|
100
|
+
}
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
if (node.children?.length && findAndUpdate(node.children)) return true
|
|
104
|
+
}
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
findAndUpdate(todotree.tree)
|
|
109
|
+
await api.store('todotree', todotree, filePath)
|
|
110
|
+
await api.reload(filePath)
|
|
111
|
+
return { ok: true }
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return { ok: false, error: e.message }
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
81
117
|
async scanAllDeps({ filePath }) {
|
|
82
118
|
try {
|
|
83
119
|
const data = await api.getTree(filePath)
|
package/view/index.html
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
color: #333;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
body.dark #graphUI { color: #
|
|
19
|
+
body.dark #graphUI { color: #cccccc; }
|
|
20
20
|
|
|
21
21
|
.toolbar {
|
|
22
22
|
padding: 10px 16px; background: #fff; border-bottom: 1px solid #e8e8e8;
|
|
23
23
|
display: flex; align-items: center; gap: 10px; flex-shrink: 0;
|
|
24
24
|
}
|
|
25
|
-
.dark .toolbar { background: #
|
|
25
|
+
.dark .toolbar { background: #252526; border-bottom-color: #3c3c3c; }
|
|
26
26
|
.toolbar-title { font-size: 14px; font-weight: 600; flex: 1; }
|
|
27
27
|
|
|
28
28
|
.btn {
|
|
@@ -32,18 +32,19 @@
|
|
|
32
32
|
.btn:active { transform: scale(0.97); }
|
|
33
33
|
.btn-ghost { background: transparent; color: #666; }
|
|
34
34
|
.btn-ghost:hover { background: #f0f0f0; }
|
|
35
|
-
.dark .btn-ghost { color: #
|
|
36
|
-
.dark .btn-ghost:hover { background: #
|
|
35
|
+
.dark .btn-ghost { color: #9d9d9d; }
|
|
36
|
+
.dark .btn-ghost:hover { background: #3c3c3c; }
|
|
37
37
|
|
|
38
38
|
.warning {
|
|
39
39
|
background: #fffbe6; border-bottom: 1px solid #ffe58f; color: #ad6800;
|
|
40
40
|
padding: 6px 16px; font-size: 12px; flex-shrink: 0;
|
|
41
41
|
}
|
|
42
|
-
.dark .warning { background: #
|
|
42
|
+
.dark .warning { background: #3a3000; border-bottom-color: #554400; color: #ffc53d; }
|
|
43
43
|
|
|
44
44
|
#graphArea { flex: 1; overflow: auto; position: relative; background: #f7f8fa; }
|
|
45
|
-
.dark #graphArea { background: #
|
|
46
|
-
#graphContainer {
|
|
45
|
+
.dark #graphArea { background: #1e1e1e; }
|
|
46
|
+
#graphContainer { min-width: 100%; min-height: 100%; display: flex; align-items: center; justify-content: center; }
|
|
47
|
+
#graphInner { position: relative; flex-shrink: 0; }
|
|
47
48
|
|
|
48
49
|
.node {
|
|
49
50
|
position: absolute; width: 200px; background: #fff;
|
|
@@ -51,29 +52,34 @@
|
|
|
51
52
|
padding: 10px 12px; cursor: default;
|
|
52
53
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
|
53
54
|
transition: box-shadow 0.2s, transform 0.15s;
|
|
55
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
54
56
|
}
|
|
55
57
|
.node:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); transform: translateY(-1px); }
|
|
56
|
-
.dark .node { background: #
|
|
58
|
+
.dark .node { background: #2d2d2d; border-color: #3c3c3c; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
|
|
57
59
|
.node.done { opacity: 0.55; }
|
|
58
|
-
.node.done .node-content { text-decoration: line-through; }
|
|
60
|
+
.node.done .node-content { text-decoration: line-through; color: #999; }
|
|
59
61
|
.node.not-found { border-style: dashed; opacity: 0.65; }
|
|
60
62
|
|
|
61
|
-
.node-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
.node-check {
|
|
64
|
+
width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0;
|
|
65
|
+
border: 2px solid #d9d9d9; background: #fff; cursor: pointer;
|
|
66
|
+
display: flex; align-items: center; justify-content: center;
|
|
67
|
+
transition: all 0.15s; margin-top: 1px;
|
|
66
68
|
}
|
|
67
|
-
.node-
|
|
68
|
-
.
|
|
69
|
-
.
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
.node-check:hover { border-color: #52c41a; background: #f6ffed; }
|
|
70
|
+
.node-check:active { transform: scale(0.9); }
|
|
71
|
+
.node-check.checked { border-color: #52c41a; background: #52c41a; }
|
|
72
|
+
.node-check.checked::after {
|
|
73
|
+
content: ''; display: block; width: 4px; height: 7px;
|
|
74
|
+
border: solid #fff; border-width: 0 1.5px 1.5px 0;
|
|
75
|
+
transform: rotate(45deg) translate(-0.5px, -0.5px);
|
|
76
|
+
}
|
|
77
|
+
.dark .node-check { border-color: #555; background: #2d2d2d; }
|
|
78
|
+
.dark .node-check:hover { border-color: #73d13d; background: #1e3a1e; }
|
|
79
|
+
.dark .node-check.checked { border-color: #73d13d; background: #73d13d; }
|
|
80
|
+
.dark .node-check.checked::after { border-color: #2d2d2d; }
|
|
74
81
|
|
|
75
|
-
.node
|
|
76
|
-
.dark .node.blocked { opacity: 0.7; }
|
|
82
|
+
.node-body { flex: 1; min-width: 0; }
|
|
77
83
|
|
|
78
84
|
.node-content {
|
|
79
85
|
font-size: 12px; line-height: 1.5; word-break: break-word;
|
|
@@ -81,16 +87,25 @@
|
|
|
81
87
|
-webkit-box-orient: vertical; overflow: hidden;
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
.node-badge {
|
|
91
|
+
display: inline-block; font-size: 10px; padding: 1px 6px;
|
|
92
|
+
border-radius: 10px; margin-top: 5px;
|
|
93
|
+
}
|
|
94
|
+
.badge-pending { background: #fff7e6; color: #d46b08; }
|
|
95
|
+
.badge-done { background: #f6ffed; color: #389e0d; }
|
|
96
|
+
.badge-blocked { background: #fff1f0; color: #cf1322; }
|
|
97
|
+
.dark .badge-pending { background: #3a3000; color: #ffc53d; }
|
|
98
|
+
.dark .badge-done { background: #1e3a1e; color: #73d13d; }
|
|
99
|
+
.dark .badge-blocked { background: #3a1a1a; color: #ff4d4f; }
|
|
100
|
+
|
|
101
|
+
.node.blocked { border-style: dashed; opacity: 0.7; }
|
|
102
|
+
.dark .node.blocked { opacity: 0.7; }
|
|
103
|
+
|
|
84
104
|
.level-default { border-color: #1890ff; }
|
|
85
105
|
.level-secondary { border-color: #bbb; }
|
|
86
106
|
.level-success { border-color: #52c41a; }
|
|
87
107
|
.level-warning { border-color: #faad14; }
|
|
88
108
|
.level-danger { border-color: #ff4d4f; }
|
|
89
|
-
.level-default .node-id { background: #1890ff; }
|
|
90
|
-
.level-secondary .node-id { background: #999; }
|
|
91
|
-
.level-success .node-id { background: #52c41a; }
|
|
92
|
-
.level-warning .node-id { background: #e8a100; }
|
|
93
|
-
.level-danger .node-id { background: #ff4d4f; }
|
|
94
109
|
|
|
95
110
|
#edgeSvg { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
96
111
|
|
|
@@ -227,8 +242,39 @@
|
|
|
227
242
|
} catch { return }
|
|
228
243
|
if (!data) return
|
|
229
244
|
|
|
230
|
-
const { todos, edges } = data
|
|
231
|
-
|
|
245
|
+
const { todos, edges: allEdges } = data
|
|
246
|
+
|
|
247
|
+
// Build adjacency (both directions) to find connected components
|
|
248
|
+
const allIds = new Set()
|
|
249
|
+
const neighbors = {}
|
|
250
|
+
for (const [from, to] of allEdges) {
|
|
251
|
+
allIds.add(from); allIds.add(to)
|
|
252
|
+
if (!neighbors[from]) neighbors[from] = []
|
|
253
|
+
if (!neighbors[to]) neighbors[to] = []
|
|
254
|
+
neighbors[from].push(to)
|
|
255
|
+
neighbors[to].push(from)
|
|
256
|
+
}
|
|
257
|
+
// BFS to find connected components, keep components that have any undone node
|
|
258
|
+
const visited = new Set()
|
|
259
|
+
const visibleIds = new Set()
|
|
260
|
+
for (const start of allIds) {
|
|
261
|
+
if (visited.has(start)) continue
|
|
262
|
+
const comp = []
|
|
263
|
+
const queue = [start]
|
|
264
|
+
visited.add(start)
|
|
265
|
+
while (queue.length) {
|
|
266
|
+
const cur = queue.shift()
|
|
267
|
+
comp.push(cur)
|
|
268
|
+
for (const nb of (neighbors[cur] || [])) {
|
|
269
|
+
if (!visited.has(nb)) { visited.add(nb); queue.push(nb) }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (comp.some(id => !todos[id]?.done)) {
|
|
273
|
+
for (const id of comp) visibleIds.add(id)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const edges = allEdges.filter(([from, to]) => visibleIds.has(from) && visibleIds.has(to))
|
|
277
|
+
const nodeIds = [...visibleIds]
|
|
232
278
|
if (!nodeIds.length) { showEmpty(); return }
|
|
233
279
|
|
|
234
280
|
const { layers, hasCycle } = assignLayers(nodeIds, edges)
|
|
@@ -299,11 +345,21 @@
|
|
|
299
345
|
return el
|
|
300
346
|
}
|
|
301
347
|
|
|
348
|
+
async function toggleTodoDone(todoId, done) {
|
|
349
|
+
try {
|
|
350
|
+
await callPlugin('toggleDone', { todoId, done, filePath: currentFilePath })
|
|
351
|
+
} catch {}
|
|
352
|
+
autoScan()
|
|
353
|
+
}
|
|
354
|
+
|
|
302
355
|
function renderGraph(nodeIds, edges, positions, todoData, layout) {
|
|
303
356
|
const container = document.getElementById('graphContainer')
|
|
304
357
|
container.innerHTML = ''
|
|
305
|
-
|
|
306
|
-
|
|
358
|
+
const inner = document.createElement('div')
|
|
359
|
+
inner.id = 'graphInner'
|
|
360
|
+
inner.style.width = layout.width + 'px'
|
|
361
|
+
inner.style.height = layout.height + 'px'
|
|
362
|
+
container.appendChild(inner)
|
|
307
363
|
|
|
308
364
|
// Build dependency map: nodeId -> [depIds]
|
|
309
365
|
const depsOf = {}
|
|
@@ -323,30 +379,32 @@
|
|
|
323
379
|
})
|
|
324
380
|
}
|
|
325
381
|
|
|
326
|
-
const edgeColor = isDark ? '#
|
|
382
|
+
const edgeColor = isDark ? '#555' : '#c8c8d0'
|
|
327
383
|
const blockedEdgeColor = isDark ? '#5c2020' : '#ffccc7'
|
|
328
|
-
const arrowColor = isDark ? '#
|
|
384
|
+
const arrowColor = isDark ? '#777' : '#aaa'
|
|
329
385
|
const blockedArrowColor = isDark ? '#ff4d4f' : '#ff7875'
|
|
330
386
|
const svg = svgEl('svg', { id: 'edgeSvg', width: layout.width, height: layout.height })
|
|
331
387
|
const defs = svgEl('defs', {})
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
388
|
+
function addMarker(id, color) {
|
|
389
|
+
const m = svgEl('marker', {
|
|
390
|
+
id, markerWidth: 8, markerHeight: 6,
|
|
391
|
+
refX: 4, refY: 3, orient: 'auto-start-reverse', markerUnits: 'userSpaceOnUse',
|
|
392
|
+
})
|
|
393
|
+
m.appendChild(svgEl('polygon', { points: '0 0,8 3,0 6', fill: color }))
|
|
394
|
+
defs.appendChild(m)
|
|
395
|
+
}
|
|
396
|
+
addMarker('arrow', arrowColor)
|
|
397
|
+
addMarker('arrow-blocked', blockedArrowColor)
|
|
398
|
+
svg.appendChild(defs)
|
|
343
399
|
|
|
400
|
+
const ARROW_H = 6
|
|
344
401
|
for (const [from, to] of edges) {
|
|
345
402
|
const p1 = positions[from], p2 = positions[to]
|
|
346
403
|
if (!p1 || !p2) continue
|
|
347
404
|
const x1 = p1.x + NODE_W / 2, y1 = p1.y + NODE_H
|
|
348
|
-
const x2 = p2.x + NODE_W / 2, y2 = p2.y
|
|
349
|
-
const
|
|
405
|
+
const x2 = p2.x + NODE_W / 2, y2 = p2.y - ARROW_H
|
|
406
|
+
const dy = Math.abs(y2 - y1)
|
|
407
|
+
const cp = Math.max(dy * 0.4, 30)
|
|
350
408
|
const fromDone = todoData[from]?.done
|
|
351
409
|
const toBlocked = isBlocked(to)
|
|
352
410
|
const isBlockingEdge = toBlocked && !fromDone
|
|
@@ -354,12 +412,12 @@
|
|
|
354
412
|
d: `M${x1},${y1} C${x1},${y1 + cp} ${x2},${y2 - cp} ${x2},${y2}`,
|
|
355
413
|
fill: 'none',
|
|
356
414
|
stroke: isBlockingEdge ? blockedEdgeColor : edgeColor,
|
|
357
|
-
'stroke-width':
|
|
415
|
+
'stroke-width': 1.5,
|
|
358
416
|
'stroke-dasharray': isBlockingEdge ? '6,3' : 'none',
|
|
359
417
|
'marker-end': isBlockingEdge ? 'url(#arrow-blocked)' : 'url(#arrow)',
|
|
360
418
|
}))
|
|
361
419
|
}
|
|
362
|
-
|
|
420
|
+
inner.appendChild(svg)
|
|
363
421
|
|
|
364
422
|
for (const id of nodeIds) {
|
|
365
423
|
const pos = positions[id]; if (!pos) continue
|
|
@@ -373,9 +431,26 @@
|
|
|
373
431
|
} else el.classList.add('not-found')
|
|
374
432
|
el.style.cssText = `left:${pos.x}px;top:${pos.y}px;width:${NODE_W}px;`
|
|
375
433
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
434
|
+
if (todo) {
|
|
435
|
+
const check = document.createElement('div')
|
|
436
|
+
check.className = 'node-check' + (todo.done ? ' checked' : '')
|
|
437
|
+
check.addEventListener('click', e => {
|
|
438
|
+
e.stopPropagation()
|
|
439
|
+
toggleTodoDone(id, !todo.done)
|
|
440
|
+
})
|
|
441
|
+
el.appendChild(check)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const body = document.createElement('div'); body.className = 'node-body'
|
|
445
|
+
const content = document.createElement('div'); content.className = 'node-content'
|
|
446
|
+
if (todo) {
|
|
447
|
+
content.textContent = todo.content
|
|
448
|
+
} else {
|
|
449
|
+
content.textContent = t.notFound
|
|
450
|
+
content.style.color = '#ff4d4f'; content.style.fontStyle = 'italic'
|
|
451
|
+
}
|
|
452
|
+
body.appendChild(content)
|
|
453
|
+
|
|
379
454
|
if (todo) {
|
|
380
455
|
const sb = document.createElement('span')
|
|
381
456
|
if (todo.done) {
|
|
@@ -388,13 +463,10 @@
|
|
|
388
463
|
sb.className = 'node-badge badge-pending'
|
|
389
464
|
sb.textContent = t.pending
|
|
390
465
|
}
|
|
391
|
-
|
|
466
|
+
body.appendChild(sb)
|
|
392
467
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (todo) content.textContent = todo.content
|
|
396
|
-
else { content.textContent = t.notFound; content.style.color = '#ff4d4f'; content.style.fontStyle = 'italic' }
|
|
397
|
-
el.appendChild(content); container.appendChild(el)
|
|
468
|
+
|
|
469
|
+
el.appendChild(body); inner.appendChild(el)
|
|
398
470
|
}
|
|
399
471
|
}
|
|
400
472
|
|