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