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