File size: 5,557 Bytes
00a912e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { create } from 'zustand'
import { storage, SETTING_DEFAULTS } from '@/lib/storage'

// 重新导出供 UI 层使用
export { SETTING_DEFAULTS }

// ---------------------------------------------------------------------------
// 类型定义
// ---------------------------------------------------------------------------

/** 默认值表的 key 类型 */
type SettingKey = keyof typeof SETTING_DEFAULTS

/** 从默认值表的 key 推导出 value 类型 */
type SettingValue<K extends SettingKey> = (typeof SETTING_DEFAULTS)[K]

/** 每个 resettable key 在 store 中生成的四个字段名 */
type ResettableFields<K extends string, V> = {
  [P in K]: V
} & {
  [P in `${K}Default`]: V
} & {
  [P in `set${Capitalize<K>}`]: (v: V) => void
} & {
  [P in `reset${Capitalize<K>}`]: () => void
}

/** 合并所有 resettable key 的字段为完整 SettingsStore 类型 */
type SettingsStore = ResettableFields<'playbackTempoSyncEnabled', boolean> &
  ResettableFields<'playbackHardSeekSyncEnabled', boolean> &
  ResettableFields<'syncPacketIntervalSeconds', number> &
  ResettableFields<'ttmlEnabled', boolean> &
  ResettableFields<'ttmlDbUrl', string> &
  ResettableFields<'lyricAlignAnchor', 'top' | 'center' | 'bottom'> &
  ResettableFields<'lyricAlignPosition', number> &
  ResettableFields<'lyricEnableSpring', boolean> &
  ResettableFields<'lyricEnableBlur', boolean> &
  ResettableFields<'lyricEnableScale', boolean> &
  ResettableFields<'lyricFontWeight', number> &
  ResettableFields<'lyricFontSize', number> &
  ResettableFields<'lyricTranslationFontSize', number> &
  ResettableFields<'lyricRomanFontSize', number> & {
    lyricOffsets: Record<string, number>
    setLyricOffset: (key: string, offsetMs: number) => void
    clearLyricOffset: (key: string) => void
  } & ResettableFields<'bgFps', number> &
  ResettableFields<'bgFlowSpeed', number> &
  ResettableFields<'bgRenderScale', number>

// ---------------------------------------------------------------------------
// Store 实现
// ---------------------------------------------------------------------------

/** 首字母大写 */
function capitalize(s: string): string {
  return s.charAt(0).toUpperCase() + s.slice(1)
}

export const useSettingsStore = create<SettingsStore>((set) => {
  /**
   * resettable 工厂:为每个 setting key 生成 4 个 store 字段
   *   key         → 当前值
   *   keyDefault  → 默认值(只读)
   *   setKey      → setter(持久化 + 更新 store)
   *   resetKey    → 恢复默认值
   */
  function resettable<K extends SettingKey>(
    key: K,
    load: () => SettingValue<K>,
    persist: (v: SettingValue<K>) => void,
  ): Record<string, unknown> {
    const defaultValue = SETTING_DEFAULTS[key]
    const initial = load()
    const cap = capitalize(key)

    const setValue = (v: SettingValue<K>) => {
      persist(v)
      set({ [key]: v } as Partial<SettingsStore>)
    }

    return {
      [key]: initial,
      [`${key}Default`]: defaultValue,
      [`set${cap}`]: setValue,
      [`reset${cap}`]: () => setValue(defaultValue),
    }
  }

  return {
    ...resettable('playbackTempoSyncEnabled', storage.getPlaybackTempoSyncEnabled, storage.setPlaybackTempoSyncEnabled),
    ...resettable(
      'playbackHardSeekSyncEnabled',
      storage.getPlaybackHardSeekSyncEnabled,
      storage.setPlaybackHardSeekSyncEnabled,
    ),
    ...resettable(
      'syncPacketIntervalSeconds',
      storage.getSyncPacketIntervalSeconds,
      storage.setSyncPacketIntervalSeconds,
    ),
    ...resettable('ttmlEnabled', storage.getTtmlEnabled, storage.setTtmlEnabled),
    ...resettable('ttmlDbUrl', storage.getTtmlDbUrl, storage.setTtmlDbUrl),
    ...resettable('lyricAlignAnchor', storage.getLyricAlignAnchor, storage.setLyricAlignAnchor),
    ...resettable('lyricAlignPosition', storage.getLyricAlignPosition, storage.setLyricAlignPosition),
    ...resettable('lyricEnableSpring', storage.getLyricEnableSpring, storage.setLyricEnableSpring),
    ...resettable('lyricEnableBlur', storage.getLyricEnableBlur, storage.setLyricEnableBlur),
    ...resettable('lyricEnableScale', storage.getLyricEnableScale, storage.setLyricEnableScale),
    ...resettable('lyricFontWeight', storage.getLyricFontWeight, storage.setLyricFontWeight),
    ...resettable('lyricFontSize', storage.getLyricFontSize, storage.setLyricFontSize),
    ...resettable('lyricTranslationFontSize', storage.getLyricTranslationFontSize, storage.setLyricTranslationFontSize),
    ...resettable('lyricRomanFontSize', storage.getLyricRomanFontSize, storage.setLyricRomanFontSize),
    lyricOffsets: storage.getLyricOffsets(),
    setLyricOffset: (key, offsetMs) => {
      const value = Math.round(Math.max(-10_000, Math.min(10_000, offsetMs)))
      set((state) => {
        const lyricOffsets = { ...state.lyricOffsets }
        if (value === 0) delete lyricOffsets[key]
        else lyricOffsets[key] = value
        storage.setLyricOffsets(lyricOffsets)
        return { lyricOffsets }
      })
    },
    clearLyricOffset: (key) => {
      set((state) => {
        if (!(key in state.lyricOffsets)) return state
        const lyricOffsets = { ...state.lyricOffsets }
        delete lyricOffsets[key]
        storage.setLyricOffsets(lyricOffsets)
        return { lyricOffsets }
      })
    },
    ...resettable('bgFps', storage.getBgFps, storage.setBgFps),
    ...resettable('bgFlowSpeed', storage.getBgFlowSpeed, storage.setBgFlowSpeed),
    ...resettable('bgRenderScale', storage.getBgRenderScale, storage.setBgRenderScale),
  } as SettingsStore
})