xref: /MusicFree/src/core/config.ts (revision 410a159129b1f6a7a1f44fde7bfad9a46f91e161)
1// import {Quality} from '@/constants/commonConst';
2import {CustomizedColors} from '@/hooks/useColors';
3import {getStorage, setStorage} from '@/utils/storage';
4import produce from 'immer';
5import {useEffect, useState} from 'react';
6
7type ExceptionType = IMusic.IMusicItem | IMusic.IMusicItem[] | IMusic.IQuality;
8interface IConfig {
9    setting: {
10        basic: {
11            autoPlayWhenAppStart: boolean;
12            /** 使用移动网络播放 */
13            useCelluarNetworkPlay: boolean;
14            /** 使用移动网络下载 */
15            useCelluarNetworkDownload: boolean;
16            /** 最大同时下载 */
17            maxDownload: number | string;
18            /** 播放歌曲行为 */
19            clickMusicInSearch: '播放歌曲' | '播放歌曲并替换播放列表';
20            /** 点击专辑单曲 */
21            clickMusicInAlbum: '播放专辑' | '播放单曲';
22            /** 下载文件夹 */
23            downloadPath: string;
24            /** 同时播放 */
25            notInterrupt: boolean;
26            /** 打断时 */
27            tempRemoteDuck: '暂停' | '降低音量';
28            /** 播放错误时自动停止 */
29            autoStopWhenError: boolean;
30            /** 插件缓存策略 todo */
31            pluginCacheControl: string;
32            /** 最大音乐缓存 */
33            maxCacheSize: number;
34            /** 默认播放音质 */
35            defaultPlayQuality: IMusic.IQualityKey;
36            /** 音质顺序 */
37            playQualityOrder: 'asc' | 'desc';
38            /** 默认下载音质 */
39            defaultDownloadQuality: IMusic.IQualityKey;
40            /** 下载音质顺序 */
41            downloadQualityOrder: 'asc' | 'desc';
42            /** 歌曲详情页 */
43            musicDetailDefault: 'album' | 'lyric';
44            /** 歌曲详情页常亮 */
45            musicDetailAwake: boolean;
46            debug: {
47                errorLog: boolean;
48                traceLog: boolean;
49                devLog: boolean;
50            };
51            /** 最大历史记录条目 */
52            maxHistoryLen: number;
53            /** 启动时自动更新插件 */
54            autoUpdatePlugin: boolean;
55            // 不检查插件版本号
56            notCheckPluginVersion: boolean;
57            /** 关联歌词方式 */
58            associateLyricType: 'input' | 'search';
59            // 是否展示退出按钮
60            showExitOnNotification: boolean;
61            // 本地歌单添加歌曲顺序
62            musicOrderInLocalSheet: 'start' | 'end';
63            // 自动换源
64            tryChangeSourceWhenPlayFail: boolean;
65        };
66        /** 歌词 */
67        lyric: {
68            showStatusBarLyric: boolean;
69            topPercent: number;
70            leftPercent: number;
71            align: number;
72            color: string;
73            backgroundColor: string;
74            widthPercent: number;
75            fontSize: number;
76            // 详情页的字体大小
77            detailFontSize: number;
78            // 自动搜索歌词
79            autoSearchLyric: boolean;
80        };
81
82        /** 主题 */
83        theme: {
84            background: string;
85            backgroundOpacity: number;
86            backgroundBlur: number;
87            colors: CustomizedColors;
88            customColors?: CustomizedColors;
89            followSystem: boolean;
90            selectedTheme: string;
91        };
92
93        backup: {
94            resumeMode: 'append' | 'overwrite';
95        };
96
97        plugin: {
98            subscribeUrl: string;
99        };
100        webdav: {
101            url: string;
102            username: string;
103            password: string;
104        };
105    };
106    status: {
107        music: {
108            /** 当前的音乐 */
109            track: IMusic.IMusicItem;
110            /** 进度 */
111            progress: number;
112            /** 模式 */
113            repeatMode: string;
114            /** 列表 */
115            musicQueue: IMusic.IMusicItem[];
116            /** 速度 */
117            rate: number;
118        };
119        app: {
120            /** 跳过特定版本 */
121            skipVersion: string;
122        };
123    };
124}
125
126type FilterType<T, R = never> = T extends Record<string | number, any>
127    ? {
128          [P in keyof T]: T[P] extends ExceptionType ? R : T[P];
129      }
130    : never;
131
132type KeyPaths<
133    T extends object,
134    Root extends boolean = true,
135    R = FilterType<T, ''>,
136    K extends keyof R = keyof R,
137> = K extends string | number
138    ?
139          | (Root extends true ? `${K}` : `.${K}`)
140          | (R[K] extends Record<string | number, any>
141                ? `${Root extends true ? `${K}` : `.${K}`}${KeyPaths<
142                      R[K],
143                      false
144                  >}`
145                : never)
146    : never;
147
148type KeyPathValue<T extends object, K extends string> = T extends Record<
149    string | number,
150    any
151>
152    ? K extends `${infer S}.${infer R}`
153        ? KeyPathValue<T[S], R>
154        : T[K]
155    : never;
156
157type KeyPathsObj<
158    T extends object,
159    K extends string = KeyPaths<T>,
160> = T extends Record<string | number, any>
161    ? {
162          [R in K]: KeyPathValue<T, R>;
163      }
164    : never;
165
166type DeepPartial<T> = {
167    [K in keyof T]?: T[K] extends Record<string | number, any>
168        ? T[K] extends ExceptionType
169            ? T[K]
170            : DeepPartial<T[K]>
171        : T[K];
172};
173
174export type IConfigPaths = KeyPaths<IConfig>;
175type PartialConfig = DeepPartial<IConfig> | null;
176type IConfigPathsObj = KeyPathsObj<DeepPartial<IConfig>, IConfigPaths>;
177
178let config: PartialConfig = null;
179/** 初始化config */
180async function setup() {
181    config = (await getStorage('local-config')) ?? {};
182    // await checkValidPath(['setting.theme.background']);
183    notify();
184}
185
186/** 设置config */
187async function setConfig<T extends IConfigPaths>(
188    key: T,
189    value: IConfigPathsObj[T],
190    shouldNotify = true,
191) {
192    if (config === null) {
193        return;
194    }
195    const keys = key.split('.');
196
197    const result = produce(config, draft => {
198        draft[keys[0] as keyof IConfig] = draft[keys[0] as keyof IConfig] ?? {};
199        let conf: any = draft[keys[0] as keyof IConfig];
200        for (let i = 1; i < keys.length - 1; ++i) {
201            if (!conf?.[keys[i]]) {
202                conf[keys[i]] = {};
203            }
204            conf = conf[keys[i]];
205        }
206        conf[keys[keys.length - 1]] = value;
207        return draft;
208    });
209
210    setStorage('local-config', result);
211    config = result;
212    if (shouldNotify) {
213        notify();
214    }
215}
216
217// todo: 获取兜底
218/** 获取config */
219function getConfig(): PartialConfig;
220function getConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
221function getConfig(key?: string) {
222    let result: any = config;
223    if (key && config) {
224        result = getPathValue(config, key);
225    }
226
227    return result;
228}
229
230/** 通过path获取值 */
231function getPathValue(obj: Record<string, any>, path: string) {
232    const keys = path.split('.');
233    let tmp = obj;
234    for (let i = 0; i < keys.length; ++i) {
235        tmp = tmp?.[keys[i]];
236    }
237    return tmp;
238}
239
240/** 同步hook */
241const notifyCbs = new Set<() => void>();
242function notify() {
243    notifyCbs.forEach(_ => _?.());
244}
245
246/** hook */
247function useConfig(): PartialConfig;
248function useConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
249function useConfig(key?: string) {
250    // TODO: 应该有性能损失
251    const [_cfg, _setCfg] = useState<PartialConfig>(config);
252    function setCfg() {
253        _setCfg(config);
254    }
255    useEffect(() => {
256        notifyCbs.add(setCfg);
257        return () => {
258            notifyCbs.delete(setCfg);
259        };
260    }, []);
261
262    if (key) {
263        return _cfg ? getPathValue(_cfg, key) : undefined;
264    } else {
265        return _cfg;
266    }
267}
268
269const Config = {
270    get: getConfig,
271    set: setConfig,
272    useConfig,
273    setup,
274};
275
276export default Config;
277