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