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