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