hide natural aspect and layout import on phones

This commit is contained in:
Josh Hawkins
2026-09-24 13:27:29 -05:00
parent e760868048
commit 5ddcf96756
5 changed files with 163 additions and 96 deletions
@@ -10,7 +10,8 @@ import {
} from "@/components/ui/dialog";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { LuInfo, LuTriangleAlert } from "react-icons/lu";
import { LuFileJson, LuInfo, LuTriangleAlert } from "react-icons/lu";
import { isMobileOnly } from "react-device-detect";
import FilterSwitch from "@/components/filter/FilterSwitch";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import {
@@ -44,7 +45,9 @@ export default function ImportUiSettingsDialog({
const available = useMemo(
() => ({
layouts: summary.layoutGroupCount > 0,
// phones use the static grid, so a saved grid layout has nothing to
// apply to and would only flip the tile sizing mode behind the scenes
layouts: !isMobileOnly && summary.layoutGroupCount > 0,
streaming: summary.streamingCameraCount > 0,
preferences: summary.preferenceCount > 0,
}),
@@ -126,87 +129,103 @@ export default function ImportUiSettingsDialog({
</DialogDescription>
</DialogHeader>
<div className="space-y-0.5">
<p className="break-all text-base text-primary-variant">{fileName}</p>
<p className="text-sm text-muted-foreground">
{t("general.backupRestore.importDialog.exportedFrom", {
date: exportedDate,
version: file.frigate_version,
})}
</p>
<div className="space-y-4">
<div className="flex items-start gap-3 rounded-lg bg-secondary p-3">
<LuFileJson className="mt-0.5 size-5 shrink-0 text-secondary-foreground" />
<div className="min-w-0">
<p className="break-all text-base font-medium text-primary-variant">
{fileName}
</p>
<p className="text-xs text-muted-foreground">
{t("general.backupRestore.importDialog.exportedFrom", {
date: exportedDate,
version: file.frigate_version,
})}
</p>
</div>
</div>
<div className="space-y-2.5">
<FilterSwitch
label={t("general.backupRestore.importDialog.layouts", {
count: summary.layoutGroupCount,
})}
isChecked={sections.layouts}
disabled={!available.layouts || isImporting}
onCheckedChange={(checked) =>
setSections((prev) => ({ ...prev, layouts: checked }))
}
/>
<FilterSwitch
label={t("general.backupRestore.importDialog.streaming", {
count: summary.streamingCameraCount,
})}
isChecked={sections.streaming}
disabled={!available.streaming || isImporting}
onCheckedChange={(checked) =>
setSections((prev) => ({ ...prev, streaming: checked }))
}
/>
<FilterSwitch
label={t("general.backupRestore.importDialog.preferences", {
count: summary.preferenceCount,
})}
isChecked={sections.preferences}
disabled={!available.preferences || isImporting}
onCheckedChange={(checked) =>
setSections((prev) => ({ ...prev, preferences: checked }))
}
/>
</div>
{isMobileOnly && summary.layoutGroupCount > 0 && (
<Alert variant="info">
<LuInfo className="size-5" />
<AlertDescription>
{t("general.backupRestore.importDialog.layoutsPhone")}
</AlertDescription>
</Alert>
)}
{layoutsModeChange !== null && (
<Alert variant="info">
<LuInfo className="size-5" />
<AlertDescription>
{t(
layoutsModeChange
? "general.backupRestore.importDialog.layoutsModeOn"
: "general.backupRestore.importDialog.layoutsModeOff",
)}
</AlertDescription>
</Alert>
)}
{(visibleUnknownGroups.length > 0 ||
visibleUnknownCameras.length > 0) && (
<Alert variant="warning">
<LuTriangleAlert className="size-5" />
<AlertDescription className="space-y-2">
{visibleUnknownGroups.length > 0 && (
<p>
{t("general.backupRestore.importDialog.unknownGroups", {
count: visibleUnknownGroups.length,
groups: visibleUnknownGroups.join(", "),
})}
</p>
)}
{visibleUnknownCameras.length > 0 && (
<p>
{t("general.backupRestore.importDialog.unknownCameras", {
count: visibleUnknownCameras.length,
cameras: visibleUnknownCameras.join(", "),
})}
</p>
)}
</AlertDescription>
</Alert>
)}
</div>
<div className="space-y-3">
<FilterSwitch
label={t("general.backupRestore.importDialog.layouts", {
count: summary.layoutGroupCount,
})}
isChecked={sections.layouts}
disabled={!available.layouts || isImporting}
onCheckedChange={(checked) =>
setSections((prev) => ({ ...prev, layouts: checked }))
}
/>
<FilterSwitch
label={t("general.backupRestore.importDialog.streaming", {
count: summary.streamingCameraCount,
})}
isChecked={sections.streaming}
disabled={!available.streaming || isImporting}
onCheckedChange={(checked) =>
setSections((prev) => ({ ...prev, streaming: checked }))
}
/>
<FilterSwitch
label={t("general.backupRestore.importDialog.preferences", {
count: summary.preferenceCount,
})}
isChecked={sections.preferences}
disabled={!available.preferences || isImporting}
onCheckedChange={(checked) =>
setSections((prev) => ({ ...prev, preferences: checked }))
}
/>
</div>
{layoutsModeChange !== null && (
<Alert variant="info">
<LuInfo className="size-5" />
<AlertDescription>
{t(
layoutsModeChange
? "general.backupRestore.importDialog.layoutsModeOn"
: "general.backupRestore.importDialog.layoutsModeOff",
)}
</AlertDescription>
</Alert>
)}
{(visibleUnknownGroups.length > 0 ||
visibleUnknownCameras.length > 0) && (
<Alert variant="warning">
<LuTriangleAlert className="size-5" />
<AlertDescription className="space-y-2">
{visibleUnknownGroups.length > 0 && (
<p>
{t("general.backupRestore.importDialog.unknownGroups", {
count: visibleUnknownGroups.length,
groups: visibleUnknownGroups.join(", "),
})}
</p>
)}
{visibleUnknownCameras.length > 0 && (
<p>
{t("general.backupRestore.importDialog.unknownCameras", {
count: visibleUnknownCameras.length,
cameras: visibleUnknownCameras.join(", "),
})}
</p>
)}
</AlertDescription>
</Alert>
)}
<DialogFooter>
<Button
aria-label={t("button.cancel", { ns: "common" })}
@@ -224,7 +243,7 @@ export default function ImportUiSettingsDialog({
>
{isImporting ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<ActivityIndicator className="size-4" />
<span>{t("general.backupRestore.importDialog.confirm")}</span>
</div>
) : (
+4 -2
View File
@@ -33,7 +33,7 @@ import { ASPECT_WIDE_LAYOUT } from "@/types/record";
import { Skeleton } from "@/components/ui/skeleton";
import { isEqual } from "lodash";
import useSWR from "swr";
import { isDesktop, isMobile } from "react-device-detect";
import { isDesktop, isMobile, isMobileOnly } from "react-device-detect";
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
import LivePlayer from "@/components/player/LivePlayer";
import { IoClose } from "react-icons/io5";
@@ -152,7 +152,9 @@ export default function DraggableGridLayout({
"naturalAspectLayout",
false,
);
const naturalAspectLayout = naturalAspectSetting ?? false;
// phones never reach this grid, and the setting is hidden there, so an
// imported or stale value must not take effect
const naturalAspectLayout = !isMobileOnly && (naturalAspectSetting ?? false);
// Bucketed mode snaps every camera to one of three tile shapes, matching the
// pre-masonry layout; the picture letterboxes inside its bucket.
+14 -8
View File
@@ -19,6 +19,7 @@ import {
useUserPersistence,
deleteUserNamespacedKey,
} from "@/hooks/use-user-persistence";
import { isMobileOnly, isSafari } from "react-device-detect";
import {
Select,
SelectContent,
@@ -368,14 +369,19 @@ export default function UiSettingsView() {
checked: cameraNames,
onCheckedChange: setCameraName,
},
{
id: "natural-aspect",
label: t("general.liveDashboard.naturalAspectLayout.label"),
description: t("general.liveDashboard.naturalAspectLayout.desc"),
note: t("general.liveDashboard.naturalAspectLayout.descNote"),
checked: naturalAspect,
onCheckedChange: setNaturalAspect,
},
// phones use the static grid, so tile sizing has nothing to affect there
...(isMobileOnly
? []
: [
{
id: "natural-aspect",
label: t("general.liveDashboard.naturalAspectLayout.label"),
description: t("general.liveDashboard.naturalAspectLayout.desc"),
note: t("general.liveDashboard.naturalAspectLayout.descNote"),
checked: naturalAspect,
onCheckedChange: setNaturalAspect,
},
]),
];
return (