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