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