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