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