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}