250 lines
8.6 kB
1
import React from "@moonlight-mod/wp/react";
2
import * as Components from "@moonlight-mod/wp/discord/components/common/index";
3
import { useStateFromStores, useStateFromStoresObject } from "@moonlight-mod/wp/discord/packages/flux";
4
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
5
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
6
import { RepositoryManifest, UpdateState } from "../types";
7
import { ConfigExtension, DetectedExtension } from "@moonlight-mod/types";
8
9
const { Button, TabBar } = Components;
10
const TabBarClasses = spacepack.findByCode(/tabBar:"tabBar_[a-z0-9]+",tabBarItem:"tabBarItem_[a-z0-9]+"/)[0].exports;
11
12
const MODULE_REGEX = /Webpack-Module-(\d+)/g;
13
14
const logger = moonlight.getLogger("moonbase/crashScreen");
15
16
type ErrorState = {
17
error: Error;
18
info: {
19
componentStack: string;
20
};
21
__moonlight_update?: UpdateState;
22
};
23
24
type WrapperProps = {
25
action: React.ReactNode;
26
state: ErrorState;
27
};
28
29
type UpdateCardProps = {
30
id: number;
31
ext: {
32
version: string;
33
download: string;
34
updateManifest: RepositoryManifest;
35
};
36
};
37
38
const updateStrings: Record<UpdateState, string> = {
39
[UpdateState.Ready]: "A new version of moonlight is available.",
40
[UpdateState.Working]: "Updating moonlight...",
41
[UpdateState.Installed]: "Updated moonlight. Click Reload to apply changes.",
42
[UpdateState.Failed]: "Failed to update moonlight. Please use the installer."
43
};
44
const buttonStrings: Record<UpdateState, string> = {
45
[UpdateState.Ready]: "Update moonlight",
46
[UpdateState.Working]: "Updating moonlight...",
47
[UpdateState.Installed]: "",
48
[UpdateState.Failed]: "Update failed"
49
};
50
const extensionButtonStrings: Record<UpdateState, string> = {
51
[UpdateState.Ready]: "Update",
52
[UpdateState.Working]: "Updating...",
53
[UpdateState.Installed]: "Updated",
54
[UpdateState.Failed]: "Update failed"
55
};
56
57
function ExtensionUpdateCard({ id, ext }: UpdateCardProps) {
58
const [state, setState] = React.useState(UpdateState.Ready);
59
const installed = useStateFromStores([MoonbaseSettingsStore], () => MoonbaseSettingsStore.getExtension(id), [id]);
60
61
return (
62
<div className="moonbase-crash-extensionCard">
63
<div className="moonbase-crash-extensionCard-meta">
64
<div className="moonbase-crash-extensionCard-title">
65
{ext.updateManifest.meta?.name ?? ext.updateManifest.id}
66
</div>
67
<div className="moonbase-crash-extensionCard-version">{`v${installed?.manifest?.version ?? "???"} -> v${
68
ext.version
69
}`}</div>
70
</div>
71
<div className="moonbase-crash-extensionCard-button">
72
<Button
73
color={Button.Colors.GREEN}
74
disabled={state !== UpdateState.Ready}
75
onClick={() => {
76
setState(UpdateState.Working);
77
MoonbaseSettingsStore.installExtension(id)
78
.then(() => setState(UpdateState.Installed))
79
.catch(() => setState(UpdateState.Failed));
80
}}
81
>
82
{extensionButtonStrings[state]}
83
</Button>
84
</div>
85
</div>
86
);
87
}
88
89
function ExtensionDisableCard({ ext }: { ext: DetectedExtension }) {
90
function disableWithDependents() {
91
const disable = new Set<string>();
92
disable.add(ext.id);
93
for (const [id, dependencies] of moonlightNode.processedExtensions.dependencyGraph) {
94
if (dependencies?.has(ext.id)) disable.add(id);
95
}
96
97
const config = structuredClone(moonlightNode.config);
98
for (const id in config.extensions) {
99
if (!disable.has(id)) continue;
100
if (typeof config.extensions[id] === "boolean") config.extensions[id] = false;
101
else (config.extensions[id] as ConfigExtension).enabled = false;
102
}
103
104
let msg = `Are you sure you want to disable "${ext.manifest.meta?.name ?? ext.id}"`;
105
if (disable.size > 1) {
106
msg += ` and its ${disable.size - 1} dependent${disable.size - 1 === 1 ? "" : "s"}`;
107
}
108
msg += "?";
109
110
if (confirm(msg)) {
111
moonlightNode.writeConfig(config);
112
window.location.reload();
113
}
114
}
115
116
return (
117
<div className="moonbase-crash-extensionCard">
118
<div className="moonbase-crash-extensionCard-meta">
119
<div className="moonbase-crash-extensionCard-title">{ext.manifest.meta?.name ?? ext.id}</div>
120
<div className="moonbase-crash-extensionCard-version">{`v${ext.manifest.version ?? "???"}`}</div>
121
</div>
122
<div className="moonbase-crash-extensionCard-button">
123
<Button color={Button.Colors.RED} onClick={disableWithDependents}>
124
Disable
125
</Button>
126
</div>
127
</div>
128
);
129
}
130
131
export function wrapAction({ action, state }: WrapperProps) {
132
const [tab, setTab] = React.useState("crash");
133
134
const { updates, updateCount } = useStateFromStoresObject([MoonbaseSettingsStore], () => {
135
const { updates } = MoonbaseSettingsStore;
136
return {
137
updates: Object.entries(updates),
138
updateCount: Object.keys(updates).length
139
};
140
});
141
142
const causes = React.useMemo(() => {
143
const causes = new Set<string>();
144
if (state.error.stack) {
145
for (const [, id] of state.error.stack.matchAll(MODULE_REGEX))
146
for (const ext of moonlight.patched.get(id) ?? []) causes.add(ext);
147
}
148
for (const [, id] of state.info.componentStack.matchAll(MODULE_REGEX))
149
for (const ext of moonlight.patched.get(id) ?? []) causes.add(ext);
150
return [...causes];
151
}, []);
152
153
return (
154
<div className="moonbase-crash-wrapper">
155
{action}
156
<TabBar
157
className={`${TabBarClasses.tabBar} moonbase-crash-tabs`}
158
type="top"
159
selectedItem={tab}
160
onItemSelect={(v) => setTab(v)}
161
>
162
<TabBar.Item className={TabBarClasses.tabBarItem} id="crash">
163
Crash details
164
</TabBar.Item>
165
<TabBar.Item className={TabBarClasses.tabBarItem} id="extensions" disabled={updateCount === 0}>
166
{`Extension updates (${updateCount})`}
167
</TabBar.Item>
168
<TabBar.Item className={TabBarClasses.tabBarItem} id="causes" disabled={causes.length === 0}>
169
{`Possible causes (${causes.length})`}
170
</TabBar.Item>
171
</TabBar>
172
{tab === "crash" ? (
173
<div className="moonbase-crash-details-wrapper">
174
<pre className="moonbase-crash-details">
175
<code>
176
{state.error.stack}
177
{"\n\nComponent stack:"}
178
{state.info.componentStack}
179
</code>
180
</pre>
181
</div>
182
) : null}
183
{tab === "extensions" ? (
184
<div className="moonbase-crash-extensions">
185
{updates.map(([id, ext]) => (
186
<ExtensionUpdateCard id={Number(id)} ext={ext} />
187
))}
188
</div>
189
) : null}
190
{tab === "causes" ? (
191
<div className="moonbase-crash-extensions">
192
{causes
193
.map((ext) => moonlightNode.extensions.find((e) => e.id === ext)!)
194
.map((ext) => (
195
<ExtensionDisableCard ext={ext} />
196
))}
197
</div>
198
) : null}
199
</div>
200
);
201
}
202
203
export function UpdateText({ state, setState }: { state: ErrorState; setState: (state: ErrorState) => void }) {
204
if (!state.__moonlight_update) {
205
setState({
206
...state,
207
__moonlight_update: UpdateState.Ready
208
});
209
}
210
const newVersion = useStateFromStores([MoonbaseSettingsStore], () => MoonbaseSettingsStore.newVersion);
211
212
return newVersion == null ? null : (
213
<p>{state.__moonlight_update !== undefined ? updateStrings[state.__moonlight_update] : ""}</p>
214
);
215
}
216
217
export function UpdateButton({ state, setState }: { state: ErrorState; setState: (state: ErrorState) => void }) {
218
const newVersion = useStateFromStores([MoonbaseSettingsStore], () => MoonbaseSettingsStore.newVersion);
219
return newVersion == null ||
220
state.__moonlight_update === UpdateState.Installed ||
221
state.__moonlight_update === undefined ? null : (
222
<Button
223
size={Button.Sizes.LARGE}
224
disabled={state.__moonlight_update !== UpdateState.Ready}
225
onClick={() => {
226
setState({
227
...state,
228
__moonlight_update: UpdateState.Working
229
});
230
231
MoonbaseSettingsStore.updateMoonlight()
232
.then(() => {
233
setState({
234
...state,
235
__moonlight_update: UpdateState.Installed
236
});
237
})
238
.catch((e) => {
239
logger.error(e);
240
setState({
241
...state,
242
__moonlight_update: UpdateState.Failed
243
});
244
});
245
}}
246
>
247
{state.__moonlight_update !== undefined ? buttonStrings[state.__moonlight_update] : ""}
248
</Button>
249
);
250
}
251