xref: /MusicFree/src/core/config.ts (revision 6704747af84cebd842b258efac7143542722fac5)
1import {getStorage, setStorage} from '@/utils/storage';
2import produce from 'immer';
3import {useEffect, useState} from 'react';
4
5type ExceptionType = IMusic.IMusicItem | IMusic.IMusicItem[];
6interface IConfig {
7  setting: {
8    basic: {
9      /** 最大同时下载 */
10      maxDownload: number | string;
11      /** 同时播放 */
12      notInterrupt: boolean;
13      /** 播放错误时自动停止 */
14      autoStopWhenError: boolean;
15
16      debug: {
17        errorLog: boolean;
18        traceLog: boolean;
19      };
20    };
21
22    /** 主题 */
23    theme: {
24      mode: 'light' | 'dark' | 'custom-light' | 'custom-dark';
25      background: string;
26      backgroundOpacity: number;
27      backgroundBlur: number;
28      colors: {
29        primary: string;
30        secondary: string;
31        textHighlight: string;
32        pageBackground: string;
33      };
34    };
35  };
36  status: {
37    music: {
38      /** 当前的音乐 */
39      track: IMusic.IMusicItem;
40      /** 进度 */
41      progress: number;
42      /** 模式 */
43      repeatMode: string;
44      /** 列表 */
45      musicQueue: IMusic.IMusicItem[];
46    };
47  };
48}
49
50type FilterType<T, R = never> = T extends Record<string | number, any>
51  ? {
52      [P in keyof T]: T[P] extends ExceptionType ? R : T[P];
53    }
54  : never;
55
56type KeyPaths<
57  T extends object,
58  Root extends boolean = true,
59  R = FilterType<T, ''>,
60  K extends keyof R = keyof R,
61> = K extends string | number
62  ?
63      | (Root extends true ? `${K}` : `.${K}`)
64      | (R[K] extends Record<string | number, any>
65          ? `${Root extends true ? `${K}` : `.${K}`}${KeyPaths<R[K], false>}`
66          : never)
67  : never;
68
69type KeyPathValue<T extends object, K extends string> = T extends Record<
70  string | number,
71  any
72>
73  ? K extends `${infer S}.${infer R}`
74    ? KeyPathValue<T[S], R>
75    : T[K]
76  : never;
77
78type KeyPathsObj<
79  T extends object,
80  K extends string = KeyPaths<T>,
81> = T extends Record<string | number, any>
82  ? {
83      [R in K]: KeyPathValue<T, R>;
84    }
85  : never;
86
87type DeepPartial<T> = {
88  [K in keyof T]?: T[K] extends Record<string | number, any>
89    ? T[K] extends ExceptionType
90      ? T[K]
91      : DeepPartial<T[K]>
92    : T[K];
93};
94
95type IConfigPaths = KeyPaths<IConfig>;
96type PartialConfig = DeepPartial<IConfig> | null;
97type IConfigPathsObj = KeyPathsObj<DeepPartial<IConfig>, IConfigPaths>;
98
99let config: PartialConfig = null;
100/** 初始化config */
101async function setup() {
102  config = (await getStorage('local-config')) ?? {};
103  // await checkValidPath(['setting.theme.background']);
104  notify();
105}
106
107/** 设置config */
108async function setConfig<T extends IConfigPaths>(
109  key: T,
110  value: IConfigPathsObj[T],
111  shouldNotify = true,
112) {
113  if (config === null) {
114    return;
115  }
116  const keys = key.split('.');
117
118  const result = produce(config, draft => {
119    draft[keys[0] as keyof IConfig] = draft[keys[0] as keyof IConfig] ?? {};
120    let conf: any = draft[keys[0] as keyof IConfig];
121    for (let i = 1; i < keys.length - 1; ++i) {
122      if (!conf?.[keys[i]]) {
123        conf[keys[i]] = {};
124      }
125      conf = conf[keys[i]];
126    }
127    conf[keys[keys.length - 1]] = value;
128    return draft;
129  });
130
131  setStorage('local-config', result);
132  config = result;
133  if (shouldNotify) {
134    notify();
135  }
136}
137
138/** 获取config */
139function getConfig(): PartialConfig;
140function getConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
141function getConfig(key?: string) {
142  let result: any = config;
143  if (key && config) {
144    result = getPathValue(config, key);
145  }
146
147  return result;
148}
149
150/** 通过path获取值 */
151function getPathValue(obj: Record<string, any>, path: string) {
152  const keys = path.split('.');
153  let tmp = obj;
154  for (let i = 0; i < keys.length; ++i) {
155    tmp = tmp?.[keys[i]];
156  }
157  return tmp;
158}
159
160/** 同步hook */
161const notifyCbs = new Set<() => void>();
162function notify() {
163  notifyCbs.forEach(_ => _?.());
164}
165
166/** hook */
167function useConfig(): PartialConfig;
168function useConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
169function useConfig(key?: string) {
170  const [_cfg, _setCfg] = useState<PartialConfig>(config);
171  function setCfg() {
172    _setCfg(config);
173  }
174  useEffect(() => {
175    notifyCbs.add(setCfg);
176    return () => {
177      notifyCbs.delete(setCfg);
178    };
179  }, []);
180
181  if (key) {
182    return _cfg ? getPathValue(_cfg, key) : undefined;
183  } else {
184    return _cfg;
185  }
186}
187
188const Config = {
189  get: getConfig,
190  set: setConfig,
191  useConfig,
192  setup,
193};
194
195export default Config;