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 /** 歌词 */ 59 lyric: { 60 showStatusBarLyric: boolean; 61 topPercent: number; 62 leftPercent: number; 63 align: number; 64 color: string; 65 backgroundColor: string; 66 widthPercent: number; 67 fontSize: number; 68 }; 69 70 /** 主题 */ 71 theme: { 72 background: string; 73 backgroundOpacity: number; 74 backgroundBlur: number; 75 colors: CustomizedColors; 76 followSystem: boolean; 77 selectedTheme: string; 78 }; 79 80 plugin: { 81 subscribeUrl: string; 82 }; 83 }; 84 status: { 85 music: { 86 /** 当前的音乐 */ 87 track: IMusic.IMusicItem; 88 /** 进度 */ 89 progress: number; 90 /** 模式 */ 91 repeatMode: string; 92 /** 列表 */ 93 musicQueue: IMusic.IMusicItem[]; 94 /** 速度 */ 95 rate: number; 96 }; 97 app: { 98 /** 跳过特定版本 */ 99 skipVersion: string; 100 }; 101 }; 102} 103 104type FilterType<T, R = never> = T extends Record<string | number, any> 105 ? { 106 [P in keyof T]: T[P] extends ExceptionType ? R : T[P]; 107 } 108 : never; 109 110type KeyPaths< 111 T extends object, 112 Root extends boolean = true, 113 R = FilterType<T, ''>, 114 K extends keyof R = keyof R, 115> = K extends string | number 116 ? 117 | (Root extends true ? `${K}` : `.${K}`) 118 | (R[K] extends Record<string | number, any> 119 ? `${Root extends true ? `${K}` : `.${K}`}${KeyPaths< 120 R[K], 121 false 122 >}` 123 : never) 124 : never; 125 126type KeyPathValue<T extends object, K extends string> = T extends Record< 127 string | number, 128 any 129> 130 ? K extends `${infer S}.${infer R}` 131 ? KeyPathValue<T[S], R> 132 : T[K] 133 : never; 134 135type KeyPathsObj< 136 T extends object, 137 K extends string = KeyPaths<T>, 138> = T extends Record<string | number, any> 139 ? { 140 [R in K]: KeyPathValue<T, R>; 141 } 142 : never; 143 144type DeepPartial<T> = { 145 [K in keyof T]?: T[K] extends Record<string | number, any> 146 ? T[K] extends ExceptionType 147 ? T[K] 148 : DeepPartial<T[K]> 149 : T[K]; 150}; 151 152export type IConfigPaths = KeyPaths<IConfig>; 153type PartialConfig = DeepPartial<IConfig> | null; 154type IConfigPathsObj = KeyPathsObj<DeepPartial<IConfig>, IConfigPaths>; 155 156let config: PartialConfig = null; 157/** 初始化config */ 158async function setup() { 159 config = (await getStorage('local-config')) ?? {}; 160 // await checkValidPath(['setting.theme.background']); 161 notify(); 162} 163 164/** 设置config */ 165async function setConfig<T extends IConfigPaths>( 166 key: T, 167 value: IConfigPathsObj[T], 168 shouldNotify = true, 169) { 170 if (config === null) { 171 return; 172 } 173 const keys = key.split('.'); 174 175 const result = produce(config, draft => { 176 draft[keys[0] as keyof IConfig] = draft[keys[0] as keyof IConfig] ?? {}; 177 let conf: any = draft[keys[0] as keyof IConfig]; 178 for (let i = 1; i < keys.length - 1; ++i) { 179 if (!conf?.[keys[i]]) { 180 conf[keys[i]] = {}; 181 } 182 conf = conf[keys[i]]; 183 } 184 conf[keys[keys.length - 1]] = value; 185 return draft; 186 }); 187 188 setStorage('local-config', result); 189 config = result; 190 if (shouldNotify) { 191 notify(); 192 } 193} 194 195// todo: 获取兜底 196/** 获取config */ 197function getConfig(): PartialConfig; 198function getConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T]; 199function getConfig(key?: string) { 200 let result: any = config; 201 if (key && config) { 202 result = getPathValue(config, key); 203 } 204 205 return result; 206} 207 208/** 通过path获取值 */ 209function getPathValue(obj: Record<string, any>, path: string) { 210 const keys = path.split('.'); 211 let tmp = obj; 212 for (let i = 0; i < keys.length; ++i) { 213 tmp = tmp?.[keys[i]]; 214 } 215 return tmp; 216} 217 218/** 同步hook */ 219const notifyCbs = new Set<() => void>(); 220function notify() { 221 notifyCbs.forEach(_ => _?.()); 222} 223 224/** hook */ 225function useConfig(): PartialConfig; 226function useConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T]; 227function useConfig(key?: string) { 228 // TODO: 应该有性能损失 229 const [_cfg, _setCfg] = useState<PartialConfig>(config); 230 function setCfg() { 231 _setCfg(config); 232 } 233 useEffect(() => { 234 notifyCbs.add(setCfg); 235 return () => { 236 notifyCbs.delete(setCfg); 237 }; 238 }, []); 239 240 if (key) { 241 return _cfg ? getPathValue(_cfg, key) : undefined; 242 } else { 243 return _cfg; 244 } 245} 246 247const Config = { 248 get: getConfig, 249 set: setConfig, 250 useConfig, 251 setup, 252}; 253 254export default Config; 255