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