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