@aicupa/plugin-todo-dependency 1.0.1 → 1.0.3

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 +31 -0
  3. package/view/index.html +131 -47
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicupa/plugin-todo-dependency",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Set and visualize dependencies between todo items",
5
5
  "description_zh": "设置并可视化待办事项之间的依赖关系",
6
6
  "main": "./service",
package/service.js CHANGED
@@ -78,6 +78,37 @@ module.exports = (api) => {
78
78
  }
79
79
  },
80
80
 
81
+ async toggleDone({ todoId, done, filePath }) {
82
+ try {
83
+ const content = await api.readFile(filePath)
84
+ const data = JSON.parse(content)
85
+ const todotree = data.todotree
86
+
87
+ function findAndUpdate(nodes) {
88
+ for (const node of nodes) {
89
+ if (node.todo && node.todo.id === todoId) {
90
+ node.todo.done = done
91
+ if (done) {
92
+ node.todo.doneAt = Date.now()
93
+ } else {
94
+ delete node.todo.doneAt
95
+ }
96
+ return true
97
+ }
98
+ if (node.children?.length && findAndUpdate(node.children)) return true
99
+ }
100
+ return false
101
+ }
102
+
103
+ findAndUpdate(todotree.tree)
104
+ await api.store('todotree', todotree, filePath)
105
+ await api.reload(filePath)
106
+ return { ok: true }
107
+ } catch (e) {
108
+ return { ok: false, error: e.message }
109
+ }
110
+ },
111
+
81
112
  async scanAllDeps({ filePath }) {
82
113
  try {
83
114
  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,17 +32,17 @@
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; }
45
+ .dark #graphArea { background: #1e1e1e; }
46
46
  #graphContainer { position: relative; min-width: 100%; min-height: 100%; }
47
47
 
48
48
  .node {
@@ -51,24 +51,34 @@
51
51
  padding: 10px 12px; cursor: default;
52
52
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
53
53
  transition: box-shadow 0.2s, transform 0.15s;
54
+ display: flex; align-items: flex-start; gap: 10px;
54
55
  }
55
56
  .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); }
57
+ .dark .node { background: #2d2d2d; border-color: #3c3c3c; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
57
58
  .node.done { opacity: 0.55; }
58
- .node.done .node-content { text-decoration: line-through; }
59
+ .node.done .node-content { text-decoration: line-through; color: #999; }
59
60
  .node.not-found { border-style: dashed; opacity: 0.65; }
60
61
 
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;
62
+ .node-check {
63
+ width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0;
64
+ border: 2px solid #d9d9d9; background: #fff; cursor: pointer;
65
+ display: flex; align-items: center; justify-content: center;
66
+ transition: all 0.15s; margin-top: 1px;
66
67
  }
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
- .dark .badge-pending { background: #332b00; color: #ffc53d; }
71
- .dark .badge-done { background: #0a2e0a; color: #73d13d; }
68
+ .node-check:hover { border-color: #52c41a; background: #f6ffed; }
69
+ .node-check:active { transform: scale(0.9); }
70
+ .node-check.checked { border-color: #52c41a; background: #52c41a; }
71
+ .node-check.checked::after {
72
+ content: ''; display: block; width: 4px; height: 7px;
73
+ border: solid #fff; border-width: 0 1.5px 1.5px 0;
74
+ transform: rotate(45deg) translate(-0.5px, -0.5px);
75
+ }
76
+ .dark .node-check { border-color: #555; background: #2d2d2d; }
77
+ .dark .node-check:hover { border-color: #73d13d; background: #1e3a1e; }
78
+ .dark .node-check.checked { border-color: #73d13d; background: #73d13d; }
79
+ .dark .node-check.checked::after { border-color: #2d2d2d; }
80
+
81
+ .node-body { flex: 1; min-width: 0; }
72
82
 
73
83
  .node-content {
74
84
  font-size: 12px; line-height: 1.5; word-break: break-word;
@@ -76,16 +86,25 @@
76
86
  -webkit-box-orient: vertical; overflow: hidden;
77
87
  }
78
88
 
89
+ .node-badge {
90
+ display: inline-block; font-size: 10px; padding: 1px 6px;
91
+ border-radius: 10px; margin-top: 5px;
92
+ }
93
+ .badge-pending { background: #fff7e6; color: #d46b08; }
94
+ .badge-done { background: #f6ffed; color: #389e0d; }
95
+ .badge-blocked { background: #fff1f0; color: #cf1322; }
96
+ .dark .badge-pending { background: #3a3000; color: #ffc53d; }
97
+ .dark .badge-done { background: #1e3a1e; color: #73d13d; }
98
+ .dark .badge-blocked { background: #3a1a1a; color: #ff4d4f; }
99
+
100
+ .node.blocked { border-style: dashed; opacity: 0.7; }
101
+ .dark .node.blocked { opacity: 0.7; }
102
+
79
103
  .level-default { border-color: #1890ff; }
80
104
  .level-secondary { border-color: #bbb; }
81
105
  .level-success { border-color: #52c41a; }
82
106
  .level-warning { border-color: #faad14; }
83
107
  .level-danger { border-color: #ff4d4f; }
84
- .level-default .node-id { background: #1890ff; }
85
- .level-secondary .node-id { background: #999; }
86
- .level-success .node-id { background: #52c41a; }
87
- .level-warning .node-id { background: #e8a100; }
88
- .level-danger .node-id { background: #ff4d4f; }
89
108
 
90
109
  #edgeSvg { position: absolute; top: 0; left: 0; pointer-events: none; }
91
110
 
@@ -133,14 +152,14 @@
133
152
  title: '依赖图谱', refresh: '刷新',
134
153
  emptyTitle: '暂无依赖关系',
135
154
  emptyHint: '右键 Todo 节点,选择「设置依赖」<br>即可建立待办之间的依赖关系',
136
- done: '已完成', pending: '待完成', notFound: '未找到该 Todo',
155
+ done: '已完成', pending: '待完成', blocked: '阻塞中', notFound: '未找到该 Todo',
137
156
  cycleWarning: '检测到循环依赖,部分节点的层级可能不准确',
138
157
  },
139
158
  en: {
140
159
  title: 'Dependency Graph', refresh: 'Refresh',
141
160
  emptyTitle: 'No dependencies yet',
142
161
  emptyHint: 'Right-click a Todo and select "Set Dependencies"<br>to create dependency relationships',
143
- done: 'Done', pending: 'Pending', notFound: 'Todo not found',
162
+ done: 'Done', pending: 'Pending', blocked: 'Blocked', notFound: 'Todo not found',
144
163
  cycleWarning: 'Cycle detected — some nodes may be positioned incorrectly',
145
164
  },
146
165
  }
@@ -294,32 +313,73 @@
294
313
  return el
295
314
  }
296
315
 
316
+ async function toggleTodoDone(todoId, done) {
317
+ try {
318
+ await callPlugin('toggleDone', { todoId, done, filePath: currentFilePath })
319
+ } catch {}
320
+ autoScan()
321
+ }
322
+
297
323
  function renderGraph(nodeIds, edges, positions, todoData, layout) {
298
324
  const container = document.getElementById('graphContainer')
299
325
  container.innerHTML = ''
300
326
  container.style.width = layout.width + 'px'
301
327
  container.style.height = layout.height + 'px'
302
328
 
303
- const edgeColor = isDark ? '#4a4a6a' : '#c8c8d0'
304
- const arrowColor = isDark ? '#6a6a8a' : '#aaa'
329
+ // Build dependency map: nodeId -> [depIds]
330
+ const depsOf = {}
331
+ for (const [from, to] of edges) {
332
+ if (!depsOf[to]) depsOf[to] = []
333
+ depsOf[to].push(from)
334
+ }
335
+
336
+ function isBlocked(id) {
337
+ const deps = depsOf[id]
338
+ if (!deps?.length) return false
339
+ const todo = todoData[id]
340
+ if (todo?.done) return false
341
+ return deps.some(depId => {
342
+ const dep = todoData[depId]
343
+ return !dep || !dep.done
344
+ })
345
+ }
346
+
347
+ const edgeColor = isDark ? '#555' : '#c8c8d0'
348
+ const blockedEdgeColor = isDark ? '#5c2020' : '#ffccc7'
349
+ const arrowColor = isDark ? '#777' : '#aaa'
350
+ const blockedArrowColor = isDark ? '#ff4d4f' : '#ff7875'
305
351
  const svg = svgEl('svg', { id: 'edgeSvg', width: layout.width, height: layout.height })
306
352
  const defs = svgEl('defs', {})
307
- const marker = svgEl('marker', {
308
- id: 'arrow', markerWidth: 10, markerHeight: 7,
309
- refX: 9, refY: 3.5, orient: 'auto', markerUnits: 'strokeWidth',
310
- })
311
- marker.appendChild(svgEl('polygon', { points: '0 0.5,9 3.5,0 6.5', fill: arrowColor }))
312
- defs.appendChild(marker); svg.appendChild(defs)
353
+ function addMarker(id, color) {
354
+ const m = svgEl('marker', {
355
+ id, markerWidth: 8, markerHeight: 6,
356
+ refX: 4, refY: 3, orient: 'auto-start-reverse', markerUnits: 'userSpaceOnUse',
357
+ })
358
+ m.appendChild(svgEl('polygon', { points: '0 0,8 3,0 6', fill: color }))
359
+ defs.appendChild(m)
360
+ }
361
+ addMarker('arrow', arrowColor)
362
+ addMarker('arrow-blocked', blockedArrowColor)
363
+ svg.appendChild(defs)
313
364
 
365
+ const ARROW_H = 6
314
366
  for (const [from, to] of edges) {
315
367
  const p1 = positions[from], p2 = positions[to]
316
368
  if (!p1 || !p2) continue
317
369
  const x1 = p1.x + NODE_W / 2, y1 = p1.y + NODE_H
318
- const x2 = p2.x + NODE_W / 2, y2 = p2.y
319
- const cp = Math.max(Math.abs(y2 - y1) * 0.35, 20)
370
+ const x2 = p2.x + NODE_W / 2, y2 = p2.y - ARROW_H
371
+ const dy = Math.abs(y2 - y1)
372
+ const cp = Math.max(dy * 0.4, 30)
373
+ const fromDone = todoData[from]?.done
374
+ const toBlocked = isBlocked(to)
375
+ const isBlockingEdge = toBlocked && !fromDone
320
376
  svg.appendChild(svgEl('path', {
321
377
  d: `M${x1},${y1} C${x1},${y1 + cp} ${x2},${y2 - cp} ${x2},${y2}`,
322
- fill: 'none', stroke: edgeColor, 'stroke-width': 2, 'marker-end': 'url(#arrow)',
378
+ fill: 'none',
379
+ stroke: isBlockingEdge ? blockedEdgeColor : edgeColor,
380
+ 'stroke-width': 1.5,
381
+ 'stroke-dasharray': isBlockingEdge ? '6,3' : 'none',
382
+ 'marker-end': isBlockingEdge ? 'url(#arrow-blocked)' : 'url(#arrow)',
323
383
  }))
324
384
  }
325
385
  container.appendChild(svg)
@@ -327,27 +387,51 @@
327
387
  for (const id of nodeIds) {
328
388
  const pos = positions[id]; if (!pos) continue
329
389
  const todo = todoData[id]
390
+ const blocked = isBlocked(id)
330
391
  const el = document.createElement('div'); el.className = 'node'
331
392
  if (todo) {
332
393
  el.classList.add('level-' + (todo.level || 'default'))
333
394
  if (todo.done) el.classList.add('done')
395
+ else if (blocked) el.classList.add('blocked')
334
396
  } else el.classList.add('not-found')
335
397
  el.style.cssText = `left:${pos.x}px;top:${pos.y}px;width:${NODE_W}px;`
336
398
 
337
- const header = document.createElement('div'); header.className = 'node-header'
338
- const badge = document.createElement('span'); badge.className = 'node-id'
339
- badge.textContent = '#' + id; header.appendChild(badge)
340
399
  if (todo) {
341
- const sb = document.createElement('span')
342
- sb.className = 'node-badge ' + (todo.done ? 'badge-done' : 'badge-pending')
343
- sb.textContent = todo.done ? t.done : t.pending
344
- header.appendChild(sb)
400
+ const check = document.createElement('div')
401
+ check.className = 'node-check' + (todo.done ? ' checked' : '')
402
+ check.addEventListener('click', e => {
403
+ e.stopPropagation()
404
+ toggleTodoDone(id, !todo.done)
405
+ })
406
+ el.appendChild(check)
345
407
  }
346
- el.appendChild(header)
408
+
409
+ const body = document.createElement('div'); body.className = 'node-body'
347
410
  const content = document.createElement('div'); content.className = 'node-content'
348
- if (todo) content.textContent = todo.content
349
- else { content.textContent = t.notFound; content.style.color = '#ff4d4f'; content.style.fontStyle = 'italic' }
350
- el.appendChild(content); container.appendChild(el)
411
+ if (todo) {
412
+ content.textContent = todo.content
413
+ } else {
414
+ content.textContent = t.notFound
415
+ content.style.color = '#ff4d4f'; content.style.fontStyle = 'italic'
416
+ }
417
+ body.appendChild(content)
418
+
419
+ if (todo) {
420
+ const sb = document.createElement('span')
421
+ if (todo.done) {
422
+ sb.className = 'node-badge badge-done'
423
+ sb.textContent = t.done
424
+ } else if (blocked) {
425
+ sb.className = 'node-badge badge-blocked'
426
+ sb.textContent = t.blocked
427
+ } else {
428
+ sb.className = 'node-badge badge-pending'
429
+ sb.textContent = t.pending
430
+ }
431
+ body.appendChild(sb)
432
+ }
433
+
434
+ el.appendChild(body); container.appendChild(el)
351
435
  }
352
436
  }
353
437