mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-15 10:21:06 +00:00
Preserve edited timelines and stabilize webcam crop controls
This commit is contained in:
1 parent
870391aaef
commit
01b682a461
19 files changed
+100
-27
No files matched your search
@@ -388,7 +388,7 @@ export function WebcamCropControl({
|
||||
ref={containerRef}
|
||||
variant="secondary"
|
||||
data-webcam-crop-control
|
||||
className="relative w-full touch-none select-none overflow-hidden rounded-lg border border-foreground/10"
|
||||
className="relative w-full touch-none select-none overflow-clip rounded-lg border border-foreground/10"
|
||||
style={{ aspectRatio: previewAspectRatio }}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={endDrag}
|
||||
|
||||
@@ -37,6 +37,7 @@ it("waits for ready media and matching cursor data before applying", () => {
|
||||
{ ready: false },
|
||||
{ totalMs: 0 },
|
||||
{ cursorTelemetry: [] },
|
||||
{ clipRegions: [] },
|
||||
{ cursorTelemetrySourcePath: "previous.mp4" },
|
||||
]) {
|
||||
const params = input(patch);
|
||||
@@ -56,3 +57,21 @@ it("keeps automatic zooms off when disabled and preserves reopened projects and
|
||||
expect(params.onZoomSuggested).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves imported sequences and clip edits made while cursor data was loading", () => {
|
||||
const original = input().clipRegions[0];
|
||||
for (const clipRegions of [
|
||||
[
|
||||
original,
|
||||
{ ...original, id: "imported", startMs: 6000, endMs: 7000, sourceStartMs: 6000 },
|
||||
],
|
||||
[{ ...original, sourceStartMs: 1000 }],
|
||||
[{ ...original, startMs: 500 }],
|
||||
[{ ...original, endMs: 5000 }],
|
||||
[{ ...original, speed: 2 }],
|
||||
]) {
|
||||
const params = input({ clipRegions });
|
||||
expect(applyAutomaticZooms(params)).toBe(true);
|
||||
expect(params.onZoomSuggested).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
@@ -22,6 +22,18 @@ export function applyAutomaticZooms(input: Input): boolean {
|
||||
if (!input.ready || !input.sourcePath || !Number.isFinite(input.totalMs) || input.totalMs <= 0)
|
||||
return false;
|
||||
if (!input.enabled || input.isSavedProject || input.zoomRegions.length > 0) return true;
|
||||
// Imported sequences get new media paths too. Only start on an unedited take;
|
||||
// adding/trimming clips while telemetry loads must preserve the user's timeline.
|
||||
const clip = input.clipRegions[0];
|
||||
if (!clip) return false;
|
||||
if (
|
||||
input.clipRegions.length !== 1 ||
|
||||
clip.startMs !== 0 ||
|
||||
Math.abs(clip.endMs - input.totalMs) > 1 ||
|
||||
(clip.sourceStartMs ?? 0) !== 0 ||
|
||||
(clip.speed ?? 1) !== 1
|
||||
)
|
||||
return true;
|
||||
if (input.cursorTelemetrySourcePath !== input.sourcePath || input.cursorTelemetry.length < 2)
|
||||
return false;
|
||||
const result = buildInteractionZoomSuggestions({
|
||||
|
||||
@@ -254,7 +254,10 @@ export function CursorSettings(props: Props) {
|
||||
</div>
|
||||
{advanced && (
|
||||
<SliderControl
|
||||
label={tSettings("effects.cursorClickBounce", "Scale on click")}
|
||||
label={tSettings(
|
||||
"effects.cursorClickBounceStrength",
|
||||
"Scale on click strength",
|
||||
)}
|
||||
value={cursorClickBounce}
|
||||
min={0}
|
||||
max={5}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "Cursor-Bewegungsunschärfe",
|
||||
"cursorClickBounce": "Beim Klicken skalieren",
|
||||
"cursorClickBounceStrength": "Stärke der Skalierung beim Klicken",
|
||||
"cursorClickBounceDuration": "Sprunggeschwindigkeit",
|
||||
"cursorSway": "Cursor-Schwankung",
|
||||
"cursorIdleHide": "Bei Inaktivität ausblenden",
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "Cursor Motion Blur",
|
||||
"cursorClickBounce": "Scale on click",
|
||||
"cursorClickBounceStrength": "Scale on click strength",
|
||||
"cursorClickBounceDuration": "Bounce Speed",
|
||||
"cursorSway": "Cursor Sway",
|
||||
"cursorIdleHide": "Hide when idle",
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "Desenfoque de movimiento del cursor",
|
||||
"cursorClickBounce": "Escalar al hacer clic",
|
||||
"cursorClickBounceStrength": "Intensidad de escala al hacer clic",
|
||||
"cursorClickBounceDuration": "Velocidad del rebote",
|
||||
"cursorSway": "Balanceo del cursor",
|
||||
"cursorIdleHide": "Ocultar al estar inactivo",
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "Flou de mouvement du curseur",
|
||||
"cursorClickBounce": "Agrandir au clic",
|
||||
"cursorClickBounceStrength": "Intensité de mise à l’échelle au clic",
|
||||
"cursorClickBounceDuration": "Vitesse du rebond",
|
||||
"cursorSway": "Oscillation du curseur",
|
||||
"cursorIdleHide": "Masquer en cas d’inactivité",
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "Motion blur del cursore",
|
||||
"cursorClickBounce": "Scala al clic",
|
||||
"cursorClickBounceStrength": "Intensità della scala al clic",
|
||||
"cursorClickBounceDuration": "Velocità rimbalzo",
|
||||
"cursorSway": "Oscillazione cursore",
|
||||
"cursorIdleHide": "Nascondi quando inattivo",
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "カーソルのモーションブラー",
|
||||
"cursorClickBounce": "クリック時に拡大",
|
||||
"cursorClickBounceStrength": "クリック時の拡大率",
|
||||
"cursorClickBounceDuration": "バウンドの速度",
|
||||
"cursorSway": "カーソルの揺れ",
|
||||
"cursorIdleHide": "操作がないときに非表示",
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "커서 모션 블러",
|
||||
"cursorClickBounce": "클릭 시 크기 조절",
|
||||
"cursorClickBounceStrength": "클릭 시 크기 조절 강도",
|
||||
"cursorClickBounceDuration": "바운스 속도",
|
||||
"cursorSway": "커서 흔들림",
|
||||
"cursorIdleHide": "대기 시 숨기기",
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "Cursor-bewegingsonscherpte",
|
||||
"cursorClickBounce": "Vergroten bij klikken",
|
||||
"cursorClickBounceStrength": "Sterkte van schalen bij klikken",
|
||||
"cursorClickBounceDuration": "Stuitsnelheid",
|
||||
"cursorSway": "Cursorbeweging",
|
||||
"cursorIdleHide": "Verbergen bij inactiviteit",
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "Desfoque de movimento do cursor",
|
||||
"cursorClickBounce": "Redimensionar ao clicar",
|
||||
"cursorClickBounceStrength": "Intensidade da escala ao clicar",
|
||||
"cursorClickBounceDuration": "Velocidade do salto",
|
||||
"cursorSway": "Balanço do cursor",
|
||||
"cursorIdleHide": "Ocultar quando inativo",
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "光标运动模糊",
|
||||
"cursorClickBounce": "点击时缩放",
|
||||
"cursorClickBounceStrength": "点击缩放强度",
|
||||
"cursorClickBounceDuration": "弹跳速度",
|
||||
"cursorSway": "光标摆动",
|
||||
"cursorIdleHide": "闲置时隐藏",
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
},
|
||||
"cursorMotionBlur": "游標動態模糊",
|
||||
"cursorClickBounce": "點擊時縮放",
|
||||
"cursorClickBounceStrength": "點擊縮放強度",
|
||||
"cursorClickBounceDuration": "彈跳速度",
|
||||
"cursorSway": "游標擺動",
|
||||
"cursorIdleHide": "閒置時隱藏",
|
||||
|
||||
@@ -13,6 +13,10 @@ test("cursor idle toggle stays available in basic and advanced settings without
|
||||
await expect(hide).not.toBeChecked();
|
||||
await page.getByRole("switch", { name: "Advanced settings", exact: true }).press("Space");
|
||||
await expect(hide).not.toBeChecked();
|
||||
await expect(page.getByRole("switch", { name: "Scale on click", exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("slider", { name: "Scale on click strength", exact: true }),
|
||||
).toBeVisible();
|
||||
for (const name of ["Idle detection", "Fade delay", "Fade duration", "Movement tolerance"])
|
||||
await expect(page.getByRole("slider", { name, exact: true })).toHaveCount(0);
|
||||
await hide.press("Space");
|
||||
|
||||
@@ -44,6 +44,11 @@ async function installSceneBridge(page: Page) {
|
||||
});
|
||||
}
|
||||
|
||||
async function openScene(page: Page, url: string) {
|
||||
await page.goto(url);
|
||||
await expect(page.locator('[data-variant="clip"]')).toHaveCount(1, { timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function storedPreferences(page: Page) {
|
||||
return page.evaluate(() =>
|
||||
JSON.parse(localStorage.getItem("recordly.editor.preferences") ?? "{}"),
|
||||
@@ -61,8 +66,7 @@ test("Scene controls carry to new projects; loading an older look does not repla
|
||||
page,
|
||||
}) => {
|
||||
await installSceneBridge(page);
|
||||
await page.goto("/?windowType=editor&project=first");
|
||||
await expect(page.locator('[data-variant="clip"]')).toHaveCount(1, { timeout: 20_000 });
|
||||
await openScene(page, "/?windowType=editor&project=first");
|
||||
await setSlider(page, "Padding", 8);
|
||||
const blur = await setSlider(page, "Blur", 3);
|
||||
const shadow = await setSlider(page, "Shadow", 4);
|
||||
@@ -91,7 +95,7 @@ test("Scene controls carry to new projects; loading an older look does not repla
|
||||
await expect.poll(() => storedPreferences(page)).toMatchObject(expected);
|
||||
|
||||
// A second recording reads the controls actually saved by the first editor.
|
||||
await page.goto("/?windowType=editor&project=second");
|
||||
await openScene(page, "/?windowType=editor&project=second");
|
||||
for (const [index, side] of ["Top", "Bottom", "Left", "Right"].entries()) {
|
||||
await expect(page.getByRole("slider", { name: side, exact: true })).toHaveValue(
|
||||
String(index + 1),
|
||||
@@ -108,7 +112,7 @@ test("Scene controls carry to new projects; loading an older look does not repla
|
||||
"true",
|
||||
);
|
||||
|
||||
await page.goto("/?windowType=editor&older=1");
|
||||
await openScene(page, "/?windowType=editor&older=1");
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Aspect ratio: 16:9", exact: true }),
|
||||
).toBeVisible();
|
||||
@@ -123,7 +127,7 @@ test("Scene controls carry to new projects; loading an older look does not repla
|
||||
await expect
|
||||
.poll(() => storedPreferences(page))
|
||||
.toMatchObject({ ...expected, shadowIntensity: 0.71 });
|
||||
await page.goto("/?windowType=editor&project=third");
|
||||
await openScene(page, "/?windowType=editor&project=third");
|
||||
await expect(page.getByRole("slider", { name: "Top", exact: true })).toHaveValue("1");
|
||||
await expect(page.getByRole("slider", { name: "Shadow", exact: true })).toHaveValue("0.71");
|
||||
await expect(page.getByRole("button", { name: "Gradient 1", exact: true })).toHaveAttribute(
|
||||
@@ -136,27 +140,29 @@ test("image, video, solid color and gradient selections survive creating another
|
||||
page,
|
||||
}) => {
|
||||
await installSceneBridge(page);
|
||||
await page.goto("/?windowType=editor&project=initial");
|
||||
for (const [index, tab] of ["Image", "Video", "Colors", "Colors"].entries()) {
|
||||
await openScene(page, "/?windowType=editor&project=initial");
|
||||
const cases = [
|
||||
{
|
||||
tab: "Image",
|
||||
label: "Golden Gate Day",
|
||||
wallpaper: "/wallpapers/golden-gate-day-no-vignette.png",
|
||||
},
|
||||
{ tab: "Video", label: "Wispy Sky", wallpaper: "/wallpapers/wispysky.mp4" },
|
||||
{ tab: "Colors", label: "Background color #E2E8F0", wallpaper: "#e2e8f0" },
|
||||
{
|
||||
tab: "Colors",
|
||||
label: "Gradient 2",
|
||||
wallpaper: "linear-gradient(135deg, #e0e7ff 0%, #c4b5fd 100%)",
|
||||
},
|
||||
];
|
||||
for (const [index, { tab, label, wallpaper }] of cases.entries()) {
|
||||
await page.getByRole("row", { name: tab, exact: true }).click();
|
||||
const tile =
|
||||
index === 2
|
||||
? page.getByRole("button", { name: "Background color #E2E8F0", exact: true })
|
||||
: index === 3
|
||||
? page.getByRole("button", { name: "Gradient 2", exact: true })
|
||||
: page
|
||||
.locator(
|
||||
`[data-background-panel="${tab.toLowerCase()}"] .wallpaper-tile`,
|
||||
)
|
||||
.nth(index === 0 ? 1 : 0);
|
||||
const label = await tile.getAttribute("aria-label");
|
||||
expect(label).toBeTruthy();
|
||||
const tile = page.getByRole("button", { name: label, exact: true });
|
||||
await tile.click();
|
||||
await expect(tile).toHaveAttribute("aria-pressed", "true");
|
||||
const wallpaper = (await storedPreferences(page)).wallpaper;
|
||||
expect(wallpaper).toBeTruthy();
|
||||
await page.goto(`/?windowType=editor&project=background-${index}`);
|
||||
await expect(page.getByRole("button", { name: label!, exact: true })).toHaveAttribute(
|
||||
await expect.poll(() => storedPreferences(page)).toMatchObject({ wallpaper });
|
||||
await openScene(page, `/?windowType=editor&project=background-${index}`);
|
||||
await expect(page.getByRole("button", { name: label, exact: true })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
|
||||
@@ -361,10 +361,17 @@ test("Clips shows original filenames and truncates long names beside real still
|
||||
});
|
||||
|
||||
for (const enabled of [true, false]) {
|
||||
test(`imported recordings stay unzoomed with legacy auto zooms ${enabled ? "on" : "off"}`, async ({
|
||||
test(`imported recordings stay unzoomed with automatic zooms ${enabled ? "on" : "off"}`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await setup(page);
|
||||
await installDesktopBridgeOverrides(page, () => {
|
||||
const get = window.electronAPI.getAppSetting;
|
||||
window.electronAPI.getAppSetting = (key) =>
|
||||
key === "recordly.editor.preferences"
|
||||
? JSON.parse(localStorage.getItem(key) ?? "{}")
|
||||
: get(key);
|
||||
});
|
||||
await page.evaluate((enabled) => {
|
||||
const prefs = JSON.parse(localStorage.getItem("recordly.editor.preferences") || "{}");
|
||||
localStorage.setItem(
|
||||
@@ -373,7 +380,17 @@ for (const enabled of [true, false]) {
|
||||
);
|
||||
}, enabled);
|
||||
await page.reload();
|
||||
await expect(page.locator('[data-variant="clip"]')).toHaveCount(1);
|
||||
await expect(page.locator('[data-variant="clip"]')).toHaveCount(1, { timeout: 20_000 });
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
const settings = page.getByRole("dialog", { name: "Settings", exact: true });
|
||||
await settings.getByRole("tab", { name: "Motion", exact: true }).click();
|
||||
const automatic = settings.getByRole("switch", {
|
||||
name: "Automatically add zooms",
|
||||
exact: true,
|
||||
});
|
||||
if (enabled) await expect(automatic).toBeChecked();
|
||||
else await expect(automatic).not.toBeChecked();
|
||||
await page.keyboard.press("Escape");
|
||||
await page.getByRole("button", { name: "Clips", exact: true }).click();
|
||||
await page.evaluate(() => {
|
||||
let count = 0;
|
||||
|
||||
@@ -61,6 +61,7 @@ test("webcam preview frame choices persist and inline crop updates immediately",
|
||||
exact: true,
|
||||
});
|
||||
await handle.focus();
|
||||
await expect.poll(() => crop.evaluate((element) => element.scrollTop)).toBe(0);
|
||||
await handle.press("ArrowLeft");
|
||||
await expect
|
||||
.poll(() =>
|
||||
|
||||
Reference in new issue
Block a user