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