Files
frigate/web/src/pages/SetupWizard.tsx
T

285 lines
8.1 KiB
TypeScript
Raw Normal View History

2026-08-18 16:02:26 -05:00
import StepIndicator from "@/components/indicators/StepIndicator";
2026-08-26 11:56:50 -05:00
import SetupAccount from "@/components/setup/SetupAccount";
2026-08-18 16:02:26 -05:00
import SetupCamera from "@/components/setup/SetupCamera";
import SetupComplete from "@/components/setup/SetupComplete";
import SetupDetector from "@/components/setup/SetupDetector";
import SetupHwAccel from "@/components/setup/SetupHwAccel";
import SetupRecording from "@/components/setup/SetupRecording";
import SetupWelcome from "@/components/setup/SetupWelcome";
2026-08-21 18:04:29 -05:00
import { Button } from "@/components/ui/button";
2026-08-18 16:02:26 -05:00
import { Card, CardContent } from "@/components/ui/card";
2026-08-21 18:04:29 -05:00
import { useTheme } from "@/context/theme-provider";
2026-08-26 11:56:50 -05:00
import { FrigateConfig } from "@/types/frigateConfig";
import { dismissSetup } from "@/utils/setupWizard";
2026-08-26 11:50:44 -05:00
import { useCallback, useMemo, useReducer } from "react";
2026-08-18 16:02:26 -05:00
import { useTranslation } from "react-i18next";
2026-08-21 18:04:29 -05:00
import { LuMoon, LuSun } from "react-icons/lu";
2026-08-26 11:56:50 -05:00
import useSWR from "swr";
2026-08-18 16:02:26 -05:00
2026-08-26 11:50:44 -05:00
type StepKey =
| "welcome"
2026-08-26 11:56:50 -05:00
| "account"
2026-08-26 11:50:44 -05:00
| "camera"
| "detector"
| "hwaccel"
| "recording"
| "complete";
const STEP_KEYS: StepKey[] = [
"welcome",
2026-08-26 11:56:50 -05:00
"account",
2026-08-26 11:50:44 -05:00
"camera",
"detector",
"hwaccel",
"recording",
"complete",
2026-08-18 16:02:26 -05:00
];
type WizardState = {
currentStep: number;
cameraNames: string[];
detectorHardwareKey?: string;
2026-08-21 12:39:50 -05:00
// camera name -> detect stream codec
detectCodecs: Record<string, string>;
2026-08-21 12:39:50 -05:00
// camera adds apply live, so they don't count toward needing a restart
2026-08-18 16:02:26 -05:00
restartRequired: boolean;
configuredSteps: {
camera: boolean;
hwaccel: boolean;
detector: boolean;
recording: boolean;
};
};
type WizardAction =
| { type: "NEXT_STEP" }
| { type: "PREV_STEP" }
| {
type: "CAMERAS_ADDED";
cameraNames: string[];
detectCodecs: Record<string, string>;
}
2026-08-18 16:02:26 -05:00
| {
type: "STEP_CONFIGURED";
step: keyof WizardState["configuredSteps"];
savedConfig: boolean;
}
| { type: "DETECTOR_DONE"; configured: boolean; hardwareKey?: string }
| { type: "SKIP_STEP" };
const initialState: WizardState = {
currentStep: 0,
cameraNames: [],
detectCodecs: {},
2026-08-18 16:02:26 -05:00
restartRequired: false,
configuredSteps: {
camera: false,
hwaccel: false,
detector: false,
recording: false,
},
};
function wizardReducer(state: WizardState, action: WizardAction): WizardState {
switch (action.type) {
case "NEXT_STEP":
return { ...state, currentStep: state.currentStep + 1 };
case "PREV_STEP":
return {
...state,
currentStep: Math.max(0, state.currentStep - 1),
};
case "CAMERAS_ADDED":
return {
...state,
currentStep: state.currentStep + 1,
cameraNames: action.cameraNames,
detectCodecs: action.detectCodecs,
2026-08-18 16:02:26 -05:00
configuredSteps: { ...state.configuredSteps, camera: true },
};
case "STEP_CONFIGURED":
return {
...state,
currentStep: state.currentStep + 1,
restartRequired: state.restartRequired || action.savedConfig,
configuredSteps: { ...state.configuredSteps, [action.step]: true },
};
case "DETECTOR_DONE":
return {
...state,
currentStep: state.currentStep + 1,
detectorHardwareKey: action.hardwareKey ?? state.detectorHardwareKey,
restartRequired: state.restartRequired || action.configured,
configuredSteps: {
...state.configuredSteps,
detector: state.configuredSteps.detector || action.configured,
},
};
case "SKIP_STEP":
return { ...state, currentStep: state.currentStep + 1 };
default:
return state;
}
}
export default function SetupWizard() {
2026-08-21 18:04:29 -05:00
const { t } = useTranslation(["views/setup", "common"]);
2026-08-18 16:02:26 -05:00
const [state, dispatch] = useReducer(wizardReducer, initialState);
2026-08-21 18:04:29 -05:00
const { theme, systemTheme, setTheme } = useTheme();
2026-08-26 11:56:50 -05:00
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
// with native auth off there are no users to manage, so the step would lie
const steps = useMemo(
() =>
config?.auth?.enabled === false
? STEP_KEYS.filter((key) => key !== "account")
: STEP_KEYS,
[config],
);
2026-08-26 11:50:44 -05:00
const stepLabels = useMemo(
() => steps.map((key) => `setupWizard.steps.${key}`),
[steps],
);
2026-08-21 18:04:29 -05:00
const isDark = (theme === "system" ? systemTheme : theme) === "dark";
2026-08-18 16:02:26 -05:00
const handleSkipSetup = useCallback(() => {
dismissSetup();
window.location.href = window.baseUrl || "/";
}, []);
2026-08-18 16:02:26 -05:00
const handleCameraNext = useCallback(
(cameraNames?: string[], detectCodecs?: Record<string, string>) => {
if (cameraNames && cameraNames.length > 0) {
dispatch({
type: "CAMERAS_ADDED",
cameraNames,
detectCodecs: detectCodecs ?? {},
});
} else {
dispatch({ type: "SKIP_STEP" });
}
},
[],
);
2026-08-18 16:02:26 -05:00
const handleHwAccelNext = useCallback((saved: boolean) => {
dispatch({ type: "STEP_CONFIGURED", step: "hwaccel", savedConfig: saved });
}, []);
const handleDetectorNext = useCallback((hardwareKey: string) => {
dispatch({ type: "DETECTOR_DONE", configured: true, hardwareKey });
}, []);
const handleDetectorSkip = useCallback((hardwareKey?: string) => {
dispatch({ type: "DETECTOR_DONE", configured: false, hardwareKey });
}, []);
const handleRecordingNext = useCallback(() => {
dispatch({ type: "STEP_CONFIGURED", step: "recording", savedConfig: true });
}, []);
const handleBack = useCallback(() => {
dispatch({ type: "PREV_STEP" });
}, []);
const handleSkipStep = useCallback(() => {
dispatch({ type: "SKIP_STEP" });
}, []);
const renderStep = () => {
2026-08-26 11:50:44 -05:00
switch (steps[state.currentStep]) {
case "welcome":
2026-08-18 16:02:26 -05:00
return (
<SetupWelcome
onNext={() => dispatch({ type: "NEXT_STEP" })}
onSkip={handleSkipSetup}
/>
);
2026-08-26 11:56:50 -05:00
case "account":
return (
<SetupAccount
onNext={handleSkipStep}
onBack={handleBack}
onSkip={handleSkipStep}
/>
);
2026-08-26 11:50:44 -05:00
case "camera":
2026-08-18 16:02:26 -05:00
return <SetupCamera onNext={handleCameraNext} onBack={handleBack} />;
2026-08-26 11:50:44 -05:00
case "detector":
2026-08-18 16:02:26 -05:00
return (
<SetupDetector
cameraCount={state.cameraNames.length}
onNext={handleDetectorNext}
onBack={handleBack}
onSkip={handleDetectorSkip}
/>
);
2026-08-26 11:50:44 -05:00
case "hwaccel":
2026-08-18 16:02:26 -05:00
return (
<SetupHwAccel
detectorHardwareKey={state.detectorHardwareKey}
detectCodecs={state.detectCodecs}
2026-08-18 16:02:26 -05:00
onNext={handleHwAccelNext}
onBack={handleBack}
onSkip={handleSkipStep}
/>
);
2026-08-26 11:50:44 -05:00
case "recording":
2026-08-18 16:02:26 -05:00
return (
<SetupRecording
cameraNames={state.cameraNames}
onNext={handleRecordingNext}
onBack={handleBack}
onSkip={handleSkipStep}
/>
);
2026-08-26 11:50:44 -05:00
case "complete":
2026-08-18 16:02:26 -05:00
return (
<SetupComplete
cameraNames={state.cameraNames}
configuredSteps={state.configuredSteps}
restartRequired={state.restartRequired}
onBack={handleBack}
/>
);
default:
return null;
}
};
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
2026-08-21 18:04:29 -05:00
<Button
type="button"
variant="ghost"
size="icon"
className="fixed right-4 top-4 text-muted-foreground hover:text-primary"
aria-label={t(isDark ? "menu.darkMode.light" : "menu.darkMode.dark", {
ns: "common",
})}
onClick={() => setTheme(isDark ? "light" : "dark")}
>
{isDark ? <LuSun className="size-4" /> : <LuMoon className="size-4" />}
</Button>
2026-08-26 16:33:32 -05:00
<Card className="w-full max-w-lg bg-background_alt">
2026-08-18 16:02:26 -05:00
<CardContent className="p-6">
<StepIndicator
2026-08-26 11:50:44 -05:00
steps={stepLabels}
2026-08-18 16:02:26 -05:00
currentStep={state.currentStep}
variant="dots"
translationNameSpace="views/setup"
className="mb-4 justify-start"
/>
<div className="fade-in">{renderStep()}</div>
</CardContent>
</Card>
</div>
);
}