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