@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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/service.js +38 -2
  3. package/view/index.html +130 -58
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicupa/plugin-todo-dependency",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Set and visualize dependencies between todo items",
5
5
  "description_zh": "设置并可视化待办事项之间的依赖关系",
6
6
  "main": "./service",
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 (depIds.length > 0) {
61
- node.todo.depIds = 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: #e0e0e0; }
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: #16213e; border-bottom-color: #2a2a4a; }
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: #aaa; }
36
- .dark .btn-ghost:hover { background: #2a2a4a; }
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: #332b00; border-bottom-color: #554400; color: #ffc53d; }
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: #1a1a2e; }
46
- #graphContainer { position: relative; min-width: 100%; min-height: 100%; }
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: #16213e; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
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-header { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
62
- .node-id {
63
- font-size: 10px; font-weight: 700; color: #fff;
64
- background: #1890ff; padding: 1px 7px; border-radius: 10px;
65
- flex-shrink: 0; letter-spacing: 0.3px;
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-badge { font-size: 10px; padding: 1px 6px; border-radius: 10px; flex-shrink: 0; }
68
- .badge-pending { background: #fff7e6; color: #d46b08; }
69
- .badge-done { background: #f6ffed; color: #389e0d; }
70
- .badge-blocked { background: #fff1f0; color: #cf1322; }
71
- .dark .badge-pending { background: #332b00; color: #ffc53d; }
72
- .dark .badge-done { background: #0a2e0a; color: #73d13d; }
73
- .dark .badge-blocked { background: #2a1215; color: #ff4d4f; }
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.blocked { border-style: dashed; opacity: 0.7; }
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
- const nodeIds = Object.keys(todos).map(Number)
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
- container.style.width = layout.width + 'px'
306
- container.style.height = layout.height + 'px'
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 ? '#4a4a6a' : '#c8c8d0'
382
+ const edgeColor = isDark ? '#555' : '#c8c8d0'
327
383
  const blockedEdgeColor = isDark ? '#5c2020' : '#ffccc7'
328
- const arrowColor = isDark ? '#6a6a8a' : '#aaa'
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
- const marker = svgEl('marker', {
333
- id: 'arrow', markerWidth: 10, markerHeight: 7,
334
- refX: 9, refY: 3.5, orient: 'auto', markerUnits: 'strokeWidth',
335
- })
336
- marker.appendChild(svgEl('polygon', { points: '0 0.5,9 3.5,0 6.5', fill: arrowColor }))
337
- const blockedMarker = svgEl('marker', {
338
- id: 'arrow-blocked', markerWidth: 10, markerHeight: 7,
339
- refX: 9, refY: 3.5, orient: 'auto', markerUnits: 'strokeWidth',
340
- })
341
- blockedMarker.appendChild(svgEl('polygon', { points: '0 0.5,9 3.5,0 6.5', fill: blockedArrowColor }))
342
- defs.appendChild(marker); defs.appendChild(blockedMarker); svg.appendChild(defs)
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 cp = Math.max(Math.abs(y2 - y1) * 0.35, 20)
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': 2,
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
- container.appendChild(svg)
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
- const header = document.createElement('div'); header.className = 'node-header'
377
- const badge = document.createElement('span'); badge.className = 'node-id'
378
- badge.textContent = '#' + id; header.appendChild(badge)
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
- header.appendChild(sb)
466
+ body.appendChild(sb)
392
467
  }
393
- el.appendChild(header)
394
- const content = document.createElement('div'); content.className = 'node-content'
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