mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-28 10:56:52 +03:00
fixes
This commit is contained in:
@@ -38,8 +38,7 @@ test.describe("Masonry live grid @critical", () => {
|
|||||||
test("tiles render at their camera's natural aspect ratio", async ({
|
test("tiles render at their camera's natural aspect ratio", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
// front_door stays 16:9; backyard is overridden to portrait so the two
|
// backyard is 9:16, which bucketed mode would snap to an 8:9 tile
|
||||||
// tiles must render with opposite orientations.
|
|
||||||
await frigateApp.installDefaults({
|
await frigateApp.installDefaults({
|
||||||
config: {
|
config: {
|
||||||
cameras: { backyard: { detect: { width: 720, height: 1280 } } },
|
cameras: { backyard: { detect: { width: 720, height: 1280 } } },
|
||||||
@@ -47,20 +46,24 @@ test.describe("Masonry live grid @critical", () => {
|
|||||||
});
|
});
|
||||||
await frigateApp.goto(`/?group=${GROUP}`);
|
await frigateApp.goto(`/?group=${GROUP}`);
|
||||||
const live = new LivePage(frigateApp.page, true);
|
const live = new LivePage(frigateApp.page, true);
|
||||||
|
|
||||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await seedLayout(frigateApp.page, "naturalAspectLayout:admin", true);
|
||||||
|
await frigateApp.page.reload();
|
||||||
|
await expect(live.cameraCard("backyard").first()).toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
|
||||||
const { front_door: landscape, backyard: portrait } = await cameraBoxes(
|
const { front_door: landscape, backyard: portrait } = await cameraBoxes(
|
||||||
frigateApp.page,
|
frigateApp.page,
|
||||||
["front_door", "backyard"] as const,
|
["front_door", "backyard"] as const,
|
||||||
"card",
|
"card",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 16:9 tile is clearly wider than tall; portrait tile is taller than wide.
|
expect(landscape.w / landscape.h).toBeCloseTo(16 / 9, 1);
|
||||||
expect(landscape.w / landscape.h).toBeGreaterThan(1.4);
|
expect(portrait.w / portrait.h).toBeCloseTo(9 / 16, 1);
|
||||||
expect(portrait.w / portrait.h).toBeLessThan(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("dragging a tile does not shove other tiles far away", async ({
|
test("dragging a tile does not shove other tiles far away", async ({
|
||||||
@@ -210,6 +213,32 @@ test.describe("Masonry live grid @critical", () => {
|
|||||||
.toBeLessThanOrEqual(fresh! + 2);
|
.toBeLessThanOrEqual(fresh! + 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a camera added to a saved layout fills an open column", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
|
await frigateApp.goto(`/?group=${GROUP}`);
|
||||||
|
const live = new LivePage(frigateApp.page, true);
|
||||||
|
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// one tall tile in the first column; backyard is missing from the layout
|
||||||
|
const key = await persistedLayoutKey(frigateApp.page, GROUP);
|
||||||
|
await seedLayout(frigateApp.page, key, {
|
||||||
|
version: 2,
|
||||||
|
naturalAspect: false,
|
||||||
|
layout: [{ i: "front_door", x: 0, y: 0, w: 32, h: 400 }],
|
||||||
|
});
|
||||||
|
await frigateApp.page.reload();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
const stored = await readLayout(frigateApp.page, key);
|
||||||
|
return stored?.layout.find((item) => item.i === "backyard");
|
||||||
|
})
|
||||||
|
.toMatchObject({ x: 32, y: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
test("saved layout from an unreadable version regenerates without error", async ({
|
test("saved layout from an unreadable version regenerates without error", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -178,6 +178,52 @@ test.describe("UI settings import/export @medium", () => {
|
|||||||
expect(payload.sections.preferences.playbackRate).toBe(2);
|
expect(payload.sections.preferences.playbackRate).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("exports only layouts built for the current tile sizing mode", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
|
// a group not opened since the mode changed still holds a layout from
|
||||||
|
// the other mode, which would import into a mode that cannot show it
|
||||||
|
await frigateApp.goto("/settings?page=uiSettings");
|
||||||
|
|
||||||
|
await writeIdb(frigateApp.page, {
|
||||||
|
[OUTDOOR_LAYOUT_KEY]: OUTDOOR_LAYOUT,
|
||||||
|
"default-draggable-layout:admin": NATURAL_OUTDOOR_LAYOUT,
|
||||||
|
"naturalAspectLayout:admin": true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadPromise = frigateApp.page.waitForEvent("download");
|
||||||
|
await frigateApp.page
|
||||||
|
.getByRole("button", { name: "Export Settings" })
|
||||||
|
.click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
const payload = JSON.parse(readFileSync((await download.path())!, "utf-8"));
|
||||||
|
|
||||||
|
expect(payload.sections.layouts).toEqual({
|
||||||
|
default: NATURAL_OUTDOOR_LAYOUT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("toggling tile sizing mode clears stored layouts", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
|
test.skip(frigateApp.isMobile, "The setting is hidden on phones");
|
||||||
|
await frigateApp.goto("/settings?page=uiSettings");
|
||||||
|
await writeIdb(frigateApp.page, { [OUTDOOR_LAYOUT_KEY]: OUTDOOR_LAYOUT });
|
||||||
|
|
||||||
|
await frigateApp.page.locator("#natural-aspect-desktop").click();
|
||||||
|
await frigateApp.page
|
||||||
|
.getByRole("alertdialog")
|
||||||
|
.getByRole("button", { name: "Enable" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(() => readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY))
|
||||||
|
.toBeNull();
|
||||||
|
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("round-trips a layout left unconverted by an upgrade", async ({
|
test("round-trips a layout left unconverted by an upgrade", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ export async function buildExportPayload(
|
|||||||
const layouts: UiSettingsFile["sections"]["layouts"] = {};
|
const layouts: UiSettingsFile["sections"]["layouts"] = {};
|
||||||
let streaming: UiSettingsFile["sections"]["streaming"] = {};
|
let streaming: UiSettingsFile["sections"]["streaming"] = {};
|
||||||
const preferences: UiSettingsFile["sections"]["preferences"] = {};
|
const preferences: UiSettingsFile["sections"]["preferences"] = {};
|
||||||
|
const naturalAspect =
|
||||||
|
(await readTransferable("naturalAspectLayout", true, username)) === true;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groupNames.map(async (group) => {
|
groupNames.map(async (group) => {
|
||||||
@@ -228,7 +230,9 @@ export async function buildExportPayload(
|
|||||||
username,
|
username,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value !== undefined) {
|
// a group not opened since the mode changed still holds a layout from
|
||||||
|
// the other mode, which the grid discards, so leave it out of the file
|
||||||
|
if (value !== undefined && layoutIsNatural(value) === naturalAspect) {
|
||||||
layouts[group] = value;
|
layouts[group] = value;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -399,8 +403,18 @@ export function summarizeImport(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bare arrays are pre-masonry bucketed layouts. A layout only renders under
|
// Bare arrays are pre-masonry bucketed layouts
|
||||||
// the mode that built it, so importing layouts applies this mode too.
|
function layoutIsNatural(layout: unknown): boolean {
|
||||||
|
return (
|
||||||
|
typeof layout === "object" &&
|
||||||
|
layout !== null &&
|
||||||
|
!Array.isArray(layout) &&
|
||||||
|
(layout as { naturalAspect?: unknown }).naturalAspect === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A layout only renders under the mode that built it, so importing layouts
|
||||||
|
// applies this mode too. Exports hold a single mode.
|
||||||
export function importedLayoutsNaturalAspect(
|
export function importedLayoutsNaturalAspect(
|
||||||
file: UiSettingsFile,
|
file: UiSettingsFile,
|
||||||
): boolean | null {
|
): boolean | null {
|
||||||
@@ -410,9 +424,7 @@ export function importedLayoutsNaturalAspect(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return layouts.some(
|
return layouts.some(layoutIsNatural);
|
||||||
(layout) => !Array.isArray(layout) && layout.naturalAspect === true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasImportableContent(summary: ImportSummary): boolean {
|
export function hasImportableContent(summary: ImportSummary): boolean {
|
||||||
|
|||||||
@@ -369,12 +369,18 @@ export default function DraggableGridLayout({
|
|||||||
const placed = new Set(existing.map((layout) => layout.i));
|
const placed = new Set(existing.map((layout) => layout.i));
|
||||||
|
|
||||||
const tileColumns = GRID_COLS / TILE_BASE_W; // 3 standard columns
|
const tileColumns = GRID_COLS / TILE_BASE_W; // 3 standard columns
|
||||||
// Start below existing items so new cameras never overlap the user's.
|
// Each column starts below every existing tile that overlaps it, so new
|
||||||
const maxBottom = existing.reduce(
|
// cameras fill open columns without overlapping the user's tiles.
|
||||||
(max, layout) => Math.max(max, layout.y + layout.h),
|
const colBottoms = Array.from({ length: tileColumns }, (_, c) =>
|
||||||
0,
|
existing.reduce(
|
||||||
|
(max, layout) =>
|
||||||
|
layout.x < (c + 1) * TILE_BASE_W &&
|
||||||
|
layout.x + layout.w > c * TILE_BASE_W
|
||||||
|
? Math.max(max, layout.y + layout.h)
|
||||||
|
: max,
|
||||||
|
0,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const colBottoms = new Array(tileColumns).fill(maxBottom);
|
|
||||||
|
|
||||||
const result: LayoutItem[] = [...existing];
|
const result: LayoutItem[] = [...existing];
|
||||||
|
|
||||||
|
|||||||
@@ -392,7 +392,9 @@ export default function UiSettingsView() {
|
|||||||
clearStreamingSettings();
|
clearStreamingSettings();
|
||||||
break;
|
break;
|
||||||
case "naturalAspect":
|
case "naturalAspect":
|
||||||
|
// a layout only renders in the mode that built it
|
||||||
setNaturalAspect(!naturalAspect);
|
setNaturalAspect(!naturalAspect);
|
||||||
|
clearStoredLayouts();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user