@objectivex666/dsh-settings-search 1.2.0 → 1.4.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/README.en.md CHANGED
@@ -16,6 +16,7 @@
16
16
  - 📂 **In-page option search (v1.2.0)** — search beyond pages and reach the specific options inside them: Plugins-page tabs (`settings.plugins.tab`), Web UI plugin cards (`web-ui.plugin.item`), general rows, and more, each shown with a "page › option" breadcrumb.
17
17
  - 🧭 **Click-to-jump** — selecting a result clicks the matching left-nav entry to open its section; for tab options it also opens the tab and flashes the target row. A manual-path hint appears if automatic navigation is not possible.
18
18
  - 🌱 **Progressive indexing** — self-drawing settings rows are harvested from the rendered DOM (via their `[data-slot]` anchors) the first time their page is visited, then stay searchable.
19
+ - 🛠 **Built-in settings page (v1.4.0)** — a "Settings Search" page in the left navigation shows the current version and checks npm for a newer release on demand; when one exists it hands you a copy-ready `dsh plugin update` command.
19
20
  - 🌐 **i18n** — built-in Chinese/English dictionaries that follow the DSH localization system and are easy to extend.
20
21
  - 🎨 **Theme-aware** — styled entirely with CSS variables, automatically following the DSH theme (light/dark).
21
22
  - ⚡ **Reactive** — subscribes to settings entries being added, removed, or changed, and refreshes results automatically.
package/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  - 📂 **页内选项搜索(v1.2.0)**:不只搜页面,还能直达每个设置页内部的具体选项——插件页的标签页(`settings.plugins.tab`)、Web UI 插件的卡片(`web-ui.plugin.item`)、通用设置行等,结果以「页面 › 选项」面包屑展示。
17
17
  - 🧭 **点击即达**:选中结果后自动点击对应的左侧导航项打开所在分区;若是页内标签页会继续点开对应标签,并高亮闪烁目标选项行。无法自动跳转时显示手动路径提示。
18
18
  - 🌱 **渐进索引**:自绘界面的设置行会在其所在页面首次被访问时从渲染 DOM 中收割标题并加入索引(此后始终可搜)。
19
+ - 🛠 **自带设置页(v1.4.0)**:左侧导航新增「设置搜索」页——展示当前版本,一键检查 npm 注册表上的最新版本;有更新时直接给出复制就用的 `dsh plugin update` 命令。
19
20
  - 🌐 **多语言支持**:内置中英文国际化,可跟随 DSH 本地化系统扩展。
20
21
  - 🎨 **主题适配**:完全使用 CSS 变量,自动跟随 DSH 主题(亮/暗)。
21
22
  - ⚡ **动态响应**:实时订阅设置项的新增/删除/变更,搜索结果自动刷新。
package/lib/client.js CHANGED
@@ -29,6 +29,9 @@ window.__ModuleLoader__.load({
29
29
  const React = require('react')
30
30
 
31
31
  const NS = 'settings-search-v5'
32
+ /** Single source of truth is package.json — scripts/check.mjs asserts this matches. */
33
+ const PKG_VERSION = '1.4.0'
34
+ const NPM_NAME = '@objectivex666/dsh-settings-search'
32
35
  const zh = {
33
36
  title: '设置',
34
37
  placeholder: '搜索设置…',
@@ -38,6 +41,17 @@ window.__ModuleLoader__.load({
38
41
  page: '页面',
39
42
  option: '选项',
40
43
  jumpFail: '未能自动跳转,请手动打开:{path}',
44
+ updn: '设置搜索',
45
+ updDesc: '设置面板顶部搜索框由本插件提供。在此检查插件的版本与更新。',
46
+ updCurrent: '当前版本',
47
+ updLatestKnown: 'npm 最新版',
48
+ updCheck: '检查更新',
49
+ updChecking: '正在检查…',
50
+ updNewest: '已是最新版本 ✓',
51
+ updFound: '发现新版本 {ver},可执行以下命令更新后重启 DSH:',
52
+ updErr: '检查失败(网络或注册表不可达),请稍后重试。',
53
+ updCopy: '复制命令',
54
+ updCopied: '已复制 ✓',
41
55
  }
42
56
  const en = {
43
57
  title: 'Settings',
@@ -48,6 +62,17 @@ window.__ModuleLoader__.load({
48
62
  page: 'Page',
49
63
  option: 'Option',
50
64
  jumpFail: 'Could not navigate automatically — open it here: {path}',
65
+ updn: 'Settings Search',
66
+ updDesc: "The search box atop the settings panel comes from this plugin. Check its version and updates here.",
67
+ updCurrent: 'Current version',
68
+ updLatestKnown: 'npm latest',
69
+ updCheck: 'Check for updates',
70
+ updChecking: 'Checking…',
71
+ updNewest: 'You are on the latest version ✓',
72
+ updFound: 'Version {ver} available. Run the command below, then restart DSH:',
73
+ updErr: 'Check failed (network or registry unreachable). Try again later.',
74
+ updCopy: 'Copy command',
75
+ updCopied: 'Copied ✓',
51
76
  }
52
77
 
53
78
  /** Inner-row slots that represent specific options inside one page. */
@@ -80,6 +105,19 @@ window.__ModuleLoader__.load({
80
105
  .sss-guide { padding: 7px 10px; border-radius: 8px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; }
81
106
  @keyframes sssFlashBg { 0% { background-color: var(--dsw-alias-interactive-bg-hover); } 100% { background-color: transparent; } }
82
107
  .sss-flash { animation: sssFlashBg 1.8s ease-out; border-radius: 8px; }
108
+ .sss-upd { display: flex; flex-direction: column; gap: 12px; padding: 16px; max-width: 560px; }
109
+ .sss-upd-title { font-size: 15px; font-weight: 600; line-height: 22px; color: var(--dsw-alias-label-primary); margin: 0; }
110
+ .sss-upd-desc { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
111
+ .sss-upd-card { display: flex; flex-direction: column; gap: 10px; padding: 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); }
112
+ .sss-upd-row { display: flex; align-items: center; gap: 8px; font-size: 13px; line-height: 20px; }
113
+ .sss-upd-key { flex-shrink: 0; width: 88px; color: var(--dsw-alias-label-secondary); }
114
+ .sss-upd-val { color: var(--dsw-alias-label-primary); }
115
+ .sss-upd-badge { flex-shrink: 0; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 18px; }
116
+ .sss-btn { height: 28px; padding: 0 12px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-size: 12px; cursor: pointer; }
117
+ .sss-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
118
+ .sss-btn:disabled { opacity: .55; cursor: default; }
119
+ .sss-cmd { user-select: all; font-family: ui-monospace, Consolas, 'Courier New', monospace; font-size: 11.5px; line-height: 20px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; padding: 4px 8px; word-break: break-all; }
120
+ .sss-upd-note { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
83
121
  `
84
122
  let stylesInstalled = false
85
123
  function installStyles() {
@@ -135,26 +173,50 @@ window.__ModuleLoader__.load({
135
173
  }
136
174
 
137
175
  /**
138
- * Read visible titles for self-drawing occupants out of the live DOM:
139
- * the shell marks every rendered inner-slot occupant with
140
- * `[data-slot="<name>"]`; when DOM order matches entry order we can
141
- * pair each rendered card with its registration id.
176
+ * Read visible titles for self-drawing occupants out of the live DOM.
177
+ * Two shell shapes exist for `[data-slot="<name>"]`:
178
+ * - per-occupant anchors: one element each, count === regs → pair ids;
179
+ * - a single WRAPPER around all rows (e.g. settings.general.item's
180
+ * stacked section): harvest each direct child as one row, keyed by
181
+ * render order ("#i") since id pairing is impossible.
142
182
  */
143
183
  const runHarvest = () => {
144
184
  let grew = false
145
185
  if (typeof document === 'undefined') return false
186
+ let map
146
187
  for (const cfg of DEEP_SLOTS) {
147
- let nodes = []
148
- try { nodes = [...document.querySelectorAll(`[data-slot="${cfg.slot}"]`)] } catch { continue }
188
+ let containers = []
189
+ try { containers = [...document.querySelectorAll(`[data-slot="${cfg.slot}"]`)] } catch { continue }
149
190
  const regs = sortEntries(entriesOf(cfg.slot))
150
- if (nodes.length === 0 || nodes.length !== regs.length) continue
151
- let map = harvest.get(cfg.slot)
191
+ if (regs.length === 0) continue
192
+ let nodes = []
193
+ if (containers.length === 1 && containers[0].childElementCount > 0) {
194
+ nodes = [...containers[0].children]
195
+ } else {
196
+ nodes = containers.filter((n) => n.childElementCount >= 0)
197
+ }
198
+ if (nodes.length === 0) continue
199
+ map = harvest.get(cfg.slot)
152
200
  if (map === undefined) { map = new Map(); harvest.set(cfg.slot, map) }
201
+ if (nodes.length !== regs.length) {
202
+ // wrapper shape: index-keyed loose harvest of every rendered row
203
+ nodes.forEach((node, i) => {
204
+ const key = '#' + i
205
+ if (map.has(key)) return
206
+ const title = titleFromNode(node)
207
+ if (title === '') return
208
+ const text = (node.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 160)
209
+ map.set(key, { title, idx: i, text }); grew = true
210
+ })
211
+ continue
212
+ }
153
213
  regs.forEach((entry, i) => {
154
214
  const id = String(entry.options?.id ?? i)
155
215
  if (map.has(id)) return
156
216
  const title = titleFromNode(nodes[i])
157
- if (title !== '') { map.set(id, title); grew = true }
217
+ if (title === '') return
218
+ const text = (nodes[i].textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 160)
219
+ map.set(id, { title, idx: i, text }); grew = true
158
220
  })
159
221
  }
160
222
  if (grew) harvestSeq += 1
@@ -194,24 +256,30 @@ window.__ModuleLoader__.load({
194
256
  const out = []
195
257
  for (const cfg of DEEP_SLOTS) {
196
258
  const mapped = harvest.get(cfg.slot)
197
- for (const entry of sortEntries(entriesOf(cfg.slot))) {
259
+ sortEntries(entriesOf(cfg.slot)).forEach((entry, i) => {
198
260
  const id = String(entry.options?.id ?? '')
199
- if (id === '') continue
200
- const label = staticLabel(entry, cfg)
201
- || (mapped ? mapped.get(id) ?? '' : '')
202
- if (label === '') continue // unmapped, unharvested — invisible until it renders once
203
- if (out.some((e) => e.key === cfg.slot + '#' + id)) continue
261
+ if (id === '') return
262
+ // static label first, then id-paired harvest, then loose index-keyed harvest
263
+ const hitId = mapped?.get(id)
264
+ const hitIdx = mapped?.get('#' + i)
265
+ const hitObj = typeof hitId === 'object' ? hitId : (typeof hitIdx === 'object' ? hitIdx : undefined)
266
+ const label = staticLabel(entry, cfg) || hitObj?.title || ''
267
+ if (label === '') return // unmapped, unharvested — invisible until it renders once
268
+ const key = cfg.slot + '#' + id
269
+ if (out.some((e) => e.key === key)) return
204
270
  out.push({
205
271
  kind: 'option',
206
- key: cfg.slot + '#' + id,
272
+ key,
207
273
  slot: cfg.slot,
208
274
  id,
209
275
  order: entry.options?.order ?? 0,
210
276
  label,
277
+ hay: label + ' ' + (hitObj?.text ?? ''),
278
+ rowIndex: hitObj?.idx ?? i,
211
279
  parentId: cfg.parent,
212
280
  parentLabel: parentLabel(cfg.parent),
213
281
  })
214
- }
282
+ })
215
283
  }
216
284
  return out.sort((a, b) => a.parentLabel !== b.parentLabel
217
285
  ? a.parentLabel.localeCompare(b.parentLabel)
@@ -232,8 +300,10 @@ window.__ModuleLoader__.load({
232
300
  const want = String(wantText ?? '').trim()
233
301
  if (want === '') return false
234
302
  const buttons = panelButtons()
235
- const hit = buttons.find((b) => (b.textContent ?? '').trim() === want)
236
- ?? buttons.find((b) => (b.textContent ?? '').trim().startsWith(want))
303
+ const textOf = (b) => (b.textContent ?? '').trim()
304
+ const hit = buttons.find((b) => textOf(b) === want)
305
+ ?? buttons.find((b) => textOf(b).startsWith(want))
306
+ ?? buttons.find((b) => want.length >= 2 && textOf(b) !== '' && (textOf(b).includes(want) || want.includes(textOf(b))))
237
307
  if (!hit) return false
238
308
  hit.click()
239
309
  return true
@@ -261,8 +331,23 @@ window.__ModuleLoader__.load({
261
331
  /** Highlight the rendered row card for one indexed option. */
262
332
  const flashOptionRow = (item) => {
263
333
  try {
264
- const nodes = [...document.querySelectorAll(`[data-slot="${item.slot}"]`)]
265
- const target = nodes.find((n) => (n.textContent ?? '').includes(item.label)) ?? nodes[0]
334
+ let target = null
335
+ if (typeof item.rowIndex === 'number') {
336
+ // wrapper shape: pick the exact rendered row by index
337
+ const container = document.querySelector(`[data-slot="${item.slot}"]`)
338
+ target = container?.children?.[item.rowIndex] ?? null
339
+ if (target && !(target.textContent ?? '').includes(item.label)) {
340
+ // index drift — fall through to textual match below
341
+ target = null
342
+ }
343
+ }
344
+ if (!target) {
345
+ const nodes = [...document.querySelectorAll(`[data-slot="${item.slot}"]`)]
346
+ const rows = nodes.length === 1 && nodes[0].childElementCount > 0
347
+ ? [...nodes[0].children]
348
+ : nodes
349
+ target = rows.find((n) => (n.textContent ?? '').includes(item.label)) ?? null
350
+ }
266
351
  if (!target) return
267
352
  target.scrollIntoView({ block: 'nearest' })
268
353
  target.classList.remove('sss-flash')
@@ -342,7 +427,7 @@ window.__ModuleLoader__.load({
342
427
  ? sections.filter((s) => matches(`${s.label} ${s.id}`.toLowerCase()))
343
428
  : []
344
429
  const shownOptions = q !== ''
345
- ? deeps.filter((o) => matches(`${o.label} ${o.parentLabel} ${o.id} ${o.slot}`.toLowerCase()))
430
+ ? deeps.filter((o) => matches(`${o.hay} ${o.parentLabel} ${o.id} ${o.slot}`.toLowerCase()))
346
431
  : []
347
432
  const none = q !== '' && shownPages.length === 0 && shownOptions.length === 0
348
433
 
@@ -416,6 +501,94 @@ window.__ModuleLoader__.load({
416
501
  ])
417
502
  }
418
503
 
504
+ const compareSemver = (a, b) => {
505
+ const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0)
506
+ const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0)
507
+ for (let i = 0; i < 3; i += 1) {
508
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0)
509
+ }
510
+ return 0
511
+ }
512
+
513
+ function UpdatePanel() {
514
+ const [state, setState] = React.useState('idle') // idle | loading | newest | outdated | error
515
+ const [latest, setLatest] = React.useState('')
516
+ const [copied, setCopied] = React.useState(false)
517
+
518
+ React.useEffect(() => {
519
+ if (!copied) return
520
+ const off = setTimeout(() => setCopied(false), 1600)
521
+ return () => clearTimeout(off)
522
+ }, [copied])
523
+
524
+ const check = async () => {
525
+ setState('loading')
526
+ setCopied(false)
527
+ try {
528
+ const res = await fetch(`https://registry.npmjs.org/${NPM_NAME}/latest?t=${Date.now()}`)
529
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
530
+ const data = await res.json()
531
+ const ver = typeof data?.version === 'string' ? data.version : ''
532
+ if (ver === '') throw new Error('no version in registry response')
533
+ setLatest(ver)
534
+ setState(compareSemver(ver, PKG_VERSION) > 0 ? 'outdated' : 'newest')
535
+ } catch {
536
+ setState('error')
537
+ }
538
+ }
539
+
540
+ const copyCmd = async () => {
541
+ const cmd = `dsh plugin update ${NPM_NAME}`
542
+ try { await navigator.clipboard.writeText(cmd); setCopied(true) } catch { /* user can copy manually */ }
543
+ }
544
+
545
+ const statusNode = {
546
+ idle: React.createElement('span', { className: 'sss-upd-val' }, '—'),
547
+ loading: React.createElement('span', { className: 'sss-upd-val' }, t('updChecking')),
548
+ newest: React.createElement('span', { className: 'sss-upd-val' }, t('updNewest')),
549
+ error: React.createElement('span', { className: 'sss-upd-val' }, t('updErr')),
550
+ outdated: latest === '' ? null : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } }, [
551
+ React.createElement('p', { className: 'sss-upd-note' }, t('updFound').replace('{ver}', `v${latest}`)),
552
+ React.createElement('code', { className: 'sss-cmd' }, `dsh plugin update ${NPM_NAME}`),
553
+ React.createElement('div', null,
554
+ React.createElement('button', { className: 'sss-btn', type: 'button', onClick: () => { void copyCmd() } },
555
+ copied ? t('updCopied') : t('updCopy'))),
556
+ ]),
557
+ }
558
+
559
+ return React.createElement('div', { className: 'sss-upd' }, [
560
+ React.createElement('h3', { className: 'sss-upd-title' }, `${t('updn')} v${PKG_VERSION}`),
561
+ React.createElement('p', { className: 'sss-upd-desc' }, t('updDesc')),
562
+ React.createElement('div', { className: 'sss-upd-card' }, [
563
+ React.createElement('div', { className: 'sss-upd-row' }, [
564
+ React.createElement('span', { className: 'sss-upd-key' }, t('updCurrent')),
565
+ React.createElement('code', { className: 'sss-cmd' }, PKG_VERSION),
566
+ state === 'newest' ? React.createElement('span', { className: 'sss-upd-badge' }, 'npm ✓') : null,
567
+ ]),
568
+ state === 'outdated' || state === 'loading'
569
+ ? React.createElement('div', { className: 'sss-upd-row' }, [
570
+ React.createElement('span', { className: 'sss-upd-key' }, t('updLatestKnown')),
571
+ state === 'loading' ? React.createElement('span', { className: 'sss-upd-val' }, '…') : React.createElement('code', { className: 'sss-cmd' }, latest),
572
+ ])
573
+ : null,
574
+ state === 'outdated' || state === 'loading' || state === 'error' || state === 'idle' || state === 'newest'
575
+ ? (state === 'outdated' ? statusNode.outdated : statusNode[state])
576
+ : null,
577
+ React.createElement('div', { className: 'sss-upd-row' },
578
+ React.createElement('button', { className: 'sss-btn', type: 'button', disabled: state === 'loading', onClick: () => { void check() } },
579
+ state === 'loading' ? t('updChecking') : t('updCheck'))),
580
+ ]),
581
+ ])
582
+ }
583
+
584
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
585
+ name: 'settings.section',
586
+ id: 'settings-search',
587
+ order: 1600,
588
+ locale: NS,
589
+ label: () => t('updn'),
590
+ }, UpdatePanel))
591
+
419
592
  ctx.slots.inject('settings.header', () => ctx.slots.register({
420
593
  name: 'settings.header',
421
594
  priority: -1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectivex666/dsh-settings-search",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "DSH settings panel search plugin – instantly filter setting sections and general items with navigation guidance. Installable via `dsh plugin add`.",
5
5
  "keywords": [
6
6
  "dsh",
package/scripts/check.mjs CHANGED
@@ -54,6 +54,12 @@ check(typeof client.apply === 'function', 'client half must export apply(ctx)')
54
54
  check(Array.isArray(client.inject) && client.inject.includes('slots') && client.inject.includes('locale'),
55
55
  'client half inject must include "slots" and "locale"')
56
56
 
57
+ // 4. embedded version constant matches the manifest (update panel displays it)
58
+ const versionMatch = /const PKG_VERSION = '([^']+)'/.exec(source)
59
+ check(versionMatch !== null, 'lib/client.js must define const PKG_VERSION')
60
+ check(versionMatch?.[1] === pkg.version,
61
+ `PKG_VERSION (${versionMatch?.[1]}) must equal package.json version (${pkg.version})`)
62
+
57
63
  if (failed > 0) {
58
64
  console.error(`${failed} check(s) failed`)
59
65
  process.exit(1)