mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-28 10:56:52 +03:00
hide natural aspect and layout import on phones
This commit is contained in:
@@ -181,6 +181,7 @@ test.describe("UI settings import/export @medium", () => {
|
|||||||
test("round-trips a layout left unconverted by an upgrade", async ({
|
test("round-trips a layout left unconverted by an upgrade", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
|
test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only");
|
||||||
// DraggableGridLayout rewrites a pre-0.19 layout only when that group's
|
// DraggableGridLayout rewrites a pre-0.19 layout only when that group's
|
||||||
// dashboard is opened, so exporting first carries the bare array into the
|
// dashboard is opened, so exporting first carries the bare array into the
|
||||||
// file. Import must accept it back rather than rejecting the whole file.
|
// file. Import must accept it back rather than rejecting the whole file.
|
||||||
@@ -216,6 +217,7 @@ test.describe("UI settings import/export @medium", () => {
|
|||||||
test("legacy layouts import turns natural aspect off so they display", async ({
|
test("legacy layouts import turns natural aspect off so they display", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
|
test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only");
|
||||||
// Bare-array layouts only render in bucketed mode; with natural aspect on
|
// Bare-array layouts only render in bucketed mode; with natural aspect on
|
||||||
// they would be discarded and regenerated on the next dashboard visit. The
|
// they would be discarded and regenerated on the next dashboard visit. The
|
||||||
// import applies the mode the layouts were built for, and the file's own
|
// import applies the mode the layouts were built for, and the file's own
|
||||||
@@ -256,6 +258,7 @@ test.describe("UI settings import/export @medium", () => {
|
|||||||
test("natural aspect layouts import turns the setting on", async ({
|
test("natural aspect layouts import turns the setting on", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
|
test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only");
|
||||||
await frigateApp.goto("/settings?page=uiSettings");
|
await frigateApp.goto("/settings?page=uiSettings");
|
||||||
|
|
||||||
await chooseImportFile(
|
await chooseImportFile(
|
||||||
@@ -314,12 +317,15 @@ test.describe("UI settings import/export @medium", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
frigateApp.page.getByText("UI preferences (2 settings)"),
|
frigateApp.page.getByText("UI preferences (2 settings)"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(frigateApp.page.getByText(/patio/)).toBeVisible();
|
// patio is layout-only, so its warning follows the layouts section
|
||||||
|
await expect(frigateApp.page.getByText(/patio/)).toBeVisible({
|
||||||
|
visible: !frigateApp.isMobile,
|
||||||
|
});
|
||||||
|
|
||||||
await confirmImport(frigateApp.page);
|
await confirmImport(frigateApp.page);
|
||||||
|
|
||||||
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
||||||
OUTDOOR_LAYOUT,
|
frigateApp.isMobile ? null : OUTDOOR_LAYOUT,
|
||||||
);
|
);
|
||||||
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual(
|
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual(
|
||||||
STREAMING_SETTINGS,
|
STREAMING_SETTINGS,
|
||||||
@@ -330,6 +336,7 @@ test.describe("UI settings import/export @medium", () => {
|
|||||||
test("hides the unknown-group warning when layouts are switched off", async ({
|
test("hides the unknown-group warning when layouts are switched off", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
|
test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only");
|
||||||
await frigateApp.goto("/settings?page=uiSettings");
|
await frigateApp.goto("/settings?page=uiSettings");
|
||||||
|
|
||||||
// patio is a layout-only group absent from this server, so the warning
|
// patio is a layout-only group absent from this server, so the warning
|
||||||
@@ -359,7 +366,39 @@ test.describe("UI settings import/export @medium", () => {
|
|||||||
|
|
||||||
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual({});
|
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual({});
|
||||||
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
||||||
OUTDOOR_LAYOUT,
|
frigateApp.isMobile ? null : OUTDOOR_LAYOUT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("phones refuse layouts and say why @mobile", async ({ frigateApp }) => {
|
||||||
|
test.skip(!frigateApp.isMobile, "Phone-only");
|
||||||
|
|
||||||
|
await frigateApp.goto("/settings?page=uiSettings");
|
||||||
|
await writeIdb(frigateApp.page, { "naturalAspectLayout:admin": false });
|
||||||
|
|
||||||
|
await chooseImportFile(frigateApp.page, importPayload());
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
frigateApp.page.getByText(/aren't imported on phones/),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// the section is still listed, but cannot be switched on
|
||||||
|
await expect(
|
||||||
|
frigateApp.page.getByText("Camera group layouts (2 groups)"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
frigateApp.page.locator('[id="Camera group layouts (2 groups)"]'),
|
||||||
|
).toBeDisabled();
|
||||||
|
|
||||||
|
await confirmImport(frigateApp.page);
|
||||||
|
|
||||||
|
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toBeNull();
|
||||||
|
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual(
|
||||||
|
STREAMING_SETTINGS,
|
||||||
|
);
|
||||||
|
// a layouts import is what flips this, so it must stay put
|
||||||
|
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -449,7 +488,7 @@ test.describe("UI settings import/export @medium", () => {
|
|||||||
await confirmImport(frigateApp.page);
|
await confirmImport(frigateApp.page);
|
||||||
|
|
||||||
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
||||||
OUTDOOR_LAYOUT,
|
frigateApp.isMobile ? null : OUTDOOR_LAYOUT,
|
||||||
);
|
);
|
||||||
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual(
|
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual(
|
||||||
STREAMING_SETTINGS,
|
STREAMING_SETTINGS,
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
"desc": "Choose what to apply from this file. Frigate will reload when the import finishes.",
|
"desc": "Choose what to apply from this file. Frigate will reload when the import finishes.",
|
||||||
"exportedFrom": "Exported {{date}} from Frigate config version {{version}}",
|
"exportedFrom": "Exported {{date}} from Frigate config version {{version}}",
|
||||||
"layouts_one": "Camera group layouts ({{count}} group)",
|
"layouts_one": "Camera group layouts ({{count}} group)",
|
||||||
|
"layoutsPhone": "Camera group layouts aren't imported on phones, which always use the standard grid.",
|
||||||
"layoutsModeOn": "These layouts use camera aspect ratio tile sizing, so importing them will also turn on \"Use Camera Aspect Ratios\".",
|
"layoutsModeOn": "These layouts use camera aspect ratio tile sizing, so importing them will also turn on \"Use Camera Aspect Ratios\".",
|
||||||
"layoutsModeOff": "These layouts use standard tile sizing, so importing them will also turn off \"Use Camera Aspect Ratios\".",
|
"layoutsModeOff": "These layouts use standard tile sizing, so importing them will also turn off \"Use Camera Aspect Ratios\".",
|
||||||
"layouts_other": "Camera group layouts ({{count}} groups)",
|
"layouts_other": "Camera group layouts ({{count}} groups)",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
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 FilterSwitch from "@/components/filter/FilterSwitch";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import {
|
import {
|
||||||
@@ -44,7 +45,9 @@ export default function ImportUiSettingsDialog({
|
|||||||
|
|
||||||
const available = useMemo(
|
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,
|
streaming: summary.streamingCameraCount > 0,
|
||||||
preferences: summary.preferenceCount > 0,
|
preferences: summary.preferenceCount > 0,
|
||||||
}),
|
}),
|
||||||
@@ -126,87 +129,103 @@ export default function ImportUiSettingsDialog({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-4">
|
||||||
<p className="break-all text-base text-primary-variant">{fileName}</p>
|
<div className="flex items-start gap-3 rounded-lg bg-secondary p-3">
|
||||||
<p className="text-sm text-muted-foreground">
|
<LuFileJson className="mt-0.5 size-5 shrink-0 text-secondary-foreground" />
|
||||||
{t("general.backupRestore.importDialog.exportedFrom", {
|
<div className="min-w-0">
|
||||||
date: exportedDate,
|
<p className="break-all text-base font-medium text-primary-variant">
|
||||||
version: file.frigate_version,
|
{fileName}
|
||||||
})}
|
</p>
|
||||||
</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>
|
||||||
|
|
||||||
<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>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("button.cancel", { ns: "common" })}
|
aria-label={t("button.cancel", { ns: "common" })}
|
||||||
@@ -224,7 +243,7 @@ export default function ImportUiSettingsDialog({
|
|||||||
>
|
>
|
||||||
{isImporting ? (
|
{isImporting ? (
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<ActivityIndicator />
|
<ActivityIndicator className="size-4" />
|
||||||
<span>{t("general.backupRestore.importDialog.confirm")}</span>
|
<span>{t("general.backupRestore.importDialog.confirm")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { ASPECT_WIDE_LAYOUT } from "@/types/record";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import useSWR from "swr";
|
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 BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
||||||
import LivePlayer from "@/components/player/LivePlayer";
|
import LivePlayer from "@/components/player/LivePlayer";
|
||||||
import { IoClose } from "react-icons/io5";
|
import { IoClose } from "react-icons/io5";
|
||||||
@@ -152,7 +152,9 @@ export default function DraggableGridLayout({
|
|||||||
"naturalAspectLayout",
|
"naturalAspectLayout",
|
||||||
false,
|
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
|
// Bucketed mode snaps every camera to one of three tile shapes, matching the
|
||||||
// pre-masonry layout; the picture letterboxes inside its bucket.
|
// pre-masonry layout; the picture letterboxes inside its bucket.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
useUserPersistence,
|
useUserPersistence,
|
||||||
deleteUserNamespacedKey,
|
deleteUserNamespacedKey,
|
||||||
} from "@/hooks/use-user-persistence";
|
} from "@/hooks/use-user-persistence";
|
||||||
|
import { isMobileOnly, isSafari } from "react-device-detect";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -368,14 +369,19 @@ export default function UiSettingsView() {
|
|||||||
checked: cameraNames,
|
checked: cameraNames,
|
||||||
onCheckedChange: setCameraName,
|
onCheckedChange: setCameraName,
|
||||||
},
|
},
|
||||||
{
|
// phones use the static grid, so tile sizing has nothing to affect there
|
||||||
id: "natural-aspect",
|
...(isMobileOnly
|
||||||
label: t("general.liveDashboard.naturalAspectLayout.label"),
|
? []
|
||||||
description: t("general.liveDashboard.naturalAspectLayout.desc"),
|
: [
|
||||||
note: t("general.liveDashboard.naturalAspectLayout.descNote"),
|
{
|
||||||
checked: naturalAspect,
|
id: "natural-aspect",
|
||||||
onCheckedChange: setNaturalAspect,
|
label: t("general.liveDashboard.naturalAspectLayout.label"),
|
||||||
},
|
description: t("general.liveDashboard.naturalAspectLayout.desc"),
|
||||||
|
note: t("general.liveDashboard.naturalAspectLayout.descNote"),
|
||||||
|
checked: naturalAspect,
|
||||||
|
onCheckedChange: setNaturalAspect,
|
||||||
|
},
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user