this repo has no description
at main 9.2 kB view raw
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+)\/(\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 148 for (const [path, id] of Object.entries(moonlight.moonmap.modules)) { 149 const MAPPING_REGEX = new RegExp( 150 // @ts-expect-error Only Firefox has RegExp.escape 151 `(${RegExp.escape ? RegExp.escape(path) : path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, 152 "g" 153 ); 154 155 if (state.error.stack) { 156 for (const match of state.error.stack.matchAll(MAPPING_REGEX)) 157 if (match) for (const ext of moonlight.patched.get(id) ?? []) causes.add(ext); 158 } 159 for (const match of state.info.componentStack.matchAll(MAPPING_REGEX)) 160 if (match) for (const ext of moonlight.patched.get(id) ?? []) causes.add(ext); 161 } 162 163 return [...causes]; 164 }, []); 165 166 return ( 167 <div className="moonbase-crash-wrapper"> 168 {action} 169 <TabBar 170 className={`${DiscoveryClasses.tabBar} moonbase-crash-tabs`} 171 type="top" 172 selectedItem={tab} 173 onItemSelect={(v) => setTab(v)} 174 > 175 <TabBar.Item className={DiscoveryClasses.tabBarItem} id="crash"> 176 Crash details 177 </TabBar.Item> 178 <TabBar.Item className={DiscoveryClasses.tabBarItem} id="extensions" disabled={updateCount === 0}> 179 {`Extension updates (${updateCount})`} 180 </TabBar.Item> 181 <TabBar.Item className={DiscoveryClasses.tabBarItem} id="causes" disabled={causes.length === 0}> 182 {`Possible causes (${causes.length})`} 183 </TabBar.Item> 184 </TabBar> 185 {tab === "crash" ? ( 186 <div className="moonbase-crash-details-wrapper"> 187 <pre className="moonbase-crash-details"> 188 <code> 189 {state.error.stack} 190 {"\n\nComponent stack:"} 191 {state.info.componentStack} 192 </code> 193 </pre> 194 </div> 195 ) : null} 196 {tab === "extensions" ? ( 197 <div className="moonbase-crash-extensions"> 198 {updates.map(([id, ext]) => ( 199 <ExtensionUpdateCard id={Number(id)} ext={ext} /> 200 ))} 201 </div> 202 ) : null} 203 {tab === "causes" ? ( 204 <div className="moonbase-crash-extensions"> 205 {causes 206 .map((ext) => moonlightNode.extensions.find((e) => e.id === ext)!) 207 .map((ext) => ( 208 <ExtensionDisableCard ext={ext} /> 209 ))} 210 </div> 211 ) : null} 212 </div> 213 ); 214} 215 216export function UpdateText({ state, setState }: { state: ErrorState; setState: (state: ErrorState) => void }) { 217 if (!state.__moonlight_update) { 218 setState({ 219 ...state, 220 __moonlight_update: UpdateState.Ready 221 }); 222 } 223 const newVersion = useStateFromStores([MoonbaseSettingsStore], () => MoonbaseSettingsStore.newVersion); 224 225 return newVersion == null ? null : ( 226 <p>{state.__moonlight_update !== undefined ? updateStrings[state.__moonlight_update] : ""}</p> 227 ); 228} 229 230export function UpdateButton({ state, setState }: { state: ErrorState; setState: (state: ErrorState) => void }) { 231 const newVersion = useStateFromStores([MoonbaseSettingsStore], () => MoonbaseSettingsStore.newVersion); 232 return newVersion == null || 233 state.__moonlight_update === UpdateState.Installed || 234 state.__moonlight_update === undefined ? null : ( 235 <Button 236 size={Button.Sizes.LARGE} 237 disabled={state.__moonlight_update !== UpdateState.Ready} 238 onClick={() => { 239 setState({ 240 ...state, 241 __moonlight_update: UpdateState.Working 242 }); 243 244 MoonbaseSettingsStore.updateMoonlight() 245 .then(() => { 246 setState({ 247 ...state, 248 __moonlight_update: UpdateState.Installed 249 }); 250 }) 251 .catch((e) => { 252 logger.error(e); 253 setState({ 254 ...state, 255 __moonlight_update: UpdateState.Failed 256 }); 257 }); 258 }} 259 > 260 {state.__moonlight_update !== undefined ? buttonStrings[state.__moonlight_update] : ""} 261 </Button> 262 ); 263}