xref: /MusicFree/src/core/config.ts (revision cd669353b6a483aad2a61863c7df332c267907c6)
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        plugin: {
48            subscribeUrl: string;
49        };
50    };
51    status: {
52        music: {
53            /** 当前的音乐 */
54            track: IMusic.IMusicItem;
55            /** 进度 */
56            progress: number;
57            /** 模式 */
58            repeatMode: string;
59            /** 列表 */
60            musicQueue: IMusic.IMusicItem[];
61        };
62    };
63}
64
65type FilterType<T, R = never> = T extends Record<string | number, any>
66    ? {
67          [P in keyof T]: T[P] extends ExceptionType ? R : T[P];
68      }
69    : never;
70
71type KeyPaths<
72    T extends object,
73    Root extends boolean = true,
74    R = FilterType<T, ''>,
75    K extends keyof R = keyof R,
76> = K extends string | number
77    ?
78          | (Root extends true ? `${K}` : `.${K}`)
79          | (R[K] extends Record<string | number, any>
80                ? `${Root extends true ? `${K}` : `.${K}`}${KeyPaths<
81                      R[K],
82                      false
83                  >}`
84                : never)
85    : never;
86
87type KeyPathValue<T extends object, K extends string> = T extends Record<
88    string | number,
89    any
90>
91    ? K extends `${infer S}.${infer R}`
92        ? KeyPathValue<T[S], R>
93        : T[K]
94    : never;
95
96type KeyPathsObj<
97    T extends object,
98    K extends string = KeyPaths<T>,
99> = T extends Record<string | number, any>
100    ? {
101          [R in K]: KeyPathValue<T, R>;
102      }
103    : never;
104
105type DeepPartial<T> = {
106    [K in keyof T]?: T[K] extends Record<string | number, any>
107        ? T[K] extends ExceptionType
108            ? T[K]
109            : DeepPartial<T[K]>
110        : T[K];
111};
112
113export type IConfigPaths = KeyPaths<IConfig>;
114type PartialConfig = DeepPartial<IConfig> | null;
115type IConfigPathsObj = KeyPathsObj<DeepPartial<IConfig>, IConfigPaths>;
116
117let config: PartialConfig = null;
118/** 初始化config */
119async function setup() {
120    config = (await getStorage('local-config')) ?? {};
121    // await checkValidPath(['setting.theme.background']);
122    notify();
123}
124
125/** 设置config */
126async function setConfig<T extends IConfigPaths>(
127    key: T,
128    value: IConfigPathsObj[T],
129    shouldNotify = true,
130) {
131    if (config === null) {
132        return;
133    }
134    const keys = key.split('.');
135
136    const result = produce(config, draft => {
137        draft[keys[0] as keyof IConfig] = draft[keys[0] as keyof IConfig] ?? {};
138        let conf: any = draft[keys[0] as keyof IConfig];
139        for (let i = 1; i < keys.length - 1; ++i) {
140            if (!conf?.[keys[i]]) {
141                conf[keys[i]] = {};
142            }
143            conf = conf[keys[i]];
144        }
145        conf[keys[keys.length - 1]] = value;
146        return draft;
147    });
148
149    setStorage('local-config', result);
150    config = result;
151    if (shouldNotify) {
152        notify();
153    }
154}
155
156/** 获取config */
157function getConfig(): PartialConfig;
158function getConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
159function getConfig(key?: string) {
160    let result: any = config;
161    if (key && config) {
162        result = getPathValue(config, key);
163    }
164
165    return result;
166}
167
168/** 通过path获取值 */
169function getPathValue(obj: Record<string, any>, path: string) {
170    const keys = path.split('.');
171    let tmp = obj;
172    for (let i = 0; i < keys.length; ++i) {
173        tmp = tmp?.[keys[i]];
174    }
175    return tmp;
176}
177
178/** 同步hook */
179const notifyCbs = new Set<() => void>();
180function notify() {
181    notifyCbs.forEach(_ => _?.());
182}
183
184/** hook */
185function useConfig(): PartialConfig;
186function useConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
187function useConfig(key?: string) {
188    const [_cfg, _setCfg] = useState<PartialConfig>(config);
189    function setCfg() {
190        _setCfg(config);
191    }
192    useEffect(() => {
193        notifyCbs.add(setCfg);
194        return () => {
195            notifyCbs.delete(setCfg);
196        };
197    }, []);
198
199    if (key) {
200        return _cfg ? getPathValue(_cfg, key) : undefined;
201    } else {
202        return _cfg;
203    }
204}
205
206const Config = {
207    get: getConfig,
208    set: setConfig,
209    useConfig,
210    setup,
211};
212
213export default Config;
214