1// import {Quality} from '@/constants/commonConst'; 2import { CustomizedColors } from "@/hooks/useColors"; 3import { getStorage, setStorage } from "@/utils/storage"; 4import { produce } from "immer"; 5import { useEffect, useState } from "react"; 6import { ResumeMode, SortType } from "@/constants/commonConst.ts"; 7 8type ExceptionType = IMusic.IMusicItem | IMusic.IMusicItem[] | IMusic.IQuality; 9interface IConfig { 10 setting: { 11 basic: { 12 autoPlayWhenAppStart: boolean; 13 /** 使用移动网络播放 */ 14 useCelluarNetworkPlay: boolean; 15 /** 使用移动网络下载 */ 16 useCelluarNetworkDownload: boolean; 17 /** 最大同时下载 */ 18 maxDownload: number | string; 19 /** 播放歌曲行为 */ 20 clickMusicInSearch: '播放歌曲' | '播放歌曲并替换播放列表'; 21 /** 点击专辑单曲 */ 22 clickMusicInAlbum: '播放专辑' | '播放单曲'; 23 /** 下载文件夹 */ 24 downloadPath: string; 25 /** 同时播放 */ 26 notInterrupt: boolean; 27 /** 打断时 */ 28 tempRemoteDuck: '暂停' | '降低音量'; 29 /** 播放错误时自动停止 */ 30 autoStopWhenError: boolean; 31 /** 插件缓存策略 todo */ 32 pluginCacheControl: string; 33 /** 最大音乐缓存 */ 34 maxCacheSize: number; 35 /** 默认播放音质 */ 36 defaultPlayQuality: IMusic.IQualityKey; 37 /** 音质顺序 */ 38 playQualityOrder: 'asc' | 'desc'; 39 /** 默认下载音质 */ 40 defaultDownloadQuality: IMusic.IQualityKey; 41 /** 下载音质顺序 */ 42 downloadQualityOrder: 'asc' | 'desc'; 43 /** 歌曲详情页 */ 44 musicDetailDefault: 'album' | 'lyric'; 45 /** 歌曲详情页常亮 */ 46 musicDetailAwake: boolean; 47 debug: { 48 errorLog: boolean; 49 traceLog: boolean; 50 devLog: boolean; 51 }; 52 /** 最大历史记录条目 */ 53 maxHistoryLen: number; 54 /** 启动时自动更新插件 */ 55 autoUpdatePlugin: boolean; 56 // 不检查插件版本号 57 notCheckPluginVersion: boolean; 58 /** 关联歌词方式 */ 59 associateLyricType: 'input' | 'search'; 60 // 是否展示退出按钮 61 showExitOnNotification: boolean; 62 // 本地歌单添加歌曲顺序 63 musicOrderInLocalSheet: SortType; 64 // 自动换源 65 tryChangeSourceWhenPlayFail: boolean; 66 }; 67 /** 歌词 */ 68 lyric: { 69 showStatusBarLyric: boolean; 70 topPercent: number; 71 leftPercent: number; 72 align: number; 73 color: string; 74 backgroundColor: string; 75 widthPercent: number; 76 fontSize: number; 77 // 详情页的字体大小 78 detailFontSize: number; 79 // 自动搜索歌词 80 autoSearchLyric: boolean; 81 }; 82 83 /** 主题 */ 84 theme: { 85 background: string; 86 backgroundOpacity: number; 87 backgroundBlur: number; 88 colors: CustomizedColors; 89 customColors?: CustomizedColors; 90 followSystem: boolean; 91 selectedTheme: string; 92 }; 93 94 backup: { 95 resumeMode: ResumeMode; 96 }; 97 98 plugin: { 99 subscribeUrl: string; 100 }; 101 webdav: { 102 url: string; 103 username: string; 104 password: string; 105 }; 106 }; 107} 108 109type FilterType<T, R = never> = T extends Record<string | number, any> 110 ? { 111 [P in keyof T]: T[P] extends ExceptionType ? R : T[P]; 112 } 113 : never; 114 115type KeyPaths< 116 T extends object, 117 Root extends boolean = true, 118 R = FilterType<T, ''>, 119 K extends keyof R = keyof R, 120> = K extends string | number 121 ? 122 | (Root extends true ? `${K}` : `.${K}`) 123 | (R[K] extends Record<string | number, any> 124 ? `${Root extends true ? `${K}` : `.${K}`}${KeyPaths< 125 R[K], 126 false 127 >}` 128 : never) 129 : never; 130 131type KeyPathValue<T extends object, K extends string> = T extends Record< 132 string | number, 133 any 134> 135 ? K extends `${infer S}.${infer R}` 136 ? KeyPathValue<T[S], R> 137 : T[K] 138 : never; 139 140type KeyPathsObj< 141 T extends object, 142 K extends string = KeyPaths<T>, 143> = T extends Record<string | number, any> 144 ? { 145 [R in K]: KeyPathValue<T, R>; 146 } 147 : never; 148 149type DeepPartial<T> = { 150 [K in keyof T]?: T[K] extends Record<string | number, any> 151 ? T[K] extends ExceptionType 152 ? T[K] 153 : DeepPartial<T[K]> 154 : T[K]; 155}; 156 157export type IConfigPaths = KeyPaths<IConfig>; 158type PartialConfig = DeepPartial<IConfig> | null; 159type IConfigPathsObj = KeyPathsObj<DeepPartial<IConfig>, IConfigPaths>; 160 161let config: PartialConfig = null; 162/** 初始化config */ 163async function setup() { 164 config = (await getStorage('local-config')) ?? {}; 165 // await checkValidPath(['setting.theme.background']); 166 notify(); 167} 168 169/** 设置config */ 170async function setConfig<T extends IConfigPaths>( 171 key: T, 172 value: IConfigPathsObj[T], 173 shouldNotify = true, 174) { 175 if (config === null) { 176 return; 177 } 178 const keys = key.split('.'); 179 180 const result = produce(config, draft => { 181 draft[keys[0] as keyof IConfig] = draft[keys[0] as keyof IConfig] ?? {}; 182 let conf: any = draft[keys[0] as keyof IConfig]; 183 for (let i = 1; i < keys.length - 1; ++i) { 184 if (!conf?.[keys[i]]) { 185 conf[keys[i]] = {}; 186 } 187 conf = conf[keys[i]]; 188 } 189 conf[keys[keys.length - 1]] = value; 190 return draft; 191 }); 192 193 setStorage('local-config', result); 194 config = result; 195 if (shouldNotify) { 196 notify(); 197 } 198} 199 200// todo: 获取兜底 201/** 获取config */ 202function getConfig(): PartialConfig; 203function getConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T]; 204function getConfig(key?: string) { 205 let result: any = config; 206 if (key && config) { 207 result = getPathValue(config, key); 208 } 209 210 return result; 211} 212 213/** 通过path获取值 */ 214function getPathValue(obj: Record<string, any>, path: string) { 215 const keys = path.split('.'); 216 let tmp = obj; 217 for (let i = 0; i < keys.length; ++i) { 218 tmp = tmp?.[keys[i]]; 219 } 220 return tmp; 221} 222 223/** 同步hook */ 224const notifyCbs = new Set<() => void>(); 225function notify() { 226 notifyCbs.forEach(_ => _?.()); 227} 228 229/** hook */ 230function useConfig(): PartialConfig; 231function useConfig<T extends IConfigPaths>(key: T): IConfigPathsObj[T]; 232function useConfig(key?: string) { 233 // TODO: 应该有性能损失 234 const [_cfg, _setCfg] = useState<PartialConfig>(config); 235 function setCfg() { 236 _setCfg(config); 237 } 238 useEffect(() => { 239 notifyCbs.add(setCfg); 240 return () => { 241 notifyCbs.delete(setCfg); 242 }; 243 }, []); 244 245 if (key) { 246 return _cfg ? getPathValue(_cfg, key) : undefined; 247 } else { 248 return _cfg; 249 } 250} 251 252const Config = { 253 get: getConfig, 254 set: setConfig, 255 useConfig, 256 setup, 257}; 258 259export default Config; 260