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