diff --git a/web/e2e/specs/settings/ui-settings-transfer.spec.ts b/web/e2e/specs/settings/ui-settings-transfer.spec.ts index ac61ea953e..bcae868054 100644 --- a/web/e2e/specs/settings/ui-settings-transfer.spec.ts +++ b/web/e2e/specs/settings/ui-settings-transfer.spec.ts @@ -181,6 +181,7 @@ test.describe("UI settings import/export @medium", () => { test("round-trips a layout left unconverted by an upgrade", async ({ frigateApp, }) => { + test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only"); // DraggableGridLayout rewrites a pre-0.19 layout only when that group's // dashboard is opened, so exporting first carries the bare array into the // 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 ({ frigateApp, }) => { + test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only"); // Bare-array layouts only render in bucketed mode; with natural aspect on // 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 @@ -256,6 +258,7 @@ test.describe("UI settings import/export @medium", () => { test("natural aspect layouts import turns the setting on", async ({ frigateApp, }) => { + test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only"); await frigateApp.goto("/settings?page=uiSettings"); await chooseImportFile( @@ -314,12 +317,15 @@ test.describe("UI settings import/export @medium", () => { await expect( frigateApp.page.getByText("UI preferences (2 settings)"), ).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); expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual( - OUTDOOR_LAYOUT, + frigateApp.isMobile ? null : OUTDOOR_LAYOUT, ); expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual( 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 ({ frigateApp, }) => { + test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only"); await frigateApp.goto("/settings?page=uiSettings"); // 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, 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); expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual( - OUTDOOR_LAYOUT, + frigateApp.isMobile ? null : OUTDOOR_LAYOUT, ); expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual( STREAMING_SETTINGS, diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 21fa7adb31..2e551f2e4b 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -200,6 +200,7 @@ "desc": "Choose what to apply from this file. Frigate will reload when the import finishes.", "exportedFrom": "Exported {{date}} from Frigate config version {{version}}", "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\".", "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)", diff --git a/web/src/components/overlay/dialog/ImportUiSettingsDialog.tsx b/web/src/components/overlay/dialog/ImportUiSettingsDialog.tsx index 8d13195842..c26cc397ee 100644 --- a/web/src/components/overlay/dialog/ImportUiSettingsDialog.tsx +++ b/web/src/components/overlay/dialog/ImportUiSettingsDialog.tsx @@ -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({ -
-

{fileName}

-

- {t("general.backupRestore.importDialog.exportedFrom", { - date: exportedDate, - version: file.frigate_version, - })} -

+
+
+ +
+

+ {fileName} +

+

+ {t("general.backupRestore.importDialog.exportedFrom", { + date: exportedDate, + version: file.frigate_version, + })} +

+
+
+ +
+ + setSections((prev) => ({ ...prev, layouts: checked })) + } + /> + + setSections((prev) => ({ ...prev, streaming: checked })) + } + /> + + setSections((prev) => ({ ...prev, preferences: checked })) + } + /> +
+ + {isMobileOnly && summary.layoutGroupCount > 0 && ( + + + + {t("general.backupRestore.importDialog.layoutsPhone")} + + + )} + + {layoutsModeChange !== null && ( + + + + {t( + layoutsModeChange + ? "general.backupRestore.importDialog.layoutsModeOn" + : "general.backupRestore.importDialog.layoutsModeOff", + )} + + + )} + + {(visibleUnknownGroups.length > 0 || + visibleUnknownCameras.length > 0) && ( + + + + {visibleUnknownGroups.length > 0 && ( +

+ {t("general.backupRestore.importDialog.unknownGroups", { + count: visibleUnknownGroups.length, + groups: visibleUnknownGroups.join(", "), + })} +

+ )} + {visibleUnknownCameras.length > 0 && ( +

+ {t("general.backupRestore.importDialog.unknownCameras", { + count: visibleUnknownCameras.length, + cameras: visibleUnknownCameras.join(", "), + })} +

+ )} +
+
+ )}
-
- - setSections((prev) => ({ ...prev, layouts: checked })) - } - /> - - setSections((prev) => ({ ...prev, streaming: checked })) - } - /> - - setSections((prev) => ({ ...prev, preferences: checked })) - } - /> -
- - {layoutsModeChange !== null && ( - - - - {t( - layoutsModeChange - ? "general.backupRestore.importDialog.layoutsModeOn" - : "general.backupRestore.importDialog.layoutsModeOff", - )} - - - )} - - {(visibleUnknownGroups.length > 0 || - visibleUnknownCameras.length > 0) && ( - - - - {visibleUnknownGroups.length > 0 && ( -

- {t("general.backupRestore.importDialog.unknownGroups", { - count: visibleUnknownGroups.length, - groups: visibleUnknownGroups.join(", "), - })} -

- )} - {visibleUnknownCameras.length > 0 && ( -

- {t("general.backupRestore.importDialog.unknownCameras", { - count: visibleUnknownCameras.length, - cameras: visibleUnknownCameras.join(", "), - })} -

- )} -
-
- )} -