mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
import { Component, useMemo, useState, type ReactNode } from "react";
|
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
import { isElectron } from "@/lib/electron";
|
|
import { usePluginStore } from "@/plugin-host/plugin-store";
|
|
import { retryPluginFrontend, syncPlugins } from "@/plugin-host/loader";
|
|
import { chosenLayoutId, useLookState } from "@/look/look-store";
|
|
import {
|
|
getRegisteredLayout,
|
|
useRegisteredLayouts,
|
|
} from "@/look/layout-registry";
|
|
import { layoutStatus } from "@/look/layout-status";
|
|
import { safeLayoutInUrl } from "@/look/resolve-look";
|
|
import { useLookTheme } from "@/look/use-look-theme";
|
|
import { RepairView } from "@/look/RepairView";
|
|
|
|
/** Catches a crash while a layout draws, so the repair view can take over. */
|
|
class LayoutErrorBoundary extends Component<
|
|
{ onCrash: (error: Error) => void; children: ReactNode },
|
|
{ crashed: boolean }
|
|
> {
|
|
state = { crashed: false };
|
|
|
|
static getDerivedStateFromError() {
|
|
return { crashed: true };
|
|
}
|
|
|
|
componentDidCatch(error: Error) {
|
|
console.error("[layout] crashed while drawing", error);
|
|
this.props.onCrash(error);
|
|
}
|
|
|
|
render() {
|
|
return this.state.crashed ? null : this.props.children;
|
|
}
|
|
}
|
|
|
|
interface CrashState {
|
|
layoutId: string | null;
|
|
count: number;
|
|
error: string | null;
|
|
}
|
|
|
|
/**
|
|
* Draws the chosen layout, or the repair view when it cannot be drawn. A
|
|
* crash remounts the layout once; a second hands over to the repair view.
|
|
*/
|
|
export function LayoutHost() {
|
|
const look = useLookState();
|
|
const store = usePluginStore();
|
|
useRegisteredLayouts();
|
|
const isMobile = useIsMobile();
|
|
const [crash, setCrash] = useState<CrashState>({
|
|
layoutId: null,
|
|
count: 0,
|
|
error: null,
|
|
});
|
|
const [attempt, setAttempt] = useState(0);
|
|
// The look store changes identity whenever the choice does.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
const layoutId = useMemo(() => chosenLayoutId(), [look]);
|
|
useLookTheme(layoutId);
|
|
|
|
const crashes = crash.layoutId === layoutId ? crash.count : 0;
|
|
const owner = [...store.records.values()].find((record) =>
|
|
record.summary.contributes?.layouts?.some((l) => l.id === layoutId),
|
|
);
|
|
const status = layoutStatus({
|
|
layoutId,
|
|
safeFromUrl: safeLayoutInUrl(window.location.search),
|
|
safeFromEnv: look.response?.safeLayout === true,
|
|
ready: look.loaded && store.settled,
|
|
registered: getRegisteredLayout(layoutId),
|
|
owner,
|
|
unavailable: look.response?.unavailable.find((plugin) =>
|
|
plugin.layouts.includes(layoutId),
|
|
),
|
|
crashes,
|
|
lastCrash: crash.layoutId === layoutId ? crash.error : null,
|
|
});
|
|
|
|
const retry = () => {
|
|
setCrash({ layoutId: null, count: 0, error: null });
|
|
setAttempt((value) => value + 1);
|
|
if (owner) void retryPluginFrontend(owner.summary.id);
|
|
else void syncPlugins();
|
|
};
|
|
|
|
if (status.kind === "loading") {
|
|
return <div className="h-dvh w-screen bg-background" />;
|
|
}
|
|
if (status.kind === "repair") {
|
|
return <RepairView status={status} onRetry={retry} />;
|
|
}
|
|
|
|
const { layout } = status;
|
|
// The desktop app always gets the desktop layout.
|
|
const Draw =
|
|
isMobile && !isElectron() && layout.mobileComponent
|
|
? layout.mobileComponent
|
|
: layout.component;
|
|
return (
|
|
<LayoutErrorBoundary
|
|
key={`${layoutId}:${crashes}:${attempt}`}
|
|
onCrash={(error) =>
|
|
setCrash((previous) => ({
|
|
layoutId,
|
|
count: (previous.layoutId === layoutId ? previous.count : 0) + 1,
|
|
error: error.message,
|
|
}))
|
|
}
|
|
>
|
|
<Draw />
|
|
</LayoutErrorBoundary>
|
|
);
|
|
}
|