@anionex/dsh-eye-care 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +86 -0
  4. package/README.zh.md +86 -0
  5. package/assets/eye-care-dark.png +0 -0
  6. package/assets/eye-care-light.png +0 -0
  7. package/cordis.patch.yml +3 -0
  8. package/lib/client.js +1106 -0
  9. package/lib/client.js.map +1 -0
  10. package/lib/index.js +14 -0
  11. package/lib/index.js.map +1 -0
  12. package/lib/rpc.js +123 -0
  13. package/lib/rpc.js.map +1 -0
  14. package/lib/settings.js +12 -0
  15. package/lib/settings.js.map +1 -0
  16. package/lib/shared.js +70 -0
  17. package/lib/shared.js.map +1 -0
  18. package/lib/types/client/EyeCareRow.d.ts +20 -0
  19. package/lib/types/client/EyeCareRow.d.ts.map +1 -0
  20. package/lib/types/client/controller.d.ts +96 -0
  21. package/lib/types/client/controller.d.ts.map +1 -0
  22. package/lib/types/client/index.d.ts +23 -0
  23. package/lib/types/client/index.d.ts.map +1 -0
  24. package/lib/types/client/locales.d.ts +54 -0
  25. package/lib/types/client/locales.d.ts.map +1 -0
  26. package/lib/types/client/themes.d.ts +14 -0
  27. package/lib/types/client/themes.d.ts.map +1 -0
  28. package/lib/types/index.d.ts +10 -0
  29. package/lib/types/index.d.ts.map +1 -0
  30. package/lib/types/rpc.d.ts +17 -0
  31. package/lib/types/rpc.d.ts.map +1 -0
  32. package/lib/types/settings.d.ts +8 -0
  33. package/lib/types/settings.d.ts.map +1 -0
  34. package/lib/types/shared.d.ts +59 -0
  35. package/lib/types/shared.d.ts.map +1 -0
  36. package/package.json +99 -0
  37. package/src/client/EyeCareRow.module.css +137 -0
  38. package/src/client/EyeCareRow.tsx +122 -0
  39. package/src/client/controller.ts +435 -0
  40. package/src/client/css-modules.d.ts +5 -0
  41. package/src/client/index.tsx +72 -0
  42. package/src/client/locales.ts +56 -0
  43. package/src/client/themes.ts +254 -0
  44. package/src/index.ts +30 -0
  45. package/src/rpc.ts +138 -0
  46. package/src/settings.ts +22 -0
  47. package/src/shared.ts +112 -0
@@ -0,0 +1,435 @@
1
+ /** Eye-care state owner: durable settings synchronization plus theme lifecycle. */
2
+
3
+ import type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client'
4
+ import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
5
+ import type { ThemeRuntime, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
6
+ import {
7
+ DEFAULT_EYE_CARE_SETTINGS,
8
+ decodeEyeCareSnapshot,
9
+ EYE_CARE_RPC_CHANNEL,
10
+ EYE_CARE_RPC_READ,
11
+ EYE_CARE_RPC_SAVE,
12
+ type EyeCareIntensity,
13
+ type EyeCareMode,
14
+ type EyeCareSettings,
15
+ type EyeCareSettingsSnapshot,
16
+ } from '../shared.ts'
17
+ import {
18
+ EYE_CARE_THEMES,
19
+ eyeCareThemeId,
20
+ isEyeCareThemeId,
21
+ } from './themes.ts'
22
+
23
+ /** State consumed by the General-settings row. */
24
+ export interface EyeCareState {
25
+ /** Current eye-care selection. */
26
+ settings: EyeCareSettings
27
+ /** Persistence lifecycle. */
28
+ status: 'loading' | 'ready' | 'saving' | 'unavailable' | 'error'
29
+ /** Whether the Host settings document can be written. */
30
+ writable: boolean
31
+ /** Host namespace revision for the next write. */
32
+ revision: number | undefined
33
+ /** User-facing read or write failure, when one stands. */
34
+ error: string | null
35
+ }
36
+
37
+ /** Inputs isolated for deterministic controller tests. */
38
+ export interface EyeCareControllerOptions {
39
+ /** DSH theme registry. */
40
+ theme: Pick<ThemeRuntime, 'getTheme' | 'setTheme' | 'register'>
41
+ /** Snapshot change subscription. */
42
+ subscribeTheme(listener: (snapshot: ThemeSnapshot) => void): () => void
43
+ /** Loopback-only Connection RPC; absent remote browsers stay process-local. */
44
+ rpc?: ClientConnectionRpc
45
+ /** System dark-mode sensor. */
46
+ media?: MediaQueryList
47
+ /** Monotonic wall-clock source used only by the one-shot startup adoption fence. */
48
+ now?: () => number
49
+ }
50
+
51
+ const INITIAL_STATE: EyeCareState = {
52
+ settings: { ...DEFAULT_EYE_CARE_SETTINGS },
53
+ status: 'loading',
54
+ writable: false,
55
+ revision: undefined,
56
+ error: null,
57
+ }
58
+
59
+ /** Maximum delay accepted for ui-theme's already-started Host adoption. */
60
+ const STARTUP_BASE_THEME_GRACE_MS = 1000
61
+
62
+ function sameSettings(left: EyeCareSettings, right: EyeCareSettings): boolean {
63
+ return left.mode === right.mode && left.intensity === right.intensity
64
+ }
65
+
66
+ function messageOf(error: unknown): string {
67
+ return error instanceof Error ? error.message : String(error)
68
+ }
69
+
70
+ /** Stateful controller shared by theme listeners and the Settings row. */
71
+ export class EyeCareController {
72
+ /** Reactive row source. */
73
+ readonly store: SnapshotStore<EyeCareState> = createSnapshotStore(INITIAL_STATE)
74
+
75
+ private readonly theme: EyeCareControllerOptions['theme']
76
+ private readonly rpc: ClientConnectionRpc | undefined
77
+ private readonly media: MediaQueryList | undefined
78
+ private readonly now: () => number
79
+ private readonly unregisterThemes: Array<() => void> = []
80
+ private unsubscribeTheme: (() => void) | undefined
81
+ private unsubscribeMedia: (() => void) | undefined
82
+ private tail: Promise<void> = Promise.resolve()
83
+ private disposeTask: Promise<void> | undefined
84
+ private pendingSettings: EyeCareSettings | undefined
85
+ private disposed = false
86
+ private applyingTheme = 0
87
+ private restoreTheme = 'system'
88
+ private hasRestoreTheme = false
89
+ private acceptedRemoteSnapshot = false
90
+ private startupBaseTheme: 'light' | 'dark' | 'system' | undefined
91
+ private startupBaseThemeDeadline = 0
92
+
93
+ /**
94
+ * Register the concrete themes and lifecycle listeners.
95
+ * @param options - theme, transport, and system-scheme collaborators.
96
+ */
97
+ constructor(options: EyeCareControllerOptions) {
98
+ this.theme = options.theme
99
+ this.rpc = options.rpc
100
+ this.now = options.now ?? Date.now
101
+ this.media = options.media ?? (typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : undefined)
102
+ const currentTheme = this.theme.getTheme().preference
103
+ if (!isEyeCareThemeId(currentTheme)) {
104
+ this.restoreTheme = currentTheme
105
+ this.hasRestoreTheme = true
106
+ }
107
+ try {
108
+ for (const definition of EYE_CARE_THEMES) {
109
+ this.unregisterThemes.push(this.theme.register(definition))
110
+ }
111
+ this.unsubscribeTheme = options.subscribeTheme(snapshot => { this.onThemeChanged(snapshot) })
112
+ if (this.media !== undefined) {
113
+ const media = this.media
114
+ const listener = (): void => {
115
+ if (!this.disposed && this.store.getSnapshot().settings.mode === 'auto') this.applyCurrentTheme()
116
+ }
117
+ media.addEventListener('change', listener)
118
+ this.unsubscribeMedia = () => { media.removeEventListener('change', listener) }
119
+ }
120
+ } catch (error) {
121
+ for (const unregister of this.unregisterThemes.splice(0).reverse()) unregister()
122
+ throw error
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Load durable settings without blocking plugin activation.
128
+ * @returns settlement after this read reaches the serialized transport queue.
129
+ */
130
+ load(): Promise<void> {
131
+ if (this.rpc === undefined) {
132
+ this.setUnavailable()
133
+ return Promise.resolve()
134
+ }
135
+ if (this.store.getSnapshot().revision === undefined) {
136
+ this.store.update(state => {
137
+ state.status = 'loading'
138
+ state.error = null
139
+ })
140
+ }
141
+ return this.enqueue(async () => { await this.readRemote() })
142
+ }
143
+
144
+ /**
145
+ * Select a mode and persist the latest selection when Host settings are writable.
146
+ * @param mode - next operating mode.
147
+ * @returns settlement after the serialized persistence attempt.
148
+ */
149
+ setMode(mode: EyeCareMode): Promise<void> {
150
+ return this.select({ ...this.store.getSnapshot().settings, mode })
151
+ }
152
+
153
+ /**
154
+ * Select a warmth and persist the latest selection when Host settings are writable.
155
+ * @param intensity - next warmth level.
156
+ * @returns settlement after the serialized persistence attempt.
157
+ */
158
+ setIntensity(intensity: EyeCareIntensity): Promise<void> {
159
+ return this.select({ ...this.store.getSnapshot().settings, intensity })
160
+ }
161
+
162
+ /** Queue a refresh after a Host settings invalidation. */
163
+ refresh(): void {
164
+ void this.load()
165
+ }
166
+
167
+ /**
168
+ * Reach transport quiescence, restore the user's live non-eye-care theme, and unregister all resources.
169
+ * @returns settlement after cleanup completes.
170
+ */
171
+ dispose(): Promise<void> {
172
+ return this.disposeTask ??= this.disposeOnce()
173
+ }
174
+
175
+ private async disposeOnce(): Promise<void> {
176
+ this.disposed = true
177
+ this.pendingSettings = undefined
178
+
179
+ const failures: unknown[] = []
180
+ for (const unsubscribe of [this.unsubscribeMedia, this.unsubscribeTheme]) {
181
+ if (unsubscribe === undefined) continue
182
+ try { unsubscribe() } catch (error) { failures.push(error) }
183
+ }
184
+ this.unsubscribeMedia = undefined
185
+ this.unsubscribeTheme = undefined
186
+
187
+ await this.tail
188
+
189
+ const current = this.theme.getTheme()
190
+ if (isEyeCareThemeId(current.preference)) {
191
+ try {
192
+ this.setTheme(this.resolveRestoreTheme())
193
+ } catch (error) {
194
+ failures.push(error)
195
+ try { this.setTheme('system') } catch (fallbackError) { failures.push(fallbackError) }
196
+ }
197
+ }
198
+ for (const unregister of this.unregisterThemes.splice(0).reverse()) {
199
+ try { unregister() } catch (error) { failures.push(error) }
200
+ }
201
+ if (failures.length > 0) throw new AggregateError(failures, 'eye-care cleanup failed')
202
+ }
203
+
204
+ private enqueue(operation: () => Promise<void>): Promise<void> {
205
+ if (this.disposed) return Promise.resolve()
206
+ const task = this.tail.then(async () => {
207
+ if (!this.disposed) await operation()
208
+ })
209
+ this.tail = task.catch(() => {})
210
+ return task
211
+ }
212
+
213
+ private select(settings: EyeCareSettings): Promise<void> {
214
+ if (this.disposed) return Promise.resolve()
215
+ const previous = this.store.getSnapshot().settings
216
+ if (previous.mode === 'off' && settings.mode !== 'off') this.captureRestoreTheme()
217
+ this.store.update(state => {
218
+ state.settings = settings
219
+ state.error = null
220
+ if (state.status === 'error') state.status = state.revision === undefined ? 'unavailable' : 'ready'
221
+ })
222
+ this.applyCurrentTheme()
223
+ return this.persist(settings)
224
+ }
225
+
226
+ private persist(settings: EyeCareSettings): Promise<void> {
227
+ const state = this.store.getSnapshot()
228
+ if (this.rpc === undefined) {
229
+ this.setUnavailable(state.error)
230
+ return Promise.resolve()
231
+ }
232
+ if (state.revision !== undefined && !state.writable) {
233
+ this.setUnavailable(state.error)
234
+ return Promise.resolve()
235
+ }
236
+ this.pendingSettings = { ...settings }
237
+ return this.enqueue(async () => { await this.flushPending() })
238
+ }
239
+
240
+ private async flushPending(): Promise<void> {
241
+ while (!this.disposed && this.pendingSettings !== undefined) {
242
+ const desired = this.pendingSettings
243
+ this.pendingSettings = undefined
244
+
245
+ let state = this.store.getSnapshot()
246
+ if (state.revision === undefined) {
247
+ if (this.pendingSettings === undefined) this.pendingSettings = desired
248
+ if (!await this.readRemote()) return
249
+ continue
250
+ }
251
+ if (!state.writable || state.revision === undefined) {
252
+ this.setUnavailable(state.error)
253
+ return
254
+ }
255
+
256
+ this.store.update(next => {
257
+ next.status = 'saving'
258
+ next.error = null
259
+ })
260
+ let response: Awaited<ReturnType<ClientConnectionRpc['call']>>
261
+ try {
262
+ const rpc = this.rpc
263
+ if (rpc === undefined) return
264
+ response = await rpc.call(EYE_CARE_RPC_CHANNEL, EYE_CARE_RPC_SAVE, {
265
+ expectedRevision: state.revision,
266
+ value: desired,
267
+ })
268
+ } catch (error) {
269
+ if (!this.disposed) this.setUnavailable(messageOf(error))
270
+ return
271
+ }
272
+ if (this.disposed) return
273
+ if (!response.ok) {
274
+ if (response.error.code === 'settings-conflict') {
275
+ const latest = this.pendingSettings ?? desired
276
+ this.pendingSettings = latest
277
+ if (!await this.readRemote()) return
278
+ if (this.disposed) return
279
+ continue
280
+ }
281
+ this.setUnavailable(response.error.message)
282
+ return
283
+ }
284
+ const snapshot = decodeEyeCareSnapshot(response.value)
285
+ if (snapshot === undefined) {
286
+ this.store.update(next => {
287
+ next.status = 'error'
288
+ next.error = 'eye-care Settings returned an invalid save response'
289
+ })
290
+ return
291
+ }
292
+ const preserveSettings = this.pendingSettings !== undefined
293
+ || !sameSettings(this.store.getSnapshot().settings, desired)
294
+ this.accept(snapshot, preserveSettings)
295
+ }
296
+ }
297
+
298
+ private async readRemote(): Promise<boolean> {
299
+ let response: Awaited<ReturnType<ClientConnectionRpc['call']>>
300
+ try {
301
+ const rpc = this.rpc
302
+ if (rpc === undefined) return false
303
+ response = await rpc.call(EYE_CARE_RPC_CHANNEL, EYE_CARE_RPC_READ, {})
304
+ } catch (error) {
305
+ if (!this.disposed) this.setUnavailable(messageOf(error), true)
306
+ return false
307
+ }
308
+ if (this.disposed) return false
309
+ if (!response.ok) {
310
+ this.setUnavailable(response.error.message, true)
311
+ return false
312
+ }
313
+ const snapshot = decodeEyeCareSnapshot(response.value)
314
+ if (snapshot === undefined) {
315
+ this.store.update(state => {
316
+ state.status = 'error'
317
+ state.error = 'eye-care Settings returned an invalid read response'
318
+ })
319
+ return false
320
+ }
321
+ this.accept(snapshot, this.pendingSettings !== undefined)
322
+ return true
323
+ }
324
+
325
+ private accept(snapshot: EyeCareSettingsSnapshot, preserveSettings: boolean): void {
326
+ const previous = this.store.getSnapshot().settings
327
+ const next = preserveSettings ? previous : snapshot.settings.value
328
+ const firstRemote = !this.acceptedRemoteSnapshot
329
+ this.acceptedRemoteSnapshot = true
330
+
331
+ if (next.mode === 'off') {
332
+ this.startupBaseTheme = undefined
333
+ this.startupBaseThemeDeadline = 0
334
+ if (!this.hasRestoreTheme && snapshot.baseThemePreference !== undefined) {
335
+ this.restoreTheme = snapshot.baseThemePreference
336
+ this.hasRestoreTheme = true
337
+ }
338
+ } else if (firstRemote) {
339
+ const base = snapshot.baseThemePreference
340
+ if (base === undefined) {
341
+ if (!this.hasRestoreTheme) this.captureRestoreTheme()
342
+ } else {
343
+ this.restoreTheme = base
344
+ this.hasRestoreTheme = true
345
+ if (this.theme.getTheme().preference !== base) {
346
+ this.startupBaseTheme = base
347
+ this.startupBaseThemeDeadline = this.now() + STARTUP_BASE_THEME_GRACE_MS
348
+ }
349
+ }
350
+ } else if (!preserveSettings && previous.mode === 'off') {
351
+ this.captureRestoreTheme(snapshot.baseThemePreference)
352
+ }
353
+
354
+ this.store.set({
355
+ settings: next,
356
+ status: snapshot.writable ? 'ready' : 'unavailable',
357
+ writable: snapshot.writable,
358
+ revision: snapshot.settings.revision,
359
+ error: null,
360
+ })
361
+ this.applyCurrentTheme()
362
+ }
363
+
364
+ private setUnavailable(error: string | null = null, clearRevision = false): void {
365
+ this.store.update(state => {
366
+ state.status = 'unavailable'
367
+ state.writable = false
368
+ if (clearRevision) state.revision = undefined
369
+ state.error = error
370
+ })
371
+ }
372
+
373
+ private captureRestoreTheme(fallback?: 'light' | 'dark' | 'system'): void {
374
+ const preference = this.theme.getTheme().preference
375
+ const candidate = isEyeCareThemeId(preference) ? fallback ?? 'system' : preference
376
+ this.restoreTheme = candidate
377
+ this.hasRestoreTheme = true
378
+ }
379
+
380
+ private resolveRestoreTheme(): string {
381
+ if (!this.hasRestoreTheme || this.restoreTheme === 'system') return 'system'
382
+ return this.theme.getTheme().themes.some(theme => theme.id === this.restoreTheme)
383
+ ? this.restoreTheme
384
+ : 'system'
385
+ }
386
+
387
+ private applyCurrentTheme(): void {
388
+ const { mode, intensity } = this.store.getSnapshot().settings
389
+ const current = this.theme.getTheme().preference
390
+ if (mode === 'off') {
391
+ if (isEyeCareThemeId(current)) this.setTheme(this.resolveRestoreTheme())
392
+ this.hasRestoreTheme = false
393
+ return
394
+ }
395
+ if (!this.hasRestoreTheme) this.captureRestoreTheme()
396
+ const scheme = mode === 'auto' ? (this.media?.matches === true ? 'dark' : 'light') : mode
397
+ this.setTheme(eyeCareThemeId(scheme, intensity))
398
+ }
399
+
400
+ private setTheme(id: string): void {
401
+ if (this.theme.getTheme().preference === id) return
402
+ this.applyingTheme += 1
403
+ try {
404
+ this.theme.setTheme(id)
405
+ } finally {
406
+ this.applyingTheme -= 1
407
+ }
408
+ }
409
+
410
+ private onThemeChanged(snapshot: ThemeSnapshot): void {
411
+ if (this.disposed || this.applyingTheme > 0 || isEyeCareThemeId(snapshot.preference)) return
412
+ if (this.startupBaseTheme !== undefined) {
413
+ const expected = this.startupBaseTheme
414
+ const withinStartupWindow = this.now() <= this.startupBaseThemeDeadline
415
+ this.startupBaseTheme = undefined
416
+ this.startupBaseThemeDeadline = 0
417
+ if (withinStartupWindow && snapshot.preference === expected) {
418
+ this.restoreTheme = expected
419
+ this.hasRestoreTheme = true
420
+ this.applyCurrentTheme()
421
+ return
422
+ }
423
+ }
424
+ const state = this.store.getSnapshot()
425
+ if (state.settings.mode === 'off') return
426
+ this.restoreTheme = snapshot.preference
427
+ this.hasRestoreTheme = true
428
+ const settings = { ...state.settings, mode: 'off' as const }
429
+ this.store.update(next => {
430
+ next.settings = settings
431
+ next.error = null
432
+ })
433
+ void this.persist(settings)
434
+ }
435
+ }
@@ -0,0 +1,5 @@
1
+ /** CSS Module class maps emitted by the browser bundle. */
2
+ declare module '*.module.css' {
3
+ const classes: Record<string, string>
4
+ export default classes
5
+ }
@@ -0,0 +1,72 @@
1
+ /** DSH Web browser half for the eye-care profile bundle. */
2
+
3
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
4
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
5
+ import type { ThemeRuntime, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
6
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
7
+ import type {} from '@deepseek-ai/dsh-api-remotes/client'
8
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
9
+ import type {} from '@deepseek-ai/dsh-client-ui-theme/client'
10
+ import { EyeCareRow, type EyeCareRowInjected } from './EyeCareRow.tsx'
11
+ import { EyeCareController } from './controller.ts'
12
+ import { en, zh, type EyeCareKey } from './locales.ts'
13
+
14
+ /** Required browser services. */
15
+ export const inject = ['slots', 'locale', 'theme', 'connection', 'remote']
16
+
17
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
18
+ interface LocaleNamespaceMap {
19
+ /** Eye-care General row copy. */
20
+ 'settings.eyeCare': EyeCareKey
21
+ }
22
+ }
23
+
24
+ /** Browser context with the theme service declaration merged by ui-theme. */
25
+ type EyeCareClientContext = ClientContext & { theme: ThemeRuntime }
26
+
27
+ /** Register settings copy, controller lifecycle, and General row. */
28
+ export function apply(ctx: EyeCareClientContext): void {
29
+ const theme = ctx.get('theme') as ThemeRuntime
30
+ const connection = ctx.get('connection') as ConnectionHandle
31
+ const controller = new EyeCareController({
32
+ theme,
33
+ ...(connection.isLoopback ? { rpc: connection.rpc } : {}),
34
+ subscribeTheme: (listener: (snapshot: ThemeSnapshot) => void): (() => void) => ctx.on('theme/change', listener),
35
+ })
36
+ ctx.effect(() => ctx.locale.register('settings.eyeCare', { zh, en }), 'dsh-eye-care: dictionaries')
37
+ ctx.effect(() => {
38
+ const refresh = (namespace?: string): void => {
39
+ if (namespace !== undefined && namespace !== 'eye-care') return
40
+ controller.refresh()
41
+ }
42
+ const disposers = [
43
+ ctx.remote.$on('settings/document-updated', refresh),
44
+ ctx.on('connection/reset', () => {
45
+ if (connection.isLoopback) controller.refresh()
46
+ }),
47
+ ]
48
+ void controller.load()
49
+ return async () => {
50
+ for (const dispose of disposers) dispose()
51
+ await controller.dispose()
52
+ }
53
+ }, 'dsh-eye-care: controller lifecycle')
54
+
55
+ const injected = (): EyeCareRowInjected => ({
56
+ hooks: { eyeCare: controller.store },
57
+ setMode: mode => controller.setMode(mode),
58
+ setIntensity: intensity => controller.setIntensity(intensity),
59
+ })
60
+ ctx.slots.inject('settings.general.item', () => ctx.slots.register({
61
+ name: 'settings.general.item',
62
+ id: 'eye-care',
63
+ order: 15,
64
+ locale: 'settings.eyeCare',
65
+ inject: injected,
66
+ }, EyeCareRow))
67
+ }
68
+
69
+ export type { EyeCareRowInjected, EyeCareRowProps } from './EyeCareRow.tsx'
70
+ export type { EyeCareState } from './controller.ts'
71
+ export { EyeCareController } from './controller.ts'
72
+ export { EYE_CARE_THEMES, eyeCareThemeId, isEyeCareThemeId } from './themes.ts'
@@ -0,0 +1,56 @@
1
+ /** `settings.eyeCare` dictionaries. */
2
+
3
+ /** Simplified Chinese dictionary. */
4
+ export const zh = {
5
+ 'title': '护眼模式',
6
+ 'description': '降低页面的冷白光感,长时间阅读更柔和。',
7
+ 'enabled.title': '护眼模式',
8
+ 'enabled.off': '关闭',
9
+ 'enabled.on': '开启',
10
+ 'advanced.title': '显示方式与暖色强度',
11
+ 'mode.title': '显示方式',
12
+ 'mode.off': '关闭',
13
+ 'mode.auto': '自动',
14
+ 'mode.light': '日间',
15
+ 'mode.dark': '夜间',
16
+ 'intensity.title': '暖色强度',
17
+ 'intensity.soft': '柔和',
18
+ 'intensity.balanced': '均衡',
19
+ 'intensity.warm': '温暖',
20
+ 'status.off': '护眼模式已关闭',
21
+ 'status.auto': '自动跟随系统明暗外观',
22
+ 'status.light': '正在使用暖色日间主题',
23
+ 'status.dark': '正在使用暖暗夜间主题',
24
+ 'status.local': '仅当前浏览器有效',
25
+ 'status.saving': '正在保存',
26
+ 'status.error': '设置暂未保存',
27
+ } satisfies Record<string, string>
28
+
29
+ /** Dictionary key union. */
30
+ export type EyeCareKey = keyof typeof zh
31
+
32
+ /** English dictionary. */
33
+ export const en = {
34
+ 'title': 'Eye care',
35
+ 'description': 'Use warm semantic themes to reduce cool-white glare during long reading sessions',
36
+ 'enabled.title': 'Eye care',
37
+ 'enabled.off': 'Off',
38
+ 'enabled.on': 'On',
39
+ 'advanced.title': 'Appearance and warmth',
40
+ 'mode.title': 'Appearance',
41
+ 'mode.off': 'Off',
42
+ 'mode.auto': 'Auto',
43
+ 'mode.light': 'Day',
44
+ 'mode.dark': 'Night',
45
+ 'intensity.title': 'Warmth',
46
+ 'intensity.soft': 'Soft',
47
+ 'intensity.balanced': 'Balanced',
48
+ 'intensity.warm': 'Warm',
49
+ 'status.off': 'Eye care is off',
50
+ 'status.auto': 'Following the system light or dark appearance',
51
+ 'status.light': 'Warm day theme is active',
52
+ 'status.dark': 'Warm night theme is active',
53
+ 'status.local': 'Current browser only',
54
+ 'status.saving': 'Saving',
55
+ 'status.error': 'Setting is not saved yet',
56
+ } satisfies Record<EyeCareKey, string>