dsh-manage-session 0.4.8 → 0.5.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.
- package/CHANGELOG.md +36 -0
- package/README.en.md +4 -0
- package/README.md +4 -0
- package/cordis.patch.yml +6 -1
- package/dsh.plugin.json +22 -22
- package/lib/client.js +3848 -202
- package/lib/index.d.ts +759 -29
- package/lib/index.js +2604 -93
- package/locale/de.json +6 -0
- package/locale/en.json +6 -0
- package/locale/es.json +6 -0
- package/locale/fr.json +6 -0
- package/locale/it.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/ko.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -32
- package/src/client/bridge-scope.ts +213 -0
- package/src/client/index.ts +154 -26
- package/src/client/locales.ts +857 -223
- package/src/client/panel.tsx +139 -118
- package/src/client/search/card.tsx +563 -0
- package/src/client/search/host-api.ts +208 -0
- package/src/client/search/locales.ts +780 -0
- package/src/client/search/manage.tsx +400 -0
- package/src/client/search/panel.tsx +815 -0
- package/src/client/search/platform.ts +136 -0
- package/src/config.ts +54 -5
- package/src/host/history/archive-source.ts +48 -0
- package/src/host/history/archive.ts +56 -1
- package/src/host/history/favorites.ts +71 -0
- package/src/host/index/archive-source.ts +90 -0
- package/src/host/index/engine.ts +571 -0
- package/src/host/index/extract.ts +272 -0
- package/src/host/index/peers.ts +62 -0
- package/src/host/index/rebuild.ts +368 -0
- package/src/host/index/schema.ts +136 -0
- package/src/host/index/snapshot.ts +135 -0
- package/src/host/index/sync.ts +286 -0
- package/src/host/registry/archive-registry.ts +317 -0
- package/src/host/settings-bridge.ts +237 -0
- package/src/index.ts +1421 -532
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-search settings card(0.1.7 起不再挂载:设置表单由 host 侧 .volatile() 字段自动生成)。
|
|
3
|
+
* plugin, following the dsh-thinking-levels card pattern.
|
|
4
|
+
*
|
|
5
|
+
* The card binds the `switch-search` settings namespace through the
|
|
6
|
+
* `settingsScope` cordis service and renders its fields as one editable card:
|
|
7
|
+
* the enable switch, the default panel mode, the independent-index sync
|
|
8
|
+
* knobs, and the index-lifecycle block (status, the non-destructive 整理
|
|
9
|
+
* button, and the JSON snapshot export/import seam). Every change commits
|
|
10
|
+
* immediately through the scope (no staged form).
|
|
11
|
+
*
|
|
12
|
+
* Kept dependency-free beyond react: the scope is subscribed with
|
|
13
|
+
* `useSyncExternalStore`, and the controls are plain HTML reusing the
|
|
14
|
+
* stylesheet the client half injects.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useState, useSyncExternalStore, type JSX } from 'react'
|
|
17
|
+
import { createElement } from 'react'
|
|
18
|
+
import type { SwitchSearchConfig } from '../../config.ts'
|
|
19
|
+
import {
|
|
20
|
+
callHostAny,
|
|
21
|
+
downloadSnapshot,
|
|
22
|
+
type HostIndexStatus,
|
|
23
|
+
} from './host-api.ts'
|
|
24
|
+
import { translate, type LocaleKey } from './locales.ts'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Structural mirror of the settings scope (the plugin must not value- or
|
|
28
|
+
* type-import a single release of the official settings packages).
|
|
29
|
+
*/
|
|
30
|
+
export interface SwitchCardScope {
|
|
31
|
+
getSnapshot(): {
|
|
32
|
+
status: 'loading' | 'ready' | 'unavailable'
|
|
33
|
+
value: SwitchSearchConfig | undefined
|
|
34
|
+
revision: number | undefined
|
|
35
|
+
writable: boolean
|
|
36
|
+
}
|
|
37
|
+
subscribe(listener: () => void): () => void
|
|
38
|
+
set(field: string, value: unknown): Promise<void>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Injected face the settings slot factory hands the card. */
|
|
42
|
+
export interface SearchSettingsCardInjected {
|
|
43
|
+
scope: SwitchCardScope
|
|
44
|
+
/** Optional host dictionary lookup (present when the locale service exists). */
|
|
45
|
+
t?: (key: LocaleKey, params?: Record<string, unknown>) => string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Full card props. */
|
|
49
|
+
export type SearchSettingsCardProps = SearchSettingsCardInjected
|
|
50
|
+
|
|
51
|
+
/** Row shared by every field of the card. */
|
|
52
|
+
function Row(props: { title: string; desc?: string; control: JSX.Element }): JSX.Element {
|
|
53
|
+
return createElement('div', { className: 'dsws_setRow' }, [
|
|
54
|
+
createElement('div', { key: 'text', className: 'dsws_setText' }, [
|
|
55
|
+
createElement('span', { key: 't', className: 'dsws_setTitle' }, props.title),
|
|
56
|
+
props.desc !== undefined
|
|
57
|
+
&& createElement('span', { key: 'd', className: 'dsws_setDesc' }, props.desc),
|
|
58
|
+
]),
|
|
59
|
+
createElement('div', { key: 'ctl' }, props.control),
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A boolean switch editing one namespace field.
|
|
65
|
+
*
|
|
66
|
+
* Drawn entirely with inline styles (thinking-levels discipline): the card
|
|
67
|
+
* must not depend on the injected stylesheet — scoped or late-loaded settings
|
|
68
|
+
* pages left the class-based switch rendering as a bare checkbox dot.
|
|
69
|
+
*/
|
|
70
|
+
function Toggle(props: { checked: boolean; writable: boolean; onChange: (checked: boolean) => void }): JSX.Element {
|
|
71
|
+
const checked = props.checked
|
|
72
|
+
return createElement('label', {
|
|
73
|
+
style: {
|
|
74
|
+
position: 'relative',
|
|
75
|
+
width: '40px',
|
|
76
|
+
height: '22px',
|
|
77
|
+
flex: 'none',
|
|
78
|
+
display: 'inline-block',
|
|
79
|
+
cursor: props.writable ? 'pointer' : 'not-allowed',
|
|
80
|
+
},
|
|
81
|
+
}, [
|
|
82
|
+
createElement('input', {
|
|
83
|
+
key: 'input',
|
|
84
|
+
type: 'checkbox',
|
|
85
|
+
checked,
|
|
86
|
+
disabled: !props.writable,
|
|
87
|
+
onChange: (e: { target: { checked: boolean } }) => props.onChange(e.target.checked),
|
|
88
|
+
style: {
|
|
89
|
+
position: 'absolute',
|
|
90
|
+
inset: 0,
|
|
91
|
+
width: '100%',
|
|
92
|
+
height: '100%',
|
|
93
|
+
opacity: 0,
|
|
94
|
+
margin: 0,
|
|
95
|
+
cursor: props.writable ? 'pointer' : 'not-allowed',
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
createElement('span', {
|
|
99
|
+
key: 'track',
|
|
100
|
+
style: {
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
inset: 0,
|
|
103
|
+
background: checked
|
|
104
|
+
? 'var(--dsw-alias-state-business-primary, #4c6ef5)'
|
|
105
|
+
: 'var(--dsw-alias-bg-module-platform, rgba(127,127,127,0.25))',
|
|
106
|
+
border: `1px solid ${checked
|
|
107
|
+
? 'var(--dsw-alias-state-business-primary, #4c6ef5)'
|
|
108
|
+
: 'var(--dsw-alias-border-l2, rgba(127,127,127,0.35))'}`,
|
|
109
|
+
borderRadius: '11px',
|
|
110
|
+
transition: 'background .15s ease, border-color .15s ease',
|
|
111
|
+
pointerEvents: 'none',
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
createElement('span', {
|
|
115
|
+
key: 'thumb',
|
|
116
|
+
style: {
|
|
117
|
+
position: 'absolute',
|
|
118
|
+
top: '2px',
|
|
119
|
+
left: '2px',
|
|
120
|
+
width: '16px',
|
|
121
|
+
height: '16px',
|
|
122
|
+
background: '#fff',
|
|
123
|
+
borderRadius: '50%',
|
|
124
|
+
boxShadow: '0 1px 2px rgba(0,0,0,.2)',
|
|
125
|
+
transition: 'transform .15s ease',
|
|
126
|
+
transform: checked ? 'translateX(18px)' : 'none',
|
|
127
|
+
pointerEvents: 'none',
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A status pill in the official ConnectionIndicator visual language: rounded
|
|
135
|
+
* chip, semantic state tokens, animated dots while syncing.
|
|
136
|
+
*/
|
|
137
|
+
function StatusPill(props: { state: 'ready' | 'syncing' | 'error' | 'neutral'; label: string; icon: string; dots?: boolean }): JSX.Element {
|
|
138
|
+
const stateClass = props.state === 'ready' ? 'dsws_pillReady'
|
|
139
|
+
: props.state === 'syncing' ? 'dsws_pillWarn'
|
|
140
|
+
: props.state === 'error' ? 'dsws_pillError'
|
|
141
|
+
: 'dsws_pillNeutral'
|
|
142
|
+
return createElement('span', { className: `dsws_pill ${stateClass}` }, [
|
|
143
|
+
createElement('span', { key: 'icon', className: 'dsws_pillIcon', 'aria-hidden': true }, props.icon),
|
|
144
|
+
createElement('span', { key: 'label', className: 'dsws_pillLabel' }, [
|
|
145
|
+
createElement('span', { key: 'text' }, props.label),
|
|
146
|
+
props.dots === true && createElement('span', {
|
|
147
|
+
key: 'dots',
|
|
148
|
+
className: 'dsws_pillDots',
|
|
149
|
+
'aria-hidden': true,
|
|
150
|
+
}, createElement('span', {}, '.'), createElement('span', {}, '.'), createElement('span', {}, '.')),
|
|
151
|
+
]),
|
|
152
|
+
])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The independent-index lifecycle block (status + 整理 + snapshot seam). */
|
|
156
|
+
function IndexBlock(props: { t?: SearchSettingsCardProps['t'] }): JSX.Element {
|
|
157
|
+
const [status, setStatus] = useState<HostIndexStatus | null>(null)
|
|
158
|
+
const [fetchError, setFetchError] = useState<string | null>(null)
|
|
159
|
+
const [attempt, setAttempt] = useState(0)
|
|
160
|
+
const [note, setNote] = useState<string | null>(null)
|
|
161
|
+
const [busy, setBusy] = useState(false)
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
let cancelled = false
|
|
165
|
+
let timer: number | undefined
|
|
166
|
+
const refresh = (): void => {
|
|
167
|
+
void callHostAny<HostIndexStatus>('index-status', {}).then((res) => {
|
|
168
|
+
if (cancelled) return
|
|
169
|
+
if (res.ok) {
|
|
170
|
+
setStatus(res as HostIndexStatus)
|
|
171
|
+
setFetchError(null)
|
|
172
|
+
} else {
|
|
173
|
+
// The host half may be older than the client bundle (browser
|
|
174
|
+
// refresh does not reload the host) — surface it, never spin.
|
|
175
|
+
setStatus(null)
|
|
176
|
+
setFetchError(res.error ?? 'index-status 请求失败')
|
|
177
|
+
}
|
|
178
|
+
const rebuilding = res.rebuild?.state === 'building' || res.rebuild?.state === 'swapping'
|
|
179
|
+
if (rebuilding) timer = window.setTimeout(refresh, 2000)
|
|
180
|
+
else setBusy(false)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
refresh()
|
|
184
|
+
return () => {
|
|
185
|
+
cancelled = true
|
|
186
|
+
if (timer !== undefined) window.clearTimeout(timer)
|
|
187
|
+
}
|
|
188
|
+
}, [attempt])
|
|
189
|
+
|
|
190
|
+
const t = props.t
|
|
191
|
+
const rebuilding = status?.rebuild?.state === 'building' || status?.rebuild?.state === 'swapping'
|
|
192
|
+
const rebuildError = status?.rebuild?.state === 'error' ? status.rebuild.error : undefined
|
|
193
|
+
const syncFailures = status?.sync?.failures?.length ?? 0
|
|
194
|
+
const blocking = busy || rebuilding
|
|
195
|
+
|
|
196
|
+
const onRebuild = (): void => {
|
|
197
|
+
setBusy(true)
|
|
198
|
+
setNote(null)
|
|
199
|
+
void callHostAny<HostIndexStatus>('index-rebuild', {}).then((res) => {
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
setNote(translate(t, 'card.action.failed', { error: res.error ?? '?' }))
|
|
202
|
+
setBusy(false)
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const onExport = (): void => {
|
|
208
|
+
setBusy(true)
|
|
209
|
+
setNote(null)
|
|
210
|
+
void downloadSnapshot()
|
|
211
|
+
.then(() => setNote(translate(t, 'card.index.exported')))
|
|
212
|
+
.catch((err: unknown) => setNote(translate(t, 'card.action.failed', { error: String(err instanceof Error ? err.message : err) })))
|
|
213
|
+
.finally(() => setBusy(false))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const onImportFile = (file: File): void => {
|
|
217
|
+
setBusy(true)
|
|
218
|
+
setNote(null)
|
|
219
|
+
void file.text().then((text) => callHostAny<HostIndexStatus>('index-import', text, 30_000)).then((res) => {
|
|
220
|
+
if (res.ok) setNote(translate(t, 'card.index.import.started'))
|
|
221
|
+
else setNote(res.error ?? translate(t, 'card.index.importParse'))
|
|
222
|
+
}).catch((err: unknown) => setNote(translate(t, 'card.action.failed', { error: String(err instanceof Error ? err.message : err) })))
|
|
223
|
+
.finally(() => setBusy(false))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const pillState: 'ready' | 'syncing' | 'error' | 'neutral' = rebuilding
|
|
227
|
+
? 'syncing'
|
|
228
|
+
: status?.rebuild?.state === 'error' || fetchError !== null
|
|
229
|
+
? 'error'
|
|
230
|
+
: status === null || status.available !== true
|
|
231
|
+
? 'neutral'
|
|
232
|
+
: 'ready'
|
|
233
|
+
const pillLabel = fetchError !== null
|
|
234
|
+
? '状态读取失败'
|
|
235
|
+
: rebuilding
|
|
236
|
+
? translate(t, 'card.index.rebuilding', { done: status?.rebuild?.done ?? 0, total: status?.rebuild?.total || '?' })
|
|
237
|
+
: status === null
|
|
238
|
+
? translate(t, 'card.index.reading')
|
|
239
|
+
: status.available === true
|
|
240
|
+
? translate(t, 'card.index.desc', { indexed: status.sync?.indexed ?? '?' })
|
|
241
|
+
: translate(t, 'card.index.empty')
|
|
242
|
+
|
|
243
|
+
const statusLine = fetchError !== null
|
|
244
|
+
? `索引状态读取失败:${fetchError}。请完全重启 dsh web(浏览器刷新不会重载 Host 半)后重试。`
|
|
245
|
+
: status === null
|
|
246
|
+
? translate(t, 'card.index.reading')
|
|
247
|
+
: rebuilding
|
|
248
|
+
? translate(t, 'card.index.rebuilding', { done: status.rebuild?.done ?? 0, total: status.rebuild?.total || '?' })
|
|
249
|
+
: status.available === true
|
|
250
|
+
? `${translate(t, 'card.index.desc', { indexed: status.sync?.indexed ?? '?' })}${(status.archives?.length ?? 0) > 0 ? translate(t, 'card.index.archives', { archives: status.archives?.length }) : ''}`
|
|
251
|
+
: translate(t, 'card.index.empty')
|
|
252
|
+
|
|
253
|
+
// Rebuild progress: a labeled bar with percentage, indeterminate sweep when
|
|
254
|
+
// the total is not known yet. Rendered prominently above the action row.
|
|
255
|
+
const rebuildDone = status?.rebuild?.done ?? 0
|
|
256
|
+
const rebuildTotal = status?.rebuild?.total ?? 0
|
|
257
|
+
const progressPct = rebuilding && rebuildTotal > 0
|
|
258
|
+
? Math.min(100, Math.round((rebuildDone / rebuildTotal) * 100))
|
|
259
|
+
: undefined
|
|
260
|
+
const progressBlock = rebuilding
|
|
261
|
+
? createElement('div', {
|
|
262
|
+
key: 'progress',
|
|
263
|
+
style: { display: 'flex', flexDirection: 'column', gap: '4px', margin: '2px 0 6px', width: '100%' },
|
|
264
|
+
}, [
|
|
265
|
+
createElement('div', {
|
|
266
|
+
key: 'bar',
|
|
267
|
+
style: {
|
|
268
|
+
height: '6px',
|
|
269
|
+
borderRadius: '3px',
|
|
270
|
+
background: 'var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.2))',
|
|
271
|
+
overflow: 'hidden',
|
|
272
|
+
},
|
|
273
|
+
}, createElement('div', {
|
|
274
|
+
style: progressPct === undefined
|
|
275
|
+
? {
|
|
276
|
+
height: '100%',
|
|
277
|
+
width: '30%',
|
|
278
|
+
borderRadius: '3px',
|
|
279
|
+
background: 'var(--dsw-alias-state-business-primary, #4c6ef5)',
|
|
280
|
+
opacity: 0.6,
|
|
281
|
+
}
|
|
282
|
+
: {
|
|
283
|
+
height: '100%',
|
|
284
|
+
width: `${progressPct}%`,
|
|
285
|
+
borderRadius: '3px',
|
|
286
|
+
background: 'var(--dsw-alias-state-business-primary, #4c6ef5)',
|
|
287
|
+
transition: 'width .4s ease',
|
|
288
|
+
},
|
|
289
|
+
})),
|
|
290
|
+
createElement('span', {
|
|
291
|
+
style: {
|
|
292
|
+
color: 'var(--dsw-alias-state-business-primary, #4c6ef5)',
|
|
293
|
+
fontSize: '12px',
|
|
294
|
+
lineHeight: '18px',
|
|
295
|
+
fontWeight: 600,
|
|
296
|
+
fontVariantNumeric: 'tabular-nums',
|
|
297
|
+
},
|
|
298
|
+
}, progressPct === undefined
|
|
299
|
+
? translate(t, 'card.index.rebuilding', { done: rebuildDone, total: '?' })
|
|
300
|
+
: `${progressPct}% · ${translate(t, 'card.index.rebuilding', { done: rebuildDone, total: rebuildTotal })}`),
|
|
301
|
+
])
|
|
302
|
+
: null
|
|
303
|
+
|
|
304
|
+
return createElement('div', { className: 'dsws_setRow', style: rebuilding ? { flexDirection: 'column', alignItems: 'stretch' } : undefined }, [
|
|
305
|
+
createElement('div', { key: 'text', className: 'dsws_setText' }, [
|
|
306
|
+
createElement('span', { key: 't', className: 'dsws_setTitle' }, translate(t, 'card.index')),
|
|
307
|
+
createElement('span', { key: 'p', style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' } }, [
|
|
308
|
+
createElement(StatusPill, {
|
|
309
|
+
key: 'pill',
|
|
310
|
+
state: pillState,
|
|
311
|
+
label: pillLabel,
|
|
312
|
+
icon: pillState === 'ready' ? '✓' : pillState === 'syncing' || pillState === 'error' ? '!' : '·',
|
|
313
|
+
dots: pillState === 'syncing',
|
|
314
|
+
}),
|
|
315
|
+
(status?.archivedSessions ?? 0) > 0 && createElement('span', {
|
|
316
|
+
key: 'archived',
|
|
317
|
+
className: 'dsws_pill dsws_pillNeutral',
|
|
318
|
+
}, translate(t, 'panel.archived', { count: status?.archivedSessions ?? 0 })),
|
|
319
|
+
]),
|
|
320
|
+
createElement('span', { key: 'd', className: 'dsws_setDesc' }, statusLine),
|
|
321
|
+
// The archived count points at a capability this plugin does not own.
|
|
322
|
+
// Left bare it read as a broken feature: "there are 72 archived sessions,
|
|
323
|
+
// where do I manage them?" The wording follows the host's probe — telling
|
|
324
|
+
// someone to install what they already have is worse than saying nothing,
|
|
325
|
+
// so only a definite `missing` says so.
|
|
326
|
+
(status?.archivedSessions ?? 0) > 0 && createElement(
|
|
327
|
+
'span',
|
|
328
|
+
{ key: 'archHint', className: 'dsws_setDesc' },
|
|
329
|
+
status?.steward === 'missing'
|
|
330
|
+
? translate(t, 'card.index.archivedMissing')
|
|
331
|
+
: translate(t, 'card.index.archivedOwner'),
|
|
332
|
+
),
|
|
333
|
+
rebuildError !== null && rebuildError !== undefined
|
|
334
|
+
&& createElement('span', { key: 'err', className: 'dsws_setDesc' }, translate(t, 'card.index.rebuildError', { error: rebuildError })),
|
|
335
|
+
syncFailures > 0 && createElement('span', { key: 'warn', className: 'dsws_setDesc' }, translate(t, 'card.index.failures', { count: syncFailures })),
|
|
336
|
+
note !== null && createElement('span', { key: 'note', className: 'dsws_setDesc' }, note),
|
|
337
|
+
createElement('span', { key: 'hint', className: 'dsws_setDesc' }, translate(t, 'card.index.rebuild.hint')),
|
|
338
|
+
]),
|
|
339
|
+
progressBlock,
|
|
340
|
+
createElement('div', { key: 'btns', className: 'dsws_btnRow' }, [
|
|
341
|
+
createElement('button', {
|
|
342
|
+
key: 'rebuild',
|
|
343
|
+
type: 'button',
|
|
344
|
+
className: 'dsws_actBtn',
|
|
345
|
+
disabled: blocking,
|
|
346
|
+
onClick: onRebuild,
|
|
347
|
+
}, rebuilding ? translate(t, 'card.index.rebuilding.btn') : translate(t, 'card.index.rebuild')),
|
|
348
|
+
createElement('button', {
|
|
349
|
+
key: 'export',
|
|
350
|
+
type: 'button',
|
|
351
|
+
className: 'dsws_actBtn',
|
|
352
|
+
disabled: blocking || status?.available !== true,
|
|
353
|
+
onClick: onExport,
|
|
354
|
+
}, translate(t, 'card.index.export')),
|
|
355
|
+
fetchError !== null && createElement('button', {
|
|
356
|
+
key: 'retryStatus',
|
|
357
|
+
type: 'button',
|
|
358
|
+
className: 'dsws_actBtn',
|
|
359
|
+
onClick: () => { setAttempt(n => n + 1) },
|
|
360
|
+
}, '重试状态'),
|
|
361
|
+
createElement('label', { key: 'import', className: 'dsws_actBtn' }, [
|
|
362
|
+
translate(t, 'card.index.import'),
|
|
363
|
+
createElement('input', {
|
|
364
|
+
key: 'file',
|
|
365
|
+
type: 'file',
|
|
366
|
+
accept: '.jsonl,.json,text/plain,application/json',
|
|
367
|
+
style: { display: 'none' },
|
|
368
|
+
onChange: (e: { target: { files: { [index: number]: File | null } | null; value: string } }) => {
|
|
369
|
+
const file = e.target.files?.[0] ?? undefined
|
|
370
|
+
e.target.value = ''
|
|
371
|
+
if (file !== undefined && file !== null) onImportFile(file)
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
]),
|
|
375
|
+
]),
|
|
376
|
+
])
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The settings card body: a collapsed drawer shell (title + description +
|
|
381
|
+
* chevron, thinking-levels pattern) expanding into the namespace fields and
|
|
382
|
+
* the index-lifecycle block.
|
|
383
|
+
* @param props - locale seat (optional) and the bound namespace scope.
|
|
384
|
+
*/
|
|
385
|
+
export function SearchSettingsCard(props: SearchSettingsCardProps): JSX.Element {
|
|
386
|
+
const { scope } = props
|
|
387
|
+
const t = props.t
|
|
388
|
+
// family tab 里默认展开:折叠态在 tab 面板里看起来像「没有设置项」。
|
|
389
|
+
const [open, setOpen] = useState(true)
|
|
390
|
+
const snapshot = useSyncExternalStore(
|
|
391
|
+
(listener) => scope.subscribe(listener),
|
|
392
|
+
() => scope.getSnapshot(),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
const body = createCardBody({ t, snapshot, scope })
|
|
396
|
+
|
|
397
|
+
return createElement('div', {
|
|
398
|
+
style: {
|
|
399
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
|
|
400
|
+
background: 'var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.05))',
|
|
401
|
+
borderRadius: '12px',
|
|
402
|
+
transition: 'border-color 0.16s, background 0.16s',
|
|
403
|
+
},
|
|
404
|
+
}, [
|
|
405
|
+
createElement('button', {
|
|
406
|
+
key: 'head',
|
|
407
|
+
type: 'button',
|
|
408
|
+
'aria-expanded': open,
|
|
409
|
+
style: {
|
|
410
|
+
appearance: 'none',
|
|
411
|
+
width: '100%',
|
|
412
|
+
font: 'inherit',
|
|
413
|
+
color: 'inherit',
|
|
414
|
+
textAlign: 'left',
|
|
415
|
+
cursor: 'pointer',
|
|
416
|
+
background: 'none',
|
|
417
|
+
border: 0,
|
|
418
|
+
borderRadius: '12px',
|
|
419
|
+
display: 'flex',
|
|
420
|
+
alignItems: 'center',
|
|
421
|
+
gap: '12px',
|
|
422
|
+
padding: '14px 16px',
|
|
423
|
+
},
|
|
424
|
+
onClick: () => { setOpen(current => !current) },
|
|
425
|
+
}, [
|
|
426
|
+
createElement('span', { key: 'text', style: { flex: '1 1 0%', minWidth: 0 } }, [
|
|
427
|
+
createElement('div', {
|
|
428
|
+
key: 'title',
|
|
429
|
+
style: { fontSize: '14px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' },
|
|
430
|
+
}, translate(t, 'card.title')),
|
|
431
|
+
createElement('div', {
|
|
432
|
+
key: 'desc',
|
|
433
|
+
style: { color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))', fontSize: '13px', lineHeight: 1.5 },
|
|
434
|
+
}, translate(t, 'card.description')),
|
|
435
|
+
]),
|
|
436
|
+
createElement('svg', {
|
|
437
|
+
key: 'chev',
|
|
438
|
+
width: 16,
|
|
439
|
+
height: 16,
|
|
440
|
+
viewBox: '0 0 16 16',
|
|
441
|
+
'aria-hidden': true,
|
|
442
|
+
style: {
|
|
443
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
444
|
+
flex: '0 0 auto',
|
|
445
|
+
transition: 'transform 0.16s',
|
|
446
|
+
transform: open ? 'rotate(180deg)' : 'none',
|
|
447
|
+
},
|
|
448
|
+
}, createElement('path', {
|
|
449
|
+
d: 'M4 6l4 4 4-4',
|
|
450
|
+
fill: 'none',
|
|
451
|
+
stroke: 'currentColor',
|
|
452
|
+
strokeWidth: 1.5,
|
|
453
|
+
strokeLinecap: 'round',
|
|
454
|
+
strokeLinejoin: 'round',
|
|
455
|
+
})),
|
|
456
|
+
]),
|
|
457
|
+
open && createElement('div', { key: 'body', style: { padding: '12px 16px' } }, body),
|
|
458
|
+
])
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** The card's expandable content: namespace fields plus the index block. */
|
|
462
|
+
function createCardBody(props: {
|
|
463
|
+
t?: SearchSettingsCardProps['t']
|
|
464
|
+
snapshot: ReturnType<SwitchCardScope['getSnapshot']>
|
|
465
|
+
scope: SwitchCardScope
|
|
466
|
+
}): JSX.Element[] {
|
|
467
|
+
const t = props.t
|
|
468
|
+
const snapshot = props.snapshot
|
|
469
|
+
const scope = props.scope
|
|
470
|
+
|
|
471
|
+
if (snapshot.status === 'unavailable') {
|
|
472
|
+
return [
|
|
473
|
+
createElement('div', { className: 'dsws_setRow', key: 'unavailable' },
|
|
474
|
+
createElement('span', { className: 'dsws_setTitle' }, translate(t, 'card.unavailable'))),
|
|
475
|
+
]
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const value: Partial<SwitchSearchConfig> = snapshot.value ?? {}
|
|
479
|
+
const writable = snapshot.writable
|
|
480
|
+
const syncIntervalSeconds = Math.round((value.syncIntervalMs ?? 30_000) / 1000)
|
|
481
|
+
const archiveKeep = value.archiveKeep ?? 2
|
|
482
|
+
|
|
483
|
+
const children: JSX.Element[] = [
|
|
484
|
+
createElement(Row, {
|
|
485
|
+
key: 'enable',
|
|
486
|
+
title: translate(t, 'card.enabled'),
|
|
487
|
+
desc: translate(t, 'card.enabled.desc'),
|
|
488
|
+
control: createElement(Toggle, {
|
|
489
|
+
checked: value.enabled ?? true,
|
|
490
|
+
writable,
|
|
491
|
+
onChange: (checked) => { void scope.set('enabled', checked) },
|
|
492
|
+
}),
|
|
493
|
+
}),
|
|
494
|
+
createElement(Row, {
|
|
495
|
+
key: 'mode',
|
|
496
|
+
title: translate(t, 'card.defaultMode'),
|
|
497
|
+
desc: translate(t, 'card.defaultMode.desc'),
|
|
498
|
+
control: createElement('div', { className: 'dsws_seg', role: 'group' },
|
|
499
|
+
(['title', 'content'] as const).map(mode => createElement('button', {
|
|
500
|
+
key: mode,
|
|
501
|
+
type: 'button',
|
|
502
|
+
className: `dsws_segBtn${(value.defaultMode ?? 'title') === mode ? ' dsws_segBtnActive' : ''}`,
|
|
503
|
+
'aria-pressed': (value.defaultMode ?? 'title') === mode,
|
|
504
|
+
disabled: !writable,
|
|
505
|
+
onClick: () => { void scope.set('defaultMode', mode) },
|
|
506
|
+
}, mode === 'title' ? translate(t, 'card.mode.title') : translate(t, 'card.mode.content')))),
|
|
507
|
+
}),
|
|
508
|
+
createElement(Row, {
|
|
509
|
+
key: 'autoSync',
|
|
510
|
+
title: translate(t, 'card.autoSync'),
|
|
511
|
+
desc: translate(t, 'card.autoSync.desc'),
|
|
512
|
+
control: createElement(Toggle, {
|
|
513
|
+
checked: value.autoSync ?? true,
|
|
514
|
+
writable,
|
|
515
|
+
onChange: (checked) => { void scope.set('autoSync', checked) },
|
|
516
|
+
}),
|
|
517
|
+
}),
|
|
518
|
+
createElement(Row, {
|
|
519
|
+
key: 'interval',
|
|
520
|
+
title: translate(t, 'card.syncInterval'),
|
|
521
|
+
desc: translate(t, 'card.syncInterval.desc'),
|
|
522
|
+
control: createElement('input', {
|
|
523
|
+
type: 'number',
|
|
524
|
+
min: 5,
|
|
525
|
+
max: 3600,
|
|
526
|
+
disabled: !writable,
|
|
527
|
+
value: syncIntervalSeconds,
|
|
528
|
+
style: { width: '72px', boxSizing: 'border-box' },
|
|
529
|
+
className: 'dsws_search',
|
|
530
|
+
onChange: (e: { target: { value: string } }) => {
|
|
531
|
+
const seconds = Number(e.target.value)
|
|
532
|
+
if (Number.isFinite(seconds) && seconds >= 5) void scope.set('syncIntervalMs', Math.round(seconds * 1000))
|
|
533
|
+
},
|
|
534
|
+
}),
|
|
535
|
+
}),
|
|
536
|
+
createElement(Row, {
|
|
537
|
+
key: 'archiveKeep',
|
|
538
|
+
title: translate(t, 'card.archiveKeep'),
|
|
539
|
+
desc: translate(t, 'card.archiveKeep.desc'),
|
|
540
|
+
control: createElement('input', {
|
|
541
|
+
type: 'number',
|
|
542
|
+
min: 0,
|
|
543
|
+
max: 20,
|
|
544
|
+
disabled: !writable,
|
|
545
|
+
value: archiveKeep,
|
|
546
|
+
style: { width: '72px', boxSizing: 'border-box' },
|
|
547
|
+
className: 'dsws_search',
|
|
548
|
+
onChange: (e: { target: { value: string } }) => {
|
|
549
|
+
const count = Number(e.target.value)
|
|
550
|
+
if (Number.isFinite(count) && count >= 0) void scope.set('archiveKeep', Math.round(count))
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
553
|
+
}),
|
|
554
|
+
]
|
|
555
|
+
|
|
556
|
+
const indexBlock = IndexBlock({ t })
|
|
557
|
+
children.push(indexBlock)
|
|
558
|
+
|
|
559
|
+
if (!writable) {
|
|
560
|
+
children.push(createElement('div', { key: 'ro', className: 'dsws_setDesc' }, translate(t, 'card.readonly')))
|
|
561
|
+
}
|
|
562
|
+
return children
|
|
563
|
+
}
|