poi-plugin-inventory-cleaner 1.16.2 → 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",
@@ -39,6 +42,7 @@
39
42
  "stock.batchToggle": "Batch keep",
40
43
  "stock.categoryAll": "All categories",
41
44
  "stock.backToTop": "Back to top",
45
+ "stock.refreshQuest": "Reload quest data",
42
46
  "stock.categoryTitle": "Filter by in-game equipment type",
43
47
  "stock.sortTitle": "Sort by",
44
48
  "stock.sortVerdict": "Guide verdict",
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": "攻略",
@@ -39,6 +42,7 @@
39
42
  "stock.batchToggle": "批量保留",
40
43
  "stock.categoryAll": "全部类别",
41
44
  "stock.backToTop": "回到顶部",
45
+ "stock.refreshQuest": "重新读取任务数据",
42
46
  "stock.categoryTitle": "按游戏装备类型筛选",
43
47
  "stock.sortTitle": "排序方式",
44
48
  "stock.sortVerdict": "按攻略判定",
package/index.js CHANGED
@@ -10,9 +10,16 @@ const {
10
10
  stepKeep,
11
11
  sortRows,
12
12
  summarize,
13
+ questNoFromText,
13
14
  OTHER_CATEGORY,
14
15
  } = require('./lib/inventory')
15
- const { buildQuestNeeds } = require('./lib/quest-needs')
16
+ const { buildQuestNeeds, resetCaches: resetQuestNeeds } = require('./lib/quest-needs')
17
+ const {
18
+ markManual,
19
+ recordQuestStates,
20
+ resetCaches: resetQuestStatus,
21
+ unmarkManual,
22
+ } = require('./lib/quest-status')
16
23
  const { CSS } = require('./lib/style')
17
24
 
18
25
  const DICTIONARIES = {
@@ -71,18 +78,34 @@ function getState() {
71
78
  let snapshot = null
72
79
  let snapshotEquips = null
73
80
  let snapshotConst = null
81
+ let snapshotQuests = null
82
+ let snapshotExtQuest = null
74
83
 
75
84
  function getSnapshot() {
76
85
  const state = getState()
77
86
  const equips =
78
87
  state && state.info && state.info.equips ? state.info.equips : null
79
88
  const constant = state ? state.const : null
80
- if (snapshot && snapshotEquips === equips && snapshotConst === constant) {
89
+ const quests =
90
+ state && state.info && state.info.quests ? state.info.quests : null
91
+ const ext = state && state.ext ? state.ext : null
92
+ const extQuest =
93
+ (ext && (ext['poi-plugin-quest-info-2'] || ext['poi-plugin-quest-info'])) || null
94
+
95
+ if (
96
+ snapshot &&
97
+ snapshotEquips === equips &&
98
+ snapshotConst === constant &&
99
+ snapshotQuests === quests &&
100
+ snapshotExtQuest === extQuest
101
+ ) {
81
102
  return snapshot
82
103
  }
83
104
  snapshotEquips = equips
84
105
  snapshotConst = constant
85
- snapshot = state ? { info: { equips }, const: constant } : null
106
+ snapshotQuests = quests
107
+ snapshotExtQuest = extQuest
108
+ snapshot = state ? { info: { equips, quests }, const: constant, ext } : null
86
109
  return snapshot
87
110
  }
88
111
 
@@ -257,7 +280,7 @@ function CategoryPicker({ value, groups, onChange }) {
257
280
  )
258
281
  }
259
282
 
260
- function StockRow({ row, onKeepChange }) {
283
+ function StockRow({ row, onKeepChange, onMarkDone, onUndoDone }) {
261
284
  const verdict = VERDICT[row.verdict] || VERDICT.unknown
262
285
  return h(
263
286
  'div',
@@ -365,20 +388,63 @@ function StockRow({ row, onKeepChange }) {
365
388
  h(
366
389
  'ol',
367
390
  { className: 'stock-consume-list' },
368
- row.consumedItems.map((item, index) =>
369
- h(
370
- 'li',
371
- { key: index, className: 'is-' + item.kind },
372
- h('span', { className: 'stock-consume-tag' }, consumeLabel(item.tag)),
373
- 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
+ ),
374
413
  ),
375
- ),
376
414
  ),
377
415
  row.consumedHidden > 0
378
416
  ? h(
379
- 'div',
417
+ 'details',
380
418
  { className: 'stock-consume-hidden' },
381
- 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
+ ),
382
448
  )
383
449
  : null,
384
450
  )
@@ -435,8 +501,50 @@ function App() {
435
501
  const [batchOpen, setBatchOpen] = React.useState(false)
436
502
  const [batchValues, setBatchValues] = React.useState({})
437
503
  const [showTop, setShowTop] = React.useState(false)
504
+ const [questTick, setQuestTick] = React.useState(0)
438
505
  const rootRef = React.useRef(null)
439
506
 
507
+ React.useEffect(() => {
508
+ const onResponse = (event) => {
509
+ const detail = event && event.detail
510
+ const path = detail && detail.path ? String(detail.path) : ''
511
+ if (!/questlist/.test(path)) return
512
+ try {
513
+ recordQuestStates(detail)
514
+ } catch (_error) {
515
+ // 记录失败不影响刷新
516
+ }
517
+ resetQuestNeeds()
518
+ resetQuestStatus()
519
+ setQuestTick((value) => value + 1)
520
+ window.setTimeout(() => {
521
+ resetQuestNeeds()
522
+ resetQuestStatus()
523
+ setQuestTick((value) => value + 1)
524
+ }, 1500)
525
+ }
526
+ window.addEventListener('game.response', onResponse)
527
+ return () => window.removeEventListener('game.response', onResponse)
528
+ }, [])
529
+
530
+ const refreshQuestData = React.useCallback(() => {
531
+ resetQuestNeeds()
532
+ resetQuestStatus()
533
+ setQuestTick((value) => value + 1)
534
+ }, [])
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
+
440
548
  const handleScroll = React.useCallback((event) => {
441
549
  setShowTop(event.currentTarget.scrollTop > 400)
442
550
  }, [])
@@ -488,10 +596,10 @@ function App() {
488
596
  saveKeeps(keeps)
489
597
  }, [keeps])
490
598
 
491
- const needs = React.useMemo(() => buildQuestNeeds(state), [state])
599
+ const needs = React.useMemo(() => buildQuestNeeds(state), [state, questTick])
492
600
  const rows = React.useMemo(
493
601
  () => buildInventory(state, { questNeeds: needs, keeps }),
494
- [state, needs, keeps],
602
+ [state, needs, keeps, questTick],
495
603
  )
496
604
  const summary = React.useMemo(() => summarize(rows), [rows])
497
605
 
@@ -606,6 +714,16 @@ function App() {
606
714
  h('option', { value: 'name' }, t('stock.sortName')),
607
715
  h('option', { value: 'category' }, t('stock.sortCategory')),
608
716
  ),
717
+ h(
718
+ 'button',
719
+ {
720
+ type: 'button',
721
+ className: 'stock-chip stock-refresh',
722
+ title: t('stock.refreshQuest'),
723
+ onClick: refreshQuestData,
724
+ },
725
+ '↻',
726
+ ),
609
727
  h(
610
728
  'div',
611
729
  { className: 'stock-views' },
@@ -729,7 +847,15 @@ function App() {
729
847
  : h(
730
848
  'div',
731
849
  { className: 'stock-list' },
732
- 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
+ ),
733
859
  ),
734
860
  h('p', { className: 'stock-warn' }, t('stock.warn')),
735
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 : '',
@@ -123,8 +123,6 @@ function clampCount(value) {
123
123
  }
124
124
 
125
125
  function readCachedQuests(now = Date.now()) {
126
- if (cacheState.quests && now - cacheState.at < CACHE_TTL) return cacheState.quests
127
-
128
126
  const file = questCachePath()
129
127
  try {
130
128
  const stat = fs.statSync(file)
@@ -185,10 +183,6 @@ function questPlannerStatePath() {
185
183
  * 里面的 observed 是它见过的任务(含标题/说明/状态),completed 是已完成集合。
186
184
  */
187
185
  function readQuestPlanner(now = Date.now()) {
188
- if (plannerCache.quests && now - plannerCache.at < CACHE_TTL) {
189
- return plannerCache.quests
190
- }
191
-
192
186
  const file = questPlannerStatePath()
193
187
  try {
194
188
  const stat = fs.statSync(file)
@@ -57,15 +57,107 @@ 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
  }
63
159
 
64
160
  function readPlannerState(now = Date.now()) {
65
- if (plannerCache.completed && now - plannerCache.at < CACHE_TTL) {
66
- return plannerCache
67
- }
68
-
69
161
  const file = questPlannerStatePath()
70
162
  try {
71
163
  const stat = fs.statSync(file)
@@ -189,8 +281,10 @@ function expandCompleted(completed, available) {
189
281
  }
190
282
  }
191
283
 
192
- // 现在还能接的任务,一定没完成;数据互相矛盾时以「可接」为准
193
- for (const no of available) result.delete(no)
284
+ // 还能接的任务不算完成;但如果它同时出现在「确定完成」里,以确定完成为准
285
+ for (const no of available) {
286
+ if (!completed.has(no)) result.delete(no)
287
+ }
194
288
  return result
195
289
  }
196
290
 
@@ -201,6 +295,8 @@ function getCompletedQuestNos(state) {
201
295
  const completed = new Set([
202
296
  ...fromState.completed,
203
297
  ...planner.completed,
298
+ ...getManualCompleted(),
299
+ ...getAutoCompleted(),
204
300
  ])
205
301
  const available = new Set([
206
302
  ...fromState.available,
@@ -217,8 +313,13 @@ function resetCaches() {
217
313
 
218
314
  module.exports = {
219
315
  expandCompleted,
316
+ getAutoCompleted,
220
317
  getCompletedQuestNos,
318
+ getManualCompleted,
319
+ markManual,
221
320
  readCachedAvailable,
222
321
  readPlannerState,
322
+ recordQuestStates,
223
323
  resetCaches,
324
+ unmarkManual,
224
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.16.2",
3
+ "version": "1.18.0",
4
4
  "description": "库存清理大师:对照改修攻略与保留上限,找出囤积过量、可以清理的装备,并标出不能拆的改修材料。",
5
5
  "main": "index.js",
6
6
  "files": [