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