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