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