poi-plugin-inventory-cleaner 1.17.1 → 1.18.0

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/i18n/en-US.json CHANGED
@@ -29,7 +29,10 @@
29
29
  "stock.consumeQuestEquip": "quest (equip)",
30
30
  "stock.consumeQuestStore": "quest (store)",
31
31
  "stock.consumeOther": "other",
32
- "stock.consumeHidden": "{{count}} completed quest(s) hidden",
32
+ "stock.consumeHidden": "{{count}} completed quest(s) hidden (click to view/undo)",
33
+ "stock.markDone": "Mark as completed",
34
+ "stock.undoDone": "Undo completed mark",
35
+ "stock.undo": "Undo",
33
36
  "stock.keepLabel": "Keep",
34
37
  "stock.keepUnit": "",
35
38
  "stock.keepPlaceholder": "guide",
package/i18n/zh-CN.json CHANGED
@@ -29,7 +29,10 @@
29
29
  "stock.consumeQuestEquip": "任务·装",
30
30
  "stock.consumeQuestStore": "任务·库",
31
31
  "stock.consumeOther": "其他",
32
- "stock.consumeHidden": "已隐藏 {{count}} 个已完成任务",
32
+ "stock.consumeHidden": "已隐藏 {{count}} 个已完成任务(点击查看/撤销)",
33
+ "stock.markDone": "标记为已完成",
34
+ "stock.undoDone": "撤销完成标记",
35
+ "stock.undo": "撤销",
33
36
  "stock.keepLabel": "保留",
34
37
  "stock.keepUnit": "件",
35
38
  "stock.keepPlaceholder": "攻略",
package/index.js CHANGED
@@ -10,10 +10,16 @@ const {
10
10
  stepKeep,
11
11
  sortRows,
12
12
  summarize,
13
+ questNoFromText,
13
14
  OTHER_CATEGORY,
14
15
  } = require('./lib/inventory')
15
16
  const { buildQuestNeeds, resetCaches: resetQuestNeeds } = require('./lib/quest-needs')
16
- const { resetCaches: resetQuestStatus } = require('./lib/quest-status')
17
+ const {
18
+ markManual,
19
+ recordQuestStates,
20
+ resetCaches: resetQuestStatus,
21
+ unmarkManual,
22
+ } = require('./lib/quest-status')
17
23
  const { CSS } = require('./lib/style')
18
24
 
19
25
  const DICTIONARIES = {
@@ -274,7 +280,7 @@ function CategoryPicker({ value, groups, onChange }) {
274
280
  )
275
281
  }
276
282
 
277
- function StockRow({ row, onKeepChange }) {
283
+ function StockRow({ row, onKeepChange, onMarkDone, onUndoDone }) {
278
284
  const verdict = VERDICT[row.verdict] || VERDICT.unknown
279
285
  return h(
280
286
  'div',
@@ -382,20 +388,63 @@ function StockRow({ row, onKeepChange }) {
382
388
  h(
383
389
  'ol',
384
390
  { className: 'stock-consume-list' },
385
- row.consumedItems.map((item, index) =>
386
- h(
387
- 'li',
388
- { key: index, className: 'is-' + item.kind },
389
- h('span', { className: 'stock-consume-tag' }, consumeLabel(item.tag)),
390
- h('span', { className: 'stock-consume-text' }, item.text),
391
+ row.consumedItems
392
+ .filter((item) => !item.done)
393
+ .map((item, index) =>
394
+ h(
395
+ 'li',
396
+ { key: index, className: 'is-' + item.kind },
397
+ h('span', { className: 'stock-consume-tag' }, consumeLabel(item.tag)),
398
+ h('span', { className: 'stock-consume-text' }, item.text),
399
+ item.kind === 'quest'
400
+ ? h(
401
+ 'button',
402
+ {
403
+ type: 'button',
404
+ className: 'stock-consume-done',
405
+ title: t('stock.markDone'),
406
+ onClick: () =>
407
+ onMarkDone(questNoFromText(item.text), item.text),
408
+ },
409
+ '✓',
410
+ )
411
+ : null,
412
+ ),
391
413
  ),
392
- ),
393
414
  ),
394
415
  row.consumedHidden > 0
395
416
  ? h(
396
- 'div',
417
+ 'details',
397
418
  { className: 'stock-consume-hidden' },
398
- t('stock.consumeHidden', { count: row.consumedHidden }),
419
+ h(
420
+ 'summary',
421
+ null,
422
+ t('stock.consumeHidden', { count: row.consumedHidden }),
423
+ ),
424
+ h(
425
+ 'ul',
426
+ { className: 'stock-consume-done-list' },
427
+ row.consumedItems
428
+ .filter((item) => item.done)
429
+ .map((item, index) =>
430
+ h(
431
+ 'li',
432
+ { key: index, className: 'is-' + item.kind },
433
+ h('span', { className: 'stock-consume-tag' }, consumeLabel(item.tag)),
434
+ h('span', { className: 'stock-consume-text' }, item.text),
435
+ h(
436
+ 'button',
437
+ {
438
+ type: 'button',
439
+ className: 'stock-consume-undo',
440
+ title: t('stock.undoDone'),
441
+ onClick: () => onUndoDone(questNoFromText(item.text)),
442
+ },
443
+ t('stock.undo'),
444
+ ),
445
+ ),
446
+ ),
447
+ ),
399
448
  )
400
449
  : null,
401
450
  )
@@ -460,6 +509,11 @@ function App() {
460
509
  const detail = event && event.detail
461
510
  const path = detail && detail.path ? String(detail.path) : ''
462
511
  if (!/questlist/.test(path)) return
512
+ try {
513
+ recordQuestStates(detail)
514
+ } catch (_error) {
515
+ // 记录失败不影响刷新
516
+ }
463
517
  resetQuestNeeds()
464
518
  resetQuestStatus()
465
519
  setQuestTick((value) => value + 1)
@@ -479,6 +533,18 @@ function App() {
479
533
  setQuestTick((value) => value + 1)
480
534
  }, [])
481
535
 
536
+ const handleMarkDone = React.useCallback((no, title) => {
537
+ if (!no) return
538
+ markManual(no, title)
539
+ setQuestTick((value) => value + 1)
540
+ }, [])
541
+
542
+ const handleUndoDone = React.useCallback((no) => {
543
+ if (!no) return
544
+ unmarkManual(no)
545
+ setQuestTick((value) => value + 1)
546
+ }, [])
547
+
482
548
  const handleScroll = React.useCallback((event) => {
483
549
  setShowTop(event.currentTarget.scrollTop > 400)
484
550
  }, [])
@@ -781,7 +847,15 @@ function App() {
781
847
  : h(
782
848
  'div',
783
849
  { className: 'stock-list' },
784
- visible.map((row) => h(StockRow, { key: row.id, row, onKeepChange: handleKeepChange })),
850
+ visible.map((row) =>
851
+ h(StockRow, {
852
+ key: row.id,
853
+ row,
854
+ onKeepChange: handleKeepChange,
855
+ onMarkDone: handleMarkDone,
856
+ onUndoDone: handleUndoDone,
857
+ }),
858
+ ),
785
859
  ),
786
860
  h('p', { className: 'stock-warn' }, t('stock.warn')),
787
861
  h('p', { className: 'stock-source' }, t('stock.source')),
package/lib/inventory.js CHANGED
@@ -168,7 +168,7 @@ function buildInventory(state, options = {}) {
168
168
  usage: rule ? rule.usage : '',
169
169
  obtain: rule ? rule.obtain : '',
170
170
  consumed,
171
- consumedItems: allConsumed.filter((item) => !item.done),
171
+ consumedItems: allConsumed,
172
172
  consumedHidden: allConsumed.filter((item) => item.done).length,
173
173
  stage: guide ? guide.stage : '',
174
174
  note: guide ? guide.note : '',
@@ -57,6 +57,102 @@ function questPlannerStatePath() {
57
57
  return path.join(root, 'state.json')
58
58
  }
59
59
 
60
+ function pluginDataDir() {
61
+ return path.join(appDataPath(), 'poi', 'plugin-data', 'poi-plugin-inventory-cleaner')
62
+ }
63
+
64
+ function readJsonFile(file) {
65
+ try {
66
+ return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''))
67
+ } catch (_error) {
68
+ return null
69
+ }
70
+ }
71
+
72
+ function writeJsonFile(file, value) {
73
+ try {
74
+ fs.mkdirSync(path.dirname(file), { recursive: true })
75
+ fs.writeFileSync(file, JSON.stringify(value, null, 1), 'utf8')
76
+ } catch (_error) {
77
+ // 写不进就只影响本次
78
+ }
79
+ }
80
+
81
+ function manualPath() {
82
+ return path.join(pluginDataDir(), 'quest-manual.json')
83
+ }
84
+
85
+ function autoPath() {
86
+ return path.join(pluginDataDir(), 'quest-auto.json')
87
+ }
88
+
89
+ function readMarked(file) {
90
+ const raw = readJsonFile(file)
91
+ const nos = new Set()
92
+ if (raw && typeof raw === 'object') {
93
+ for (const key of Object.keys(raw)) {
94
+ const no = Number(key)
95
+ if (no) nos.add(no)
96
+ }
97
+ }
98
+ return nos
99
+ }
100
+
101
+ /** 用户手动标记完成的任务。 */
102
+ function getManualCompleted() {
103
+ return readMarked(manualPath())
104
+ }
105
+
106
+ /** 从任务列表响应里记下来的「达成」任务(api_state === 3)。 */
107
+ function getAutoCompleted() {
108
+ return readMarked(autoPath())
109
+ }
110
+
111
+ function markManual(no, title) {
112
+ const value = Number(no)
113
+ if (!value) return false
114
+ const file = manualPath()
115
+ const raw = readJsonFile(file) || {}
116
+ raw[String(value)] = { at: Date.now(), title: String(title || '') }
117
+ writeJsonFile(file, raw)
118
+ return true
119
+ }
120
+
121
+ function unmarkManual(no) {
122
+ const value = Number(no)
123
+ if (!value) return false
124
+ const file = manualPath()
125
+ const raw = readJsonFile(file) || {}
126
+ if (!raw[String(value)]) return false
127
+ delete raw[String(value)]
128
+ writeJsonFile(file, raw)
129
+ return true
130
+ }
131
+
132
+ /**
133
+ * 记录一次 questlist 响应:api_state === 3 的任务记为已完成,长期保存。
134
+ */
135
+ function recordQuestStates(detail) {
136
+ const body = detail && detail.body
137
+ const list = body && Array.isArray(body.api_list) ? body.api_list : null
138
+ if (!list || list.length === 0) return 0
139
+
140
+ const file = autoPath()
141
+ const raw = readJsonFile(file) || {}
142
+ let added = 0
143
+ for (const quest of list) {
144
+ if (!quest) continue
145
+ const no = Number(quest.api_no)
146
+ if (!no || Number(quest.api_state) !== 3) continue
147
+ if (!raw[String(no)]) {
148
+ raw[String(no)] = { at: Date.now(), title: String(quest.api_title || '') }
149
+ added += 1
150
+ }
151
+ }
152
+ if (added > 0) writeJsonFile(file, raw)
153
+ return added
154
+ }
155
+
60
156
  function emptyPlannerState() {
61
157
  return { completed: new Set(), available: new Set() }
62
158
  }
@@ -199,6 +295,8 @@ function getCompletedQuestNos(state) {
199
295
  const completed = new Set([
200
296
  ...fromState.completed,
201
297
  ...planner.completed,
298
+ ...getManualCompleted(),
299
+ ...getAutoCompleted(),
202
300
  ])
203
301
  const available = new Set([
204
302
  ...fromState.available,
@@ -215,8 +313,13 @@ function resetCaches() {
215
313
 
216
314
  module.exports = {
217
315
  expandCompleted,
316
+ getAutoCompleted,
218
317
  getCompletedQuestNos,
318
+ getManualCompleted,
319
+ markManual,
219
320
  readCachedAvailable,
220
321
  readPlannerState,
322
+ recordQuestStates,
221
323
  resetCaches,
324
+ unmarkManual,
222
325
  }
package/lib/style.js CHANGED
@@ -438,9 +438,77 @@ const CSS = `
438
438
  }
439
439
 
440
440
  .stock-consume-hidden {
441
- margin-top: 2px;
442
- font-size: 10px;
443
- opacity: 0.4;
441
+ margin-top: 4px;
442
+ font-size: 10.5px;
443
+ opacity: 0.55;
444
+ }
445
+
446
+ .stock-consume-hidden > summary {
447
+ cursor: pointer;
448
+ list-style: none;
449
+ user-select: none;
450
+ }
451
+
452
+ .stock-consume-hidden > summary::-webkit-details-marker {
453
+ display: none;
454
+ }
455
+
456
+ .stock-consume-done-list {
457
+ margin: 4px 0 0;
458
+ padding: 0;
459
+ list-style: none;
460
+ display: grid;
461
+ gap: 3px;
462
+ }
463
+
464
+ .stock-consume-done-list li {
465
+ display: flex;
466
+ align-items: baseline;
467
+ gap: 6px;
468
+ }
469
+
470
+ .stock-consume-done-list .stock-consume-text {
471
+ text-decoration: line-through;
472
+ opacity: 0.7;
473
+ }
474
+
475
+ .stock-consume-done {
476
+ flex: none;
477
+ width: 20px;
478
+ height: 20px;
479
+ padding: 0;
480
+ border: 1px solid var(--stock-border);
481
+ border-radius: 5px;
482
+ color: var(--stock-green);
483
+ background: transparent;
484
+ font-family: inherit;
485
+ font-size: 11px;
486
+ line-height: 1;
487
+ cursor: pointer;
488
+ opacity: 0.55;
489
+ }
490
+
491
+ .stock-consume-done:hover {
492
+ opacity: 1;
493
+ border-color: var(--stock-green);
494
+ }
495
+
496
+ .stock-consume-undo {
497
+ flex: none;
498
+ height: 18px;
499
+ padding: 0 6px;
500
+ border: 1px solid var(--stock-border);
501
+ border-radius: 5px;
502
+ color: inherit;
503
+ background: transparent;
504
+ font-family: inherit;
505
+ font-size: 9.5px;
506
+ cursor: pointer;
507
+ }
508
+
509
+ .stock-consume-undo:hover {
510
+ border-color: var(--stock-indigo);
511
+ color: var(--stock-indigo);
444
512
  }
445
513
 
446
514
  .stock-consume-text {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-inventory-cleaner",
3
- "version": "1.17.1",
3
+ "version": "1.18.0",
4
4
  "description": "库存清理大师:对照改修攻略与保留上限,找出囤积过量、可以清理的装备,并标出不能拆的改修材料。",
5
5
  "main": "index.js",
6
6
  "files": [