xref: /MusicFree/src/core/config.ts (revision 6613e77203923e5b1742a49281bfa5de03fc1440)
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    status: {
108        music: {
109            /** 当前的音乐 */
110            track: IMusic.IMusicItem;
111            /** 进度 */
112            progress: number;
113            /** 模式 */
114            repeatMode: string;
115            /** 列表 */
116            musicQueue: IMusic.IMusicItem[];
117            /** 速度 */
118            rate: number;
119        };
120        app: {
121            /** 跳过特定版本 */
122            skipVersion: string;
123        };
124    };
125}
126
127type FilterType<T, R = never> = T extends Record<string | number, any>
128    ? {
129          [P in keyof T]: T[P] extends ExceptionType ? R : T[P];
130      }
131    : never;
132
133type KeyPaths<
134    T extends object,
135    Root extends boolean = true,
136    R = FilterType<T, ''>,
137    K extends keyof R = keyof R,
138> = K extends string | number
139    ?
140          | (Root extends true ? `${K}` : `.${K}`)
141          | (R[K] extends Record<string | number, any>
142                ? `${Root extends true ? `${K}` : `.${K}`}${KeyPaths<
143                      R[K],
144                      false
145                  >}`
146                : never)
147    : never;
148
149type KeyPathValue<T extends object, K extends string> = T extends Record<
150    string | number,
151    any
152>
153    ? K extends `${infer S}.${infer R}`
154        ? KeyPathValue<T[S], R>
155        : T[K]
156    : never;
157
158type KeyPathsObj<
159    T extends object,
160    K extends string = KeyPaths<T>,
161> = T extends Record<string | number, any>
162    ? {
163          [R in K]: KeyPathValue<T, R>;
164      }
165    : never;
166
167type DeepPartial<T> = {
168    [K in keyof T]?: T[K] extends Record<string | number, any>
169        ? T[K] extends ExceptionType
170            ? T[K]
171            : DeepPartial<T[K]>
172        : T[K];
173};
174
175export type IConfigPaths = KeyPaths<IConfig>;
176type PartialConfig = DeepPartial<IConfig> | null;
177type IConfigPathsObj = KeyPathsObj<DeepPartial<IConfig>, IConfigPaths>;
178
179let config: PartialConfig = null;
180/** 初始化config */
181async function setup() {
182    config = (await getStorage('local-config')) ?? {};
183    // await checkValidPath(['setting.theme.background']);
184    notify();
185}
186
187/** 设置config */
188async function setConfig<T extends IConfigPaths>(
189    key: T,
190    value: IConfigPathsObj[T],
191    shouldNotify = true,
192) {
193    if (config === null) {
194        return;
195    }
196    const keys = key.split('.');
197
198    const result = produce(config, draft => {
199        draft[keys[0] as keyof IConfig] = draft[keys[0] as keyof IConfig] ?? {};
200        let conf: any = draft[keys[0] as keyof IConfig];
201        for (let i = 1; i < keys.length - 1; ++i) {
202            if (!conf?.[keys[i]]) {
203                conf[keys[i]] = {};
204            }
205            conf = conf[keys[i]];
206        }
207        conf[keys[keys.length - 1]] = value;
208        return draft;
209    });
210
211    setStorage('local-config', result);
212    config = result;
213    if (shouldNotify) {
214        notify();
215    }
216}
217
218// todo: 获取兜底
219/** 获取config */
220function getConfig(): PartialConfig;
221function getConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
222function getConfig(key?: string) {
223    let result: any = config;
224    if (key && config) {
225        result = getPathValue(config, key);
226    }
227
228    return result;
229}
230
231/** 通过path获取值 */
232function getPathValue(obj: Record<string, any>, path: string) {
233    const keys = path.split('.');
234    let tmp = obj;
235    for (let i = 0; i < keys.length; ++i) {
236        tmp = tmp?.[keys[i]];
237    }
238    return tmp;
239}
240
241/** 同步hook */
242const notifyCbs = new Set<() => void>();
243function notify() {
244    notifyCbs.forEach(_ => _?.());
245}
246
247/** hook */
248function useConfig(): PartialConfig;
249function useConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T];
250function useConfig(key?: string) {
251    // TODO: 应该有性能损失
252    const [_cfg, _setCfg] = useState<PartialConfig>(config);
253    function setCfg() {
254        _setCfg(config);
255    }
256    useEffect(() => {
257        notifyCbs.add(setCfg);
258        return () => {
259            notifyCbs.delete(setCfg);
260        };
261    }, []);
262
263    if (key) {
264        return _cfg ? getPathValue(_cfg, key) : undefined;
265    } else {
266        return _cfg;
267    }
268}
269
270const Config = {
271    get: getConfig,
272    set: setConfig,
273    useConfig,
274    setup,
275};
276
277export default Config;
278