this repo has no description
1import { Config, ExtensionLoadSource } from "@moonlight-mod/types"; 2import { 3 ExtensionState, 4 MoonbaseExtension, 5 MoonbaseNatives, 6 RepositoryManifest 7} from "../types"; 8import { Store } from "@moonlight-mod/wp/discord/packages/flux"; 9import Dispatcher from "@moonlight-mod/wp/discord/Dispatcher"; 10import getNatives from "../native"; 11import { mainRepo } from "@moonlight-mod/types/constants"; 12import { 13 checkExtensionCompat, 14 ExtensionCompat 15} from "@moonlight-mod/core/extension/loader"; 16import { CustomComponent } from "@moonlight-mod/types/coreExtensions/moonbase"; 17 18const logger = moonlight.getLogger("moonbase"); 19 20let natives: MoonbaseNatives = moonlight.getNatives("moonbase"); 21if (moonlightNode.isBrowser) natives = getNatives(); 22 23class MoonbaseSettingsStore extends Store<any> { 24 private origConfig: Config; 25 private config: Config; 26 private extensionIndex: number; 27 private configComponents: Record<string, Record<string, CustomComponent>> = 28 {}; 29 30 modified: boolean; 31 submitting: boolean; 32 installing: boolean; 33 34 newVersion: string | null; 35 shouldShowNotice: boolean; 36 37 extensions: { [id: number]: MoonbaseExtension }; 38 updates: { 39 [id: number]: { 40 version: string; 41 download: string; 42 updateManifest: RepositoryManifest; 43 }; 44 }; 45 46 constructor() { 47 super(Dispatcher); 48 49 this.origConfig = moonlightNode.config; 50 this.config = this.clone(this.origConfig); 51 this.extensionIndex = 0; 52 53 this.modified = false; 54 this.submitting = false; 55 this.installing = false; 56 57 this.newVersion = null; 58 this.shouldShowNotice = false; 59 60 this.extensions = {}; 61 this.updates = {}; 62 for (const ext of moonlightNode.extensions) { 63 const uniqueId = this.extensionIndex++; 64 this.extensions[uniqueId] = { 65 ...ext, 66 uniqueId, 67 state: moonlight.enabledExtensions.has(ext.id) 68 ? ExtensionState.Enabled 69 : ExtensionState.Disabled, 70 compat: checkExtensionCompat(ext.manifest), 71 hasUpdate: false 72 }; 73 } 74 75 natives! 76 .fetchRepositories(this.config.repositories) 77 .then((ret) => { 78 for (const [repo, exts] of Object.entries(ret)) { 79 try { 80 for (const ext of exts) { 81 const uniqueId = this.extensionIndex++; 82 const extensionData = { 83 id: ext.id, 84 uniqueId, 85 manifest: ext, 86 source: { type: ExtensionLoadSource.Normal, url: repo }, 87 state: ExtensionState.NotDownloaded, 88 compat: ExtensionCompat.Compatible, 89 hasUpdate: false 90 }; 91 92 // Don't present incompatible updates 93 if (checkExtensionCompat(ext) !== ExtensionCompat.Compatible) 94 continue; 95 96 const existing = this.getExisting(extensionData); 97 if (existing != null) { 98 // Make sure the download URL is properly updated 99 for (const [id, e] of Object.entries(this.extensions)) { 100 if (e.id === ext.id && e.source.url === repo) { 101 this.extensions[parseInt(id)].manifest = { 102 ...e.manifest, 103 download: ext.download 104 }; 105 break; 106 } 107 } 108 109 if (this.hasUpdate(extensionData)) { 110 this.updates[existing.uniqueId] = { 111 version: ext.version!, 112 download: ext.download, 113 updateManifest: ext 114 }; 115 existing.hasUpdate = true; 116 } 117 118 continue; 119 } 120 121 this.extensions[uniqueId] = extensionData; 122 } 123 } catch (e) { 124 logger.error(`Error processing repository ${repo}`, e); 125 } 126 } 127 128 this.emitChange(); 129 }) 130 .then(() => 131 this.getExtensionConfigRaw("moonbase", "updateChecking", true) 132 ? natives!.checkForMoonlightUpdate() 133 : new Promise<null>((resolve) => resolve(null)) 134 ) 135 .then((version) => { 136 this.newVersion = version; 137 this.emitChange(); 138 }) 139 .then(() => { 140 this.shouldShowNotice = 141 this.newVersion != null || Object.keys(this.updates).length > 0; 142 this.emitChange(); 143 }); 144 } 145 146 private getExisting(ext: MoonbaseExtension) { 147 return Object.values(this.extensions).find( 148 (e) => e.id === ext.id && e.source.url === ext.source.url 149 ); 150 } 151 152 private hasUpdate(ext: MoonbaseExtension) { 153 const existing = Object.values(this.extensions).find( 154 (e) => e.id === ext.id && e.source.url === ext.source.url 155 ); 156 if (existing == null) return false; 157 158 return ( 159 existing.manifest.version !== ext.manifest.version && 160 existing.state !== ExtensionState.NotDownloaded 161 ); 162 } 163 164 // Jank 165 private isModified() { 166 const orig = JSON.stringify(this.origConfig); 167 const curr = JSON.stringify(this.config); 168 return orig !== curr; 169 } 170 171 get busy() { 172 return this.submitting || this.installing; 173 } 174 175 showNotice() { 176 return this.modified; 177 } 178 179 getExtension(uniqueId: number) { 180 return this.extensions[uniqueId]; 181 } 182 183 getExtensionUniqueId(id: string) { 184 return Object.values(this.extensions).find((ext) => ext.id === id) 185 ?.uniqueId; 186 } 187 188 getExtensionConflicting(uniqueId: number) { 189 const ext = this.getExtension(uniqueId); 190 if (ext.state !== ExtensionState.NotDownloaded) return false; 191 return Object.values(this.extensions).some( 192 (e) => 193 e.id === ext.id && 194 e.uniqueId !== uniqueId && 195 e.state !== ExtensionState.NotDownloaded 196 ); 197 } 198 199 getExtensionName(uniqueId: number) { 200 const ext = this.getExtension(uniqueId); 201 return ext.manifest.meta?.name ?? ext.id; 202 } 203 204 getExtensionUpdate(uniqueId: number) { 205 return this.updates[uniqueId]?.version; 206 } 207 208 getExtensionEnabled(uniqueId: number) { 209 const ext = this.getExtension(uniqueId); 210 if (ext.state === ExtensionState.NotDownloaded) return false; 211 const val = this.config.extensions[ext.id]; 212 if (val == null) return false; 213 return typeof val === "boolean" ? val : val.enabled; 214 } 215 216 getExtensionConfig<T>(uniqueId: number, key: string): T | undefined { 217 const ext = this.getExtension(uniqueId); 218 const defaultValue = ext.manifest.settings?.[key]?.default; 219 const clonedDefaultValue = this.clone(defaultValue); 220 const cfg = this.config.extensions[ext.id]; 221 222 if (cfg == null || typeof cfg === "boolean") return clonedDefaultValue; 223 return cfg.config?.[key] ?? clonedDefaultValue; 224 } 225 226 getExtensionConfigRaw<T>( 227 id: string, 228 key: string, 229 defaultValue: T | undefined 230 ): T | undefined { 231 const cfg = this.config.extensions[id]; 232 233 if (cfg == null || typeof cfg === "boolean") return defaultValue; 234 return cfg.config?.[key] ?? defaultValue; 235 } 236 237 getExtensionConfigName(uniqueId: number, key: string) { 238 const ext = this.getExtension(uniqueId); 239 return ext.manifest.settings?.[key]?.displayName ?? key; 240 } 241 242 getExtensionConfigDescription(uniqueId: number, key: string) { 243 const ext = this.getExtension(uniqueId); 244 return ext.manifest.settings?.[key]?.description; 245 } 246 247 setExtensionConfig(id: string, key: string, value: any) { 248 const oldConfig = this.config.extensions[id]; 249 const newConfig = 250 typeof oldConfig === "boolean" 251 ? { 252 enabled: oldConfig, 253 config: { [key]: value } 254 } 255 : { 256 ...oldConfig, 257 config: { ...(oldConfig?.config ?? {}), [key]: value } 258 }; 259 260 this.config.extensions[id] = newConfig; 261 this.modified = this.isModified(); 262 this.emitChange(); 263 } 264 265 setExtensionEnabled(uniqueId: number, enabled: boolean) { 266 const ext = this.getExtension(uniqueId); 267 let val = this.config.extensions[ext.id]; 268 269 if (val == null) { 270 this.config.extensions[ext.id] = { enabled }; 271 this.modified = this.isModified(); 272 this.emitChange(); 273 return; 274 } 275 276 if (typeof val === "boolean") { 277 val = enabled; 278 } else { 279 val.enabled = enabled; 280 } 281 282 this.config.extensions[ext.id] = val; 283 this.modified = this.isModified(); 284 this.emitChange(); 285 } 286 287 async installExtension(uniqueId: number) { 288 const ext = this.getExtension(uniqueId); 289 if (!("download" in ext.manifest)) { 290 throw new Error("Extension has no download URL"); 291 } 292 293 this.installing = true; 294 try { 295 const update = this.updates[uniqueId]; 296 const url = update?.download ?? ext.manifest.download; 297 await natives!.installExtension(ext.manifest, url, ext.source.url!); 298 if (ext.state === ExtensionState.NotDownloaded) { 299 this.extensions[uniqueId].state = ExtensionState.Disabled; 300 } 301 302 if (update != null) 303 this.extensions[uniqueId].compat = checkExtensionCompat( 304 update.updateManifest 305 ); 306 307 delete this.updates[uniqueId]; 308 } catch (e) { 309 logger.error("Error installing extension:", e); 310 } 311 312 this.installing = false; 313 this.emitChange(); 314 } 315 316 private getRank(ext: MoonbaseExtension) { 317 if (ext.source.type === ExtensionLoadSource.Developer) return 3; 318 if (ext.source.type === ExtensionLoadSource.Core) return 2; 319 if (ext.source.url === mainRepo) return 1; 320 return 0; 321 } 322 323 async getDependencies(uniqueId: number) { 324 const ext = this.getExtension(uniqueId); 325 326 const missingDeps = []; 327 for (const dep of ext.manifest.dependencies ?? []) { 328 const anyInstalled = Object.values(this.extensions).some( 329 (e) => e.id === dep && e.state !== ExtensionState.NotDownloaded 330 ); 331 if (!anyInstalled) missingDeps.push(dep); 332 } 333 334 if (missingDeps.length === 0) return null; 335 336 const deps: Record<string, MoonbaseExtension[]> = {}; 337 for (const dep of missingDeps) { 338 const candidates = Object.values(this.extensions).filter( 339 (e) => e.id === dep 340 ); 341 342 deps[dep] = candidates.sort((a, b) => { 343 const aRank = this.getRank(a); 344 const bRank = this.getRank(b); 345 if (aRank === bRank) { 346 const repoIndex = this.config.repositories.indexOf(a.source.url!); 347 const otherRepoIndex = this.config.repositories.indexOf( 348 b.source.url! 349 ); 350 return repoIndex - otherRepoIndex; 351 } else { 352 return bRank - aRank; 353 } 354 }); 355 } 356 357 return deps; 358 } 359 360 async deleteExtension(uniqueId: number) { 361 const ext = this.getExtension(uniqueId); 362 if (ext == null) return; 363 364 this.installing = true; 365 try { 366 await natives!.deleteExtension(ext.id); 367 this.extensions[uniqueId].state = ExtensionState.NotDownloaded; 368 } catch (e) { 369 logger.error("Error deleting extension:", e); 370 } 371 372 this.installing = false; 373 this.emitChange(); 374 } 375 376 async updateMoonlight() { 377 await natives.updateMoonlight(); 378 } 379 380 getConfigOption<K extends keyof Config>(key: K): Config[K] { 381 return this.config[key]; 382 } 383 384 setConfigOption<K extends keyof Config>(key: K, value: Config[K]) { 385 this.config[key] = value; 386 this.modified = this.isModified(); 387 this.emitChange(); 388 } 389 390 tryGetExtensionName(id: string) { 391 const uniqueId = this.getExtensionUniqueId(id); 392 return (uniqueId != null ? this.getExtensionName(uniqueId) : null) ?? id; 393 } 394 395 registerConfigComponent( 396 ext: string, 397 name: string, 398 component: CustomComponent 399 ) { 400 if (!(ext in this.configComponents)) this.configComponents[ext] = {}; 401 this.configComponents[ext][name] = component; 402 } 403 404 getExtensionConfigComponent(ext: string, name: string) { 405 return this.configComponents[ext]?.[name]; 406 } 407 408 writeConfig() { 409 this.submitting = true; 410 411 moonlightNode.writeConfig(this.config); 412 this.origConfig = this.clone(this.config); 413 414 this.submitting = false; 415 this.modified = false; 416 this.emitChange(); 417 } 418 419 reset() { 420 this.submitting = false; 421 this.modified = false; 422 this.config = this.clone(this.origConfig); 423 this.emitChange(); 424 } 425 426 // Required because electron likes to make it immutable sometimes. 427 // This sucks. 428 private clone<T>(obj: T): T { 429 return structuredClone(obj); 430 } 431} 432 433const settingsStore = new MoonbaseSettingsStore(); 434export { settingsStore as MoonbaseSettingsStore };