mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-13 15:31:14 +03:00
radio and checkbox tweaks
This commit is contained in:
parent
16b2396565
commit
eb1aad0421
@ -37,12 +37,14 @@ import { cn } from "@/lib/utils";
|
|||||||
import { isReplayCamera, processCameraName } from "@/utils/cameraUtil";
|
import { isReplayCamera, processCameraName } from "@/utils/cameraUtil";
|
||||||
import type { FrigateConfig } from "@/types/frigateConfig";
|
import type { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { LuTriangleAlert } from "react-icons/lu";
|
import { LuTriangleAlert } from "react-icons/lu";
|
||||||
import {
|
import {
|
||||||
CLONE_CATEGORIES,
|
CLONE_CATEGORIES,
|
||||||
type CloneCategoryKey,
|
type CloneCategoryKey,
|
||||||
type CloneCategoryGroup,
|
type CloneCategoryGroup,
|
||||||
|
type RawCameraPaths,
|
||||||
getCategoryDefaults,
|
getCategoryDefaults,
|
||||||
resolutionsMatch,
|
resolutionsMatch,
|
||||||
buildClonedCameraPayloads,
|
buildClonedCameraPayloads,
|
||||||
@ -74,6 +76,7 @@ export default function CloneCameraDialog({
|
|||||||
}: CloneCameraDialogProps) {
|
}: CloneCameraDialogProps) {
|
||||||
const { t } = useTranslation(["views/settings", "common"]);
|
const { t } = useTranslation(["views/settings", "common"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const { data: rawPaths } = useSWR<RawCameraPaths>("config/raw_paths");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const otherCameras = useMemo(() => {
|
const otherCameras = useMemo(() => {
|
||||||
@ -225,16 +228,18 @@ export default function CloneCameraDialog({
|
|||||||
selectedKeys: selectedCategories,
|
selectedKeys: selectedCategories,
|
||||||
fullConfig: config,
|
fullConfig: config,
|
||||||
fullSchema,
|
fullSchema,
|
||||||
|
rawPaths,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
config,
|
config,
|
||||||
fullSchema,
|
fullSchema,
|
||||||
srcCfg,
|
srcCfg,
|
||||||
|
sourceCamera,
|
||||||
targetIsNew,
|
targetIsNew,
|
||||||
existingTarget,
|
existingTarget,
|
||||||
watchedNewName,
|
watchedNewName,
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
sourceCamera,
|
rawPaths,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const previewTarget = targetIsNew
|
const previewTarget = targetIsNew
|
||||||
@ -388,10 +393,10 @@ export default function CloneCameraDialog({
|
|||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<fieldset className="space-y-3">
|
<div className="space-y-3">
|
||||||
<legend className="text-sm font-medium">
|
<Label className="text-base">
|
||||||
{t("cameraManagement.clone.target.legend")}
|
{t("cameraManagement.clone.target.legend")}
|
||||||
</legend>
|
</Label>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="targetMode"
|
name="targetMode"
|
||||||
@ -404,17 +409,25 @@ export default function CloneCameraDialog({
|
|||||||
className="space-y-3"
|
className="space-y-3"
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center space-x-3 space-y-0">
|
||||||
<RadioGroupItem value="new" id="clone-target-new" />
|
<RadioGroupItem
|
||||||
<label
|
value="new"
|
||||||
|
id="clone-target-new"
|
||||||
|
className={
|
||||||
|
targetMode === "new"
|
||||||
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
||||||
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
htmlFor="clone-target-new"
|
htmlFor="clone-target-new"
|
||||||
className="text-sm"
|
className="font-normal"
|
||||||
>
|
>
|
||||||
{t("cameraManagement.clone.target.newRadio")}
|
{t("cameraManagement.clone.target.newRadio")}
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{targetMode === "new" && (
|
{targetMode === "new" && (
|
||||||
<FormItem className="ml-6">
|
<FormItem className="ml-7">
|
||||||
<FormLabel className="sr-only">
|
<FormLabel className="sr-only">
|
||||||
{t(
|
{t(
|
||||||
"cameraManagement.clone.target.newNameLabel",
|
"cameraManagement.clone.target.newNameLabel",
|
||||||
@ -446,16 +459,21 @@ export default function CloneCameraDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center space-x-3 space-y-0">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
value="existing"
|
value="existing"
|
||||||
id="clone-target-existing"
|
id="clone-target-existing"
|
||||||
disabled={otherCameras.length === 0}
|
disabled={otherCameras.length === 0}
|
||||||
|
className={
|
||||||
|
targetMode === "existing"
|
||||||
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
||||||
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<label
|
<Label
|
||||||
htmlFor="clone-target-existing"
|
htmlFor="clone-target-existing"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"font-normal",
|
||||||
otherCameras.length === 0 &&
|
otherCameras.length === 0 &&
|
||||||
"text-muted-foreground",
|
"text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
@ -470,7 +488,7 @@ export default function CloneCameraDialog({
|
|||||||
)
|
)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{targetMode === "existing" &&
|
{targetMode === "existing" &&
|
||||||
otherCameras.length > 0 && (
|
otherCameras.length > 0 && (
|
||||||
@ -478,7 +496,7 @@ export default function CloneCameraDialog({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="existingTarget"
|
name="existingTarget"
|
||||||
render={({ field: tgtField }) => (
|
render={({ field: tgtField }) => (
|
||||||
<FormItem className="ml-6">
|
<FormItem className="ml-7">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
value={tgtField.value}
|
value={tgtField.value}
|
||||||
@ -512,29 +530,34 @@ export default function CloneCameraDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</div>
|
||||||
|
|
||||||
<fieldset className="space-y-4">
|
<div className="space-y-4">
|
||||||
<legend className="text-sm font-medium">
|
<Label className="text-base">
|
||||||
{t("cameraManagement.clone.categories.legend")}
|
{t("cameraManagement.clone.categories.legend")}
|
||||||
</legend>
|
</Label>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<Label className="font-medium">
|
||||||
{t("cameraManagement.clone.categories.general")}
|
{t("cameraManagement.clone.categories.general")}
|
||||||
</p>
|
</Label>
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 rounded-lg bg-secondary p-4 sm:grid-cols-2">
|
||||||
{groupedCategories.general.map((cat) => (
|
{groupedCategories.general.map((cat) => (
|
||||||
<label
|
<label
|
||||||
key={cat.key}
|
key={cat.key}
|
||||||
className="flex items-center gap-2 text-sm"
|
className="flex flex-row items-center space-x-3 space-y-0 text-sm font-normal"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
|
||||||
checked={selectedCategories.has(cat.key)}
|
checked={selectedCategories.has(cat.key)}
|
||||||
onCheckedChange={() => toggleCategory(cat.key)}
|
onCheckedChange={() => toggleCategory(cat.key)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
{t(`cameraManagement.clone.categories.items.${cat.key}`)}
|
<span>
|
||||||
|
{t(
|
||||||
|
`cameraManagement.clone.categories.items.${cat.key}`,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -542,23 +565,28 @@ export default function CloneCameraDialog({
|
|||||||
|
|
||||||
{groupedCategories.spatial.length > 0 && (
|
{groupedCategories.spatial.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<Label className="font-medium">
|
||||||
{t("cameraManagement.clone.categories.spatial")}
|
{t("cameraManagement.clone.categories.spatial")}
|
||||||
</p>
|
</Label>
|
||||||
{!targetIsNew &&
|
{!targetIsNew &&
|
||||||
!resMatch &&
|
!resMatch &&
|
||||||
srcCfg?.detect &&
|
srcCfg?.detect &&
|
||||||
dstCfg?.detect && (
|
dstCfg?.detect && (
|
||||||
<Alert
|
<Alert variant="warning">
|
||||||
variant="default"
|
<LuTriangleAlert className="size-5" />
|
||||||
role="alert"
|
<AlertTitle>
|
||||||
className="border-warning"
|
{t(
|
||||||
>
|
"cameraManagement.clone.categories.spatialWarningTitle",
|
||||||
<LuTriangleAlert className="size-4" />
|
)}
|
||||||
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t(
|
{t(
|
||||||
"cameraManagement.clone.categories.spatialWarning",
|
"cameraManagement.clone.categories.spatialWarning",
|
||||||
{
|
{
|
||||||
|
srcCamera: sourceFriendlyName,
|
||||||
|
dstCamera:
|
||||||
|
config?.cameras?.[existingTarget]
|
||||||
|
?.friendly_name ?? existingTarget,
|
||||||
srcWidth: srcCfg.detect.width,
|
srcWidth: srcCfg.detect.width,
|
||||||
srcHeight: srcCfg.detect.height,
|
srcHeight: srcCfg.detect.height,
|
||||||
dstWidth: dstCfg.detect.width,
|
dstWidth: dstCfg.detect.width,
|
||||||
@ -568,20 +596,23 @@ export default function CloneCameraDialog({
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 rounded-lg bg-secondary p-4 sm:grid-cols-2">
|
||||||
{groupedCategories.spatial.map((cat) => (
|
{groupedCategories.spatial.map((cat) => (
|
||||||
<label
|
<label
|
||||||
key={cat.key}
|
key={cat.key}
|
||||||
className="flex items-center gap-2 text-sm"
|
className="flex flex-row items-center space-x-3 space-y-0 text-sm font-normal"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
|
||||||
checked={selectedCategories.has(cat.key)}
|
checked={selectedCategories.has(cat.key)}
|
||||||
onCheckedChange={() => toggleCategory(cat.key)}
|
onCheckedChange={() => toggleCategory(cat.key)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
{t(
|
<span>
|
||||||
`cameraManagement.clone.categories.items.${cat.key}`,
|
{t(
|
||||||
)}
|
`cameraManagement.clone.categories.items.${cat.key}`,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -590,55 +621,74 @@ export default function CloneCameraDialog({
|
|||||||
|
|
||||||
{targetIsNew && groupedCategories.streams.length > 0 && (
|
{targetIsNew && groupedCategories.streams.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<Label className="font-medium">
|
||||||
{t("cameraManagement.clone.categories.streams")}
|
{t("cameraManagement.clone.categories.streams")}
|
||||||
</p>
|
</Label>
|
||||||
<div className="grid grid-cols-1 gap-2">
|
<div className="grid grid-cols-1 gap-3 rounded-lg bg-secondary p-4">
|
||||||
{groupedCategories.streams.map((cat) => (
|
{groupedCategories.streams.map((cat) => (
|
||||||
<label
|
<label
|
||||||
key={cat.key}
|
key={cat.key}
|
||||||
className="flex items-center gap-2 text-sm text-muted-foreground"
|
className="flex flex-row items-center space-x-3 space-y-0 text-sm font-normal text-muted-foreground"
|
||||||
>
|
>
|
||||||
<Checkbox checked disabled />
|
<Checkbox
|
||||||
{t(
|
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
|
||||||
`cameraManagement.clone.categories.items.${cat.key}`,
|
checked
|
||||||
)}
|
disabled
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{t(
|
||||||
|
`cameraManagement.clone.categories.items.${cat.key}`,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</fieldset>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<DialogFooter className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between sm:space-x-0">
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex flex-col gap-1 text-sm text-muted-foreground">
|
||||||
<span>
|
|
||||||
{t("cameraManagement.clone.footer.changeCount", {
|
|
||||||
count: changeCount,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
{changeCount > 0 && (
|
{changeCount > 0 && (
|
||||||
<SaveAllPreviewPopover
|
<>
|
||||||
items={previewItems}
|
<div className="flex items-center gap-1">
|
||||||
className="h-7 w-7"
|
<span>
|
||||||
align="start"
|
{t("cameraManagement.clone.footer.changeCount", {
|
||||||
side="top"
|
count: changeCount,
|
||||||
/>
|
})}
|
||||||
|
</span>
|
||||||
|
{changeCount > 0 && (
|
||||||
|
<SaveAllPreviewPopover
|
||||||
|
items={previewItems}
|
||||||
|
className="h-7 w-7"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
disablePortal
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs">
|
||||||
|
{anyNeedsRestart
|
||||||
|
? t("cameraManagement.clone.footer.restartNeeded")
|
||||||
|
: t("cameraManagement.clone.footer.liveOnly")}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<span className="ml-2 text-xs">
|
|
||||||
{anyNeedsRestart
|
|
||||||
? t("cameraManagement.clone.footer.restartNeeded")
|
|
||||||
: t("cameraManagement.clone.footer.liveOnly")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row">
|
||||||
<Button type="button" disabled={isSubmitting} onClick={onClose}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
{t("button.cancel", { ns: "common" })}
|
{t("button.cancel", { ns: "common" })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting || changeCount === 0}
|
disabled={isSubmitting || changeCount === 0}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user