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 Flux from "@moonlight-mod/wp/common_flux";
9import Dispatcher from "@moonlight-mod/wp/common_fluxDispatcher";
10
11const natives: MoonbaseNatives = moonlight.getNatives("moonbase");
12const logger = moonlight.getLogger("moonbase");
13
14class MoonbaseSettingsStore extends Flux.Store<any> {
15 private origConfig: Config;
16 private config: Config;
17
18 modified: boolean;
19 submitting: boolean;
20 installing: boolean;
21
22 extensions: { [id: string]: MoonbaseExtension };
23 updates: { [id: string]: { version: string; download: string } };
24
25 constructor() {
26 super(Dispatcher);
27
28 this.origConfig = moonlightNode.config;
29 this.config = this.clone(this.origConfig);
30
31 this.modified = false;
32 this.submitting = false;
33 this.installing = false;
34
35 this.extensions = {};
36 this.updates = {};
37 for (const ext of moonlightNode.extensions) {
38 const existingExtension = this.extensions[ext.id];
39 if (existingExtension != null) continue;
40
41 this.extensions[ext.id] = {
42 ...ext,
43 state: moonlight.enabledExtensions.has(ext.id)
44 ? ExtensionState.Enabled
45 : ExtensionState.Disabled
46 };
47 }
48
49 natives.fetchRepositories(this.config.repositories).then((ret) => {
50 for (const [repo, exts] of Object.entries(ret)) {
51 try {
52 for (const ext of exts) {
53 try {
54 const existingExtension = this.extensions[ext.id];
55 if (existingExtension !== undefined) {
56 if (this.hasUpdate(repo, ext, existingExtension)) {
57 this.updates[ext.id] = {
58 version: ext.version!,
59 download: ext.download
60 };
61 }
62 continue;
63 }
64
65 this.extensions[ext.id] = {
66 id: ext.id,
67 manifest: ext,
68 source: { type: ExtensionLoadSource.Normal, url: repo },
69 state: ExtensionState.NotDownloaded
70 };
71 } catch (e) {
72 logger.error(`Error processing extension ${ext.id}`, e);
73 }
74 }
75 } catch (e) {
76 logger.error(`Error processing repository ${repo}`, e);
77 }
78 }
79
80 this.emitChange();
81 });
82 }
83
84 // this logic sucks so bad lol
85 private hasUpdate(
86 repo: string,
87 repoExt: RepositoryManifest,
88 existing: MoonbaseExtension
89 ) {
90 return (
91 existing.source.type === ExtensionLoadSource.Normal &&
92 existing.source.url != null &&
93 existing.source.url === repo &&
94 repoExt.version != null &&
95 existing.manifest.version !== repoExt.version
96 );
97 }
98
99 // Jank
100 private isModified() {
101 const orig = JSON.stringify(this.origConfig);
102 const curr = JSON.stringify(this.config);
103 return orig !== curr;
104 }
105
106 get busy() {
107 return this.submitting || this.installing;
108 }
109
110 showNotice() {
111 return this.modified;
112 }
113
114 getExtension(id: string) {
115 return this.extensions[id];
116 }
117
118 getExtensionName(id: string) {
119 return Object.prototype.hasOwnProperty.call(this.extensions, id)
120 ? this.extensions[id].manifest.meta?.name ?? id
121 : id;
122 }
123
124 getExtensionUpdate(id: string) {
125 return Object.prototype.hasOwnProperty.call(this.updates, id)
126 ? this.updates[id]
127 : null;
128 }
129
130 getExtensionEnabled(id: string) {
131 const val = this.config.extensions[id];
132 if (val == null) return false;
133 return typeof val === "boolean" ? val : val.enabled;
134 }
135
136 getExtensionConfig<T>(id: string, key: string): T | undefined {
137 const defaultValue = this.extensions[id].manifest.settings?.[key]?.default;
138 const clonedDefaultValue = this.clone(defaultValue);
139 const cfg = this.config.extensions[id];
140
141 if (cfg == null || typeof cfg === "boolean") return clonedDefaultValue;
142 return cfg.config?.[key] ?? clonedDefaultValue;
143 }
144
145 getExtensionConfigName(id: string, key: string) {
146 return this.extensions[id].manifest.settings?.[key]?.displayName ?? key;
147 }
148
149 getExtensionConfigDescription(id: string, key: string) {
150 return this.extensions[id].manifest.settings?.[key]?.description;
151 }
152
153 setExtensionConfig(id: string, key: string, value: any) {
154 const oldConfig = this.config.extensions[id];
155 const newConfig =
156 typeof oldConfig === "boolean"
157 ? {
158 enabled: oldConfig,
159 config: { [key]: value }
160 }
161 : {
162 ...oldConfig,
163 config: { ...(oldConfig?.config ?? {}), [key]: value }
164 };
165
166 this.config.extensions[id] = newConfig;
167 this.modified = this.isModified();
168 this.emitChange();
169 }
170
171 setExtensionEnabled(id: string, enabled: boolean) {
172 let val = this.config.extensions[id];
173
174 if (val == null) {
175 this.config.extensions[id] = { enabled };
176 this.modified = this.isModified();
177 this.emitChange();
178 return;
179 }
180
181 if (typeof val === "boolean") {
182 val = enabled;
183 } else {
184 val.enabled = enabled;
185 }
186
187 this.config.extensions[id] = val;
188 this.modified = this.isModified();
189 this.emitChange();
190 }
191
192 async installExtension(id: string) {
193 const ext = this.getExtension(id);
194 if (!("download" in ext.manifest)) {
195 throw new Error("Extension has no download URL");
196 }
197
198 this.installing = true;
199 try {
200 const url = this.updates[id]?.download ?? ext.manifest.download;
201 await natives.installExtension(ext.manifest, url, ext.source.url!);
202 if (ext.state === ExtensionState.NotDownloaded) {
203 this.extensions[id].state = ExtensionState.Disabled;
204 }
205
206 delete this.updates[id];
207 } catch (e) {
208 logger.error("Error installing extension:", e);
209 }
210
211 this.installing = false;
212 this.emitChange();
213 }
214
215 async deleteExtension(id: string) {
216 const ext = this.getExtension(id);
217 if (ext == null) return;
218
219 this.installing = true;
220 try {
221 await natives.deleteExtension(ext.id);
222 this.extensions[id].state = ExtensionState.NotDownloaded;
223 } catch (e) {
224 logger.error("Error deleting extension:", e);
225 }
226
227 this.installing = false;
228 this.emitChange();
229 }
230
231 getConfigOption<K extends keyof Config>(key: K): Config[K] {
232 return this.config[key];
233 }
234
235 setConfigOption<K extends keyof Config>(key: K, value: Config[K]) {
236 this.config[key] = value;
237 this.modified = this.isModified();
238 this.emitChange();
239 }
240
241 writeConfig() {
242 this.submitting = true;
243
244 try {
245 moonlightNode.writeConfig(this.config);
246 this.origConfig = this.clone(this.config);
247 } catch (e) {
248 logger.error("Error writing config", e);
249 }
250
251 this.submitting = false;
252 this.modified = false;
253 this.emitChange();
254 }
255
256 reset() {
257 this.submitting = false;
258 this.modified = false;
259 this.config = this.clone(this.origConfig);
260 this.emitChange();
261 }
262
263 // Required because electron likes to make it immutable sometimes.
264 // This sucks.
265 private clone<T>(obj: T): T {
266 return structuredClone(obj);
267 }
268}
269
270const settingsStore = new MoonbaseSettingsStore();
271export { settingsStore as MoonbaseSettingsStore };