1import {getStorage, setStorage} from '@/utils/storage'; 2import produce from 'immer'; 3import {useEffect, useState} from 'react'; 4 5type ExceptionType = IMusic.IMusicItem | IMusic.IMusicItem[]; 6interface IConfig { 7 setting: { 8 basic: { 9 /** 最大同时下载 */ 10 maxDownload: number | string; 11 /** 同时播放 */ 12 notInterrupt: boolean; 13 /** 播放错误时自动停止 */ 14 autoStopWhenError: boolean; 15 16 debug: { 17 errorLog: boolean; 18 traceLog: boolean; 19 }; 20 }; 21 22 /** 主题 */ 23 theme: { 24 mode: 'light' | 'dark' | 'custom-light' | 'custom-dark'; 25 background: string; 26 backgroundOpacity: number; 27 backgroundBlur: number; 28 colors: { 29 primary: string; 30 secondary: string; 31 textHighlight: string; 32 pageBackground: string; 33 }; 34 }; 35 }; 36 status: { 37 music: { 38 /** 当前的音乐 */ 39 track: IMusic.IMusicItem; 40 /** 进度 */ 41 progress: number; 42 /** 模式 */ 43 repeatMode: string; 44 /** 列表 */ 45 musicQueue: IMusic.IMusicItem[]; 46 }; 47 }; 48} 49 50type FilterType<T, R = never> = T extends Record<string | number, any> 51 ? { 52 [P in keyof T]: T[P] extends ExceptionType ? R : T[P]; 53 } 54 : never; 55 56type KeyPaths< 57 T extends object, 58 Root extends boolean = true, 59 R = FilterType<T, ''>, 60 K extends keyof R = keyof R, 61> = K extends string | number 62 ? 63 | (Root extends true ? `${K}` : `.${K}`) 64 | (R[K] extends Record<string | number, any> 65 ? `${Root extends true ? `${K}` : `.${K}`}${KeyPaths<R[K], false>}` 66 : never) 67 : never; 68 69type KeyPathValue<T extends object, K extends string> = T extends Record< 70 string | number, 71 any 72> 73 ? K extends `${infer S}.${infer R}` 74 ? KeyPathValue<T[S], R> 75 : T[K] 76 : never; 77 78type KeyPathsObj< 79 T extends object, 80 K extends string = KeyPaths<T>, 81> = T extends Record<string | number, any> 82 ? { 83 [R in K]: KeyPathValue<T, R>; 84 } 85 : never; 86 87type DeepPartial<T> = { 88 [K in keyof T]?: T[K] extends Record<string | number, any> 89 ? T[K] extends ExceptionType 90 ? T[K] 91 : DeepPartial<T[K]> 92 : T[K]; 93}; 94 95type IConfigPaths = KeyPaths<IConfig>; 96type PartialConfig = DeepPartial<IConfig> | null; 97type IConfigPathsObj = KeyPathsObj<DeepPartial<IConfig>, IConfigPaths>; 98 99let config: PartialConfig = null; 100/** 初始化config */ 101async function setup() { 102 config = (await getStorage('local-config')) ?? {}; 103 // await checkValidPath(['setting.theme.background']); 104 notify(); 105} 106 107/** 设置config */ 108async function setConfig<T extends IConfigPaths>( 109 key: T, 110 value: IConfigPathsObj[T], 111 shouldNotify = true, 112) { 113 if (config === null) { 114 return; 115 } 116 const keys = key.split('.'); 117 118 const result = produce(config, draft => { 119 draft[keys[0] as keyof IConfig] = draft[keys[0] as keyof IConfig] ?? {}; 120 let conf: any = draft[keys[0] as keyof IConfig]; 121 for (let i = 1; i < keys.length - 1; ++i) { 122 if (!conf?.[keys[i]]) { 123 conf[keys[i]] = {}; 124 } 125 conf = conf[keys[i]]; 126 } 127 conf[keys[keys.length - 1]] = value; 128 return draft; 129 }); 130 131 setStorage('local-config', result); 132 config = result; 133 if (shouldNotify) { 134 notify(); 135 } 136} 137 138/** 获取config */ 139function getConfig(): PartialConfig; 140function getConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T]; 141function getConfig(key?: string) { 142 let result: any = config; 143 if (key && config) { 144 result = getPathValue(config, key); 145 } 146 147 return result; 148} 149 150/** 通过path获取值 */ 151function getPathValue(obj: Record<string, any>, path: string) { 152 const keys = path.split('.'); 153 let tmp = obj; 154 for (let i = 0; i < keys.length; ++i) { 155 tmp = tmp?.[keys[i]]; 156 } 157 return tmp; 158} 159 160/** 同步hook */ 161const notifyCbs = new Set<() => void>(); 162function notify() { 163 notifyCbs.forEach(_ => _?.()); 164} 165 166/** hook */ 167function useConfig(): PartialConfig; 168function useConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T]; 169function useConfig(key?: string) { 170 const [_cfg, _setCfg] = useState<PartialConfig>(config); 171 function setCfg() { 172 _setCfg(config); 173 } 174 useEffect(() => { 175 notifyCbs.add(setCfg); 176 return () => { 177 notifyCbs.delete(setCfg); 178 }; 179 }, []); 180 181 if (key) { 182 return _cfg ? getPathValue(_cfg, key) : undefined; 183 } else { 184 return _cfg; 185 } 186} 187 188const Config = { 189 get: getConfig, 190 set: setConfig, 191 useConfig, 192 setup, 193}; 194 195export default Config;