this repo has no description
1import React from "@moonlight-mod/wp/react";
2import { Button, TabBar } from "@moonlight-mod/wp/discord/components/common/index";
3import { useStateFromStores, useStateFromStoresObject } from "@moonlight-mod/wp/discord/packages/flux";
4import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
5import { RepositoryManifest, UpdateState } from "../types";
6import { ConfigExtension, DetectedExtension } from "@moonlight-mod/types";
7import DiscoveryClasses from "@moonlight-mod/wp/discord/modules/discovery/web/Discovery.css";
8
9const MODULE_REGEX = /Webpack-Module-(\d+)/g;
10
11const logger = moonlight.getLogger("moonbase/crashScreen");
12
13type ErrorState = {
14 error: Error;
15 info: {
16 componentStack: string;
17 };
18 __moonlight_update?: UpdateState;
19};
20
21type WrapperProps = {
22 action: React.ReactNode;
23 state: ErrorState;
24};
25
26type UpdateCardProps = {
27 id: number;
28 ext: {
29 version: string;
30 download: string;
31 updateManifest: RepositoryManifest;
32 };
33};
34
35const 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};
41const buttonStrings: Record<UpdateState, string> = {
42 [UpdateState.Ready]: "Update moonlight",
43 [UpdateState.Working]: "Updating moonlight...",
44 [UpdateState.Installed]: "",
45 [UpdateState.Failed]: "Update failed"
46};
47const extensionButtonStrings: Record<UpdateState, string> = {
48 [UpdateState.Ready]: "Update",
49 [UpdateState.Working]: "Updating...",
50 [UpdateState.Installed]: "Updated",
51 [UpdateState.Failed]: "Update failed"
52};
53
54function 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
86function 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
128export 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
200export 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
214export 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}