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