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