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