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