xref: /MusicFree/src/core/config.ts (revision 9c72192f09e3e71b70c1fb446447ba438c4fffbd)
1// import {Quality} from '@/constants/commonConst';
2import {CustomizedColors} from '@/hooks/useColors';
3import {getStorage, setStorage} from '@/utils/storage';
4import produce from 'immer';
5import {useEffect, useState} from 'react';
6
7type ExceptionType = IMusic.IMusicItem | IMusic.IMusicItem[] | IMusic.IQuality;
8interface IConfig {
9    setting: {
10        basic: {
11            autoPlayWhenAppStart: boolean;
12            /** 使用移动网络播放 */
13            useCelluarNetworkPlay: boolean;
14            /** 使用移动网络下载 */
15            useCelluarNetworkDownload: boolean;
16            /** 最大同时下载 */
17            maxDownload: number | string;
18            /** 播放歌曲行为 */
19            clickMusicInSearch: '播放歌曲' | '播放歌曲并替换播放列表';
20            /** 点击专辑单曲 */
21            clickMusicInAlbum: '播放专辑' | '播放单曲';
22            /** 下载文件夹 */
23            downloadPath: string;
24            /** 同时播放 */
25            notInterrupt: boolean;
26            /** 打断时 */
27            tempRemoteDuck: '暂停' | '降低音量';
28            /** 播放错误时自动停止 */
29            autoStopWhenError: boolean;
30            /** 插件缓存策略 todo */
31            pluginCacheControl: string;
32            /** 最大音乐缓存 */
33            maxCacheSize: number;
34            /** 默认播放音质 */
35            defaultPlayQuality: IMusic.IQualityKey;
36            /** 音质顺序 */
37            playQualityOrder: 'asc' | 'desc';
38            /** 默认下载音质 */
39            defaultDownloadQuality: IMusic.IQualityKey;
40            /** 下载音质顺序 */
41            downloadQualityOrder: 'asc' | 'desc';
42            /** 歌曲详情页 */
43            musicDetailDefault: 'album' | 'lyric';
44            /** 歌曲详情页常亮 */
45            musicDetailAwake: boolean;
46            debug: {
47                errorLog: boolean;
48                traceLog: boolean;
49                devLog: boolean;
50            };
51            /** 最大历史记录条目 */
52            maxHistoryLen: number;
53            /** 启动时自动更新插件 */
54            autoUpdatePlugin: boolean;
55        };
56        /** 歌词 */
57        lyric: {
58            showStatusBarLyric: boolean;
59            topPercent: number;
60            leftPercent: number;
61            align: number;
62            color: string;
63            backgroundColor: string;
64            widthPercent: number;
65            fontSize: number;
66        };
67
68        /** 主题 */
69        theme: {
70            background: string;
71            backgroundOpacity: number;
72            backgroundBlur: number;
73            colors: CustomizedColors;
74            followSystem: boolean;
75            selectedTheme: string;
76        };
77
78        plugin: {
79            subscribeUrl: string;
80        };
81    };
82    status: {
83        music: {
84            /** 当前的音乐 */
85            track: IMusic.IMusicItem;
86            /** 进度 */
87            progress: number;
88            /** 模式 */
89            repeatMode: string;
90            /** 列表 */
91            musicQueue: IMusic.IMusicItem[];
92            /** 速度 */
93            rate: number;
94        };
95        app: {
96            /** 跳过特定版本 */
97            skipVersion: string;
98        };
99    };
100}
101
102type FilterType<T, R = never> = T extends Record<string | number, any>
103    ? {
104          [P in keyof T]: T[P] extends ExceptionType ? R : T[P];
105      }
106    : never;
107
108type KeyPaths<
109    T extends object,
110    Root extends boolean = true,
111    R = FilterType<T, ''>,
112    K extends keyof R = keyof R,
113> = K extends string | number
114    ?
115          | (Root extends true ? `${K}` : `.${K}`)
116          | (R[K] extends Record<string | number, any>
117                ? `${Root extends true ? `${K}` : `.${K}`}${KeyPaths<
118                      R[K],
119                      false
120                  >}`
121                : never)
122    : never;
123
124type KeyPathValue<T extends object, K extends string> = T extends Record<
125    string | number,
126    any
127>
128    ? K extends `${infer S}.${infer R}`
129        ? KeyPathValue<T[S], R>
130        : T[K]
131    : never;
132
133type KeyPathsObj<
134    T extends object,
135    K extends string = KeyPaths<T>,
136> = T extends Record<string | number, any>
137    ? {
138          [R in K]: KeyPathValue<T, R>;
139      }
140    : never;
141
142type DeepPartial<T> = {
143    [K in keyof T]?: T[K] extends Record<string | number, any>
144        ? T[K] extends ExceptionType
145            ? T[K]
146            : DeepPartial<T[K]>
147        : T[K];
148};
149
150export type IConfigPaths = KeyPaths<IConfig>;
151type PartialConfig = DeepPartial<IConfig> | null;
152type IConfigPathsObj = KeyPathsObj<DeepPartial<IConfig>, IConfigPaths>;
153
154let config: PartialConfig = null;
155/** 初始化config */
156async function setup() {
157    config = (await getStorage('local-config')) ?? {};
158    // await checkValidPath(['setting.theme.background']);
159    notify();
160}
161
162/** 设置config */
163async function setConfig<T extends IConfigPaths>(
164    key: T,
165    value: IConfigPathsObj[T],
166    shouldNotify = true,
167) {
168    if (config === null) {
169        return;
170    }
171    const keys = key.split('.');
172
173    const result = produce(config, draft => {
174        draft[keys[0] as keyof IConfig] = draft[keys[0] as keyof IConfig] ?? {};
175        let conf: any = draft[keys[0] as keyof IConfig];
176        for (let i = 1; i < keys.length - 1; ++i) {
177            if (!conf?.[keys[i]]) {
178                conf[keys[i]] = {};
179            }
180            conf = conf[keys[i]];
181        }
182        conf[keys[keys.length - 1]] = value;
183        return draft;
184    });
185
186    setStorage('local-config', result);
187    config = result;
188    if (shouldNotify) {
189        notify();
190    }
191}
192
193// todo: 获取兜底
194/** 获取config */
195function getConfig(): PartialConfig;
196function getConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
197function getConfig(key?: string) {
198    let result: any = config;
199    if (key && config) {
200        result = getPathValue(config, key);
201    }
202
203    return result;
204}
205
206/** 通过path获取值 */
207function getPathValue(obj: Record<string, any>, path: string) {
208    const keys = path.split('.');
209    let tmp = obj;
210    for (let i = 0; i < keys.length; ++i) {
211        tmp = tmp?.[keys[i]];
212    }
213    return tmp;
214}
215
216/** 同步hook */
217const notifyCbs = new Set<() => void>();
218function notify() {
219    notifyCbs.forEach(_ => _?.());
220}
221
222/** hook */
223function useConfig(): PartialConfig;
224function useConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
225function useConfig(key?: string) {
226    // TODO: 应该有性能损失
227    const [_cfg, _setCfg] = useState<PartialConfig>(config);
228    function setCfg() {
229        _setCfg(config);
230    }
231    useEffect(() => {
232        notifyCbs.add(setCfg);
233        return () => {
234            notifyCbs.delete(setCfg);
235        };
236    }, []);
237
238    if (key) {
239        return _cfg ? getPathValue(_cfg, key) : undefined;
240    } else {
241        return _cfg;
242    }
243}
244
245const Config = {
246    get: getConfig,
247    set: setConfig,
248    useConfig,
249    setup,
250};
251
252export default Config;
253