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