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