Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions src/components/ai-edition/v4/V4Timeline.geometry.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,14 @@ function clip(startSec: number, endSec: number) {
* which is what the Full Camera button is gated on. */
const NO_CAMERA_ASSET = { id: "a1", label: "rec", durationSec: TOTAL_SEC };

/** By default one 30-minute clip carrying a single one-second annotation. */
/** By default one 30-minute clip carrying a single one-second annotation, and a store that
* holds an edit region (so Clear timeline shows); `overrides` replaces any `tl` member. */
function renderTimeline(
clips = [clip(0, TOTAL_SEC)],
annotation = { id: "ann1", startMs: 10_000, endMs: 11_000 },
assets: Array<Record<string, unknown>> = [NO_CAMERA_ASSET],
onRender?: ProfilerOnRenderCallback,
overrides: Record<string, unknown> = {},
) {
const tl = {
clips,
Expand All @@ -104,6 +106,8 @@ function renderTimeline(
cameraFullscreenRegions: [],
zoomRegions: [],
trimRanges: [],
hasEditRegions: true,
...overrides,
selection: null,
multiSelection: [],
clipSelection: null,
Expand All @@ -119,6 +123,9 @@ function renderTimeline(
addZoom: vi.fn(async () => {
/* the toolbar only awaits it */
}),
clearTimeline: vi.fn(async () => {
/* the toolbar only awaits it */
}),
};
const setCurrentTime = vi.fn();
const timeline = (
Expand All @@ -145,7 +152,11 @@ function renderTimeline(
),
);
return {
pill: screen.getByTitle("toolbar.newAnnotation"),
// A getter: a test that empties `annotationRegions` renders no such pill, and an eager
// lookup would throw before its own assertions ran.
get pill() {
return screen.getByTitle("toolbar.newAnnotation");
},
clipEls: Array.from(document.querySelectorAll<HTMLElement>("[data-clip-id]")),
tl,
setCurrentTime,
Expand Down Expand Up @@ -403,6 +414,46 @@ describe("V4Timeline create-from-toolbar", () => {
expect(durationOf(tl)).toBeCloseTo(0.25, 3);
});

// Clear timeline (#723) follows the STORED edit regions (`tl.hasEditRegions`), never the
// pills the lanes draw: a trim whose clip is gone is stored, cleared, and has no pill.
// Absent, never greyed out, when there is nothing to clear.
const toolbarOf = () => screen.getByRole("toolbar", { name: "toolbar.timelineTools" });
const dividersIn = (toolbar: HTMLElement) =>
Array.from(toolbar.querySelectorAll("[class*=tlToolSep]"));

it("shows no Clear timeline button while the store holds no edit region, and no divider for it", () => {
// A pill is drawn, and the button still follows the store.
renderTimeline(undefined, undefined, undefined, undefined, { hasEditRegions: false });
expect(screen.queryByLabelText("buttons.clearTimeline")).not.toBeInTheDocument();
// Only the divider after the auto-enhance button: none is left dangling at the end.
expect(dividersIn(toolbarOf())).toHaveLength(1);
});

it("shows it for a stored region no lane draws, and one click asks the store to clear", () => {
const { tl } = renderTimeline(undefined, undefined, undefined, undefined, {
annotationRegions: [],
hasEditRegions: true,
});
expect(document.querySelector("[class*=lanePill]")).toBeNull();
fireEvent.click(screen.getByLabelText("buttons.clearTimeline"));
expect(tl.clearTimeline).toHaveBeenCalledTimes(1);
});

it("puts Clear timeline last, behind a divider, after the Add Full Camera button", () => {
renderTimeline();
const toolbar = toolbarOf();
const buttons = Array.from(toolbar.querySelectorAll("button"));
const clear = screen.getByLabelText("buttons.clearTimeline");
expect(buttons.at(-1)).toBe(clear);

const divider = clear.previousElementSibling;
expect(divider?.className).toContain("tlToolSep");
expect(divider?.previousElementSibling).toBe(
screen.getByLabelText("buttons.addCameraFullscreen"),
);
expect(dividersIn(toolbar)).toHaveLength(2);
});

// #353. A camera-fullscreen region grows the webcam overlay, so with no webcam on the
// timeline it renders nothing in the preview and nothing in the export — the region is
// stored and forgotten. `addCameraFullscreen` now refuses to write one; the button says
Expand Down
20 changes: 20 additions & 0 deletions src/components/ai-edition/v4/V4Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AudioLines,
Clock,
Crosshair,
Eraser,
Loader2,
Maximize2,
MessageSquare,
Expand Down Expand Up @@ -1998,6 +1999,25 @@ export function V4Timeline({
<Maximize2 size={16} />
</button>
</Tooltip>
{/* Last, behind a divider: every button before it adds a region, this one
clears them. Absent with its divider, not greyed out, when there is
nothing to clear. One write in the store, so one Ctrl+Z restores every
region. */}
{tl.hasEditRegions ? (
<>
<span className={styles.tlToolSep} aria-hidden />
<Tooltip content={t("buttons.clearTimeline")}>
<button
type="button"
className={styles.tlToolBtn}
aria-label={t("buttons.clearTimeline")}
onClick={() => void tl.clearTimeline()}
>
<Eraser size={16} />
</button>
</Tooltip>
</>
) : null}
</div>
</TooltipProvider>
) : (
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ar/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "اقتراحات التكبير التلقائي معطّلة — انقر لاقتراح تكبيرات من المؤشر",
"autoFocusAllOn": "التركيز التلقائي مفعّل لجميع التكبيرات — انقر للتبديل إلى يدوي للجميع",
"autoFocusAllOff": "تفعيل التركيز التلقائي لجميع التكبيرات (الكاميرا تتبع المؤشر)",
"clearTimeline": "مسح المخطط الزمني",
"addTrim": "إضافة قص (T)",
"addAnnotation": "إضافة شرح (A)",
"addSpeed": "إضافة سرعة (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/cs/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Automatické návrhy přiblížení vypnuty, klikněte pro návrh přiblížení podle kurzoru",
"autoFocusAllOn": "Automatické zaostření zapnuto pro všechna přiblížení, klikněte pro přepnutí všech na ruční",
"autoFocusAllOff": "Automaticky zaostřit všechna přiblížení (kamera sleduje kurzor)",
"clearTimeline": "Vymazat časovou osu",
"addTrim": "Přidat oříznutí (T)",
"addAnnotation": "Přidat anotaci (A)",
"addSpeed": "Přidat rychlost (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/de/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Zoom-Vorschläge aus — klicken, um Zooms aus der Cursorbewegung vorzuschlagen",
"autoFocusAllOn": "Auto-Fokus für alle Zooms an — klicken, um alle auf manuell umzustellen",
"autoFocusAllOff": "Auto-Fokus für alle Zooms (Kamera folgt dem Cursor)",
"clearTimeline": "Timeline leeren",
"addTrim": "Trim hinzufügen (T)",
"addAnnotation": "Anmerkung hinzufügen (A)",
"addSpeed": "Tempo hinzufügen (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Auto zoom suggestions off — click to suggest zooms from cursor",
"autoFocusAllOn": "Auto-Focus on for all zooms — click to switch all to manual",
"autoFocusAllOff": "Auto-Focus all zooms (camera follows the cursor)",
"clearTimeline": "Clear timeline",
"addTrim": "Add trim (T)",
"addAnnotation": "Add annotation (A)",
"addSpeed": "Add speed (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Sugerencias de zoom automático desactivadas — haz clic para sugerir zooms desde el cursor",
"autoFocusAllOn": "Enfoque automático activado para todos los zooms — haz clic para pasar todos a manual",
"autoFocusAllOff": "Activar enfoque automático para todos los zooms (la cámara sigue el cursor)",
"clearTimeline": "Borrar la línea de tiempo",
"addTrim": "Agregar recorte (T)",
"addAnnotation": "Agregar anotación (A)",
"addSpeed": "Agregar velocidad (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/fr/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Suggestions de zoom automatique désactivées — cliquez pour suggérer des zooms depuis le curseur",
"autoFocusAllOn": "Mise au point automatique activée pour tous les zooms — cliquez pour tout passer en manuel",
"autoFocusAllOff": "Activer la mise au point automatique pour tous les zooms (la caméra suit le curseur)",
"clearTimeline": "Effacer la timeline",
"addTrim": "Ajouter une coupe (T)",
"addAnnotation": "Ajouter une annotation (A)",
"addSpeed": "Ajouter une vitesse (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/it/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Suggerimenti di zoom automatico disattivi — clicca per suggerire zoom dal cursore",
"autoFocusAllOn": "Messa a fuoco automatica attiva per tutti gli zoom — clicca per passare tutti a manuale",
"autoFocusAllOff": "Attiva la messa a fuoco automatica per tutti gli zoom (la fotocamera segue il cursore)",
"clearTimeline": "Cancella la timeline",
"addTrim": "Aggiungi taglio (T)",
"addAnnotation": "Aggiungi annotazione (A)",
"addSpeed": "Aggiungi velocità (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ja-JP/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "自動ズーム提案がオフ — クリックするとカーソル位置からズームを提案します",
"autoFocusAllOn": "すべてのズームでオートフォーカスがオン — クリックするとすべて手動に切り替わります",
"autoFocusAllOff": "すべてのズームでオートフォーカスをオンにする(カメラがカーソルに追従)",
"clearTimeline": "タイムラインをクリア",
"addTrim": "トリムを追加 (T)",
"addAnnotation": "注釈を追加 (A)",
"addSpeed": "再生速度を追加 (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ko-KR/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "자동 줌 제안 꺼짐 — 클릭하면 커서 기반으로 줌을 제안합니다",
"autoFocusAllOn": "모든 줌에 자동 초점 켜짐 — 클릭하면 모두 수동으로 전환합니다",
"autoFocusAllOff": "모든 줌에 자동 초점 켜기 (카메라가 커서를 따라갑니다)",
"clearTimeline": "타임라인 지우기",
"addTrim": "트림 추가 (T)",
"addAnnotation": "주석 추가 (A)",
"addSpeed": "속도 추가 (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/pt-BR/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Sugestões de zoom automático desativadas — clique para sugerir zooms a partir do cursor",
"autoFocusAllOn": "Foco automático ativado para todos os zooms — clique para mudar todos para manual",
"autoFocusAllOff": "Ativar foco automático para todos os zooms (a câmera segue o cursor)",
"clearTimeline": "Limpar a linha do tempo",
"addTrim": "Adicionar Recorte (T)",
"addAnnotation": "Adicionar Anotação (A)",
"addSpeed": "Adicionar Velocidade (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ru/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Автоматические предложения масштабирования выключены — нажмите, чтобы предложить зумы по курсору",
"autoFocusAllOn": "Автофокус включён для всех зумов — нажмите, чтобы переключить все в ручной режим",
"autoFocusAllOff": "Включить автофокус для всех зумов (камера следует за курсором)",
"clearTimeline": "Очистить таймлайн",
"addTrim": "Добавить обрезку (T)",
"addAnnotation": "Добавить аннотацию (A)",
"addSpeed": "Изменить скорость (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/tr/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Otomatik yakınlaştırma önerileri kapalı — imleçten yakınlaştırma önermek için tıklayın",
"autoFocusAllOn": "Tüm yakınlaştırmalarda Otomatik Odak açık — hepsini manuel yapmak için tıklayın",
"autoFocusAllOff": "Tüm yakınlaştırmalarda Otomatik Odağı aç (kamera imleci takip eder)",
"clearTimeline": "Zaman çizelgesini temizle",
"addTrim": "Kırpma Ekle (T)",
"addAnnotation": "Açıklama Ekle (A)",
"addSpeed": "Hız Ekle (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/vi/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "Đề xuất thu phóng tự động đang tắt — nhấp để đề xuất thu phóng từ con trỏ",
"autoFocusAllOn": "Lấy nét tự động đang bật cho tất cả các thu phóng — nhấp để chuyển tất cả sang thủ công",
"autoFocusAllOff": "Bật lấy nét tự động cho tất cả các thu phóng (máy ảnh theo dõi con trỏ)",
"clearTimeline": "Xóa dòng thời gian",
"addTrim": "Thêm Cắt (T)",
"addAnnotation": "Thêm Chú thích (A)",
"addSpeed": "Thêm Tốc độ (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh-CN/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "自动缩放建议已关闭 — 点击可根据光标建议缩放",
"autoFocusAllOn": "所有缩放的自动对焦已开启 — 点击可将全部切换为手动",
"autoFocusAllOff": "为所有缩放开启自动对焦(摄像头跟随光标)",
"clearTimeline": "清除时间轴",
"addTrim": "添加剪辑 (T)",
"addAnnotation": "添加标注 (A)",
"addSpeed": "添加速度 (S)",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh-TW/timeline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"autoZoomOff": "自動縮放建議已關閉 — 點擊可根據游標建議縮放",
"autoFocusAllOn": "所有縮放的自動對焦已開啟 — 點擊可將全部切換為手動",
"autoFocusAllOff": "為所有縮放開啟自動對焦(攝影機跟隨游標)",
"clearTimeline": "清除時間軸",
"addTrim": "新增剪輯 (T)",
"addAnnotation": "新增標註 (A)",
"addSpeed": "新增速度 (S)",
Expand Down
47 changes: 47 additions & 0 deletions src/lib/ai-edition/document/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,53 @@ export function removeRegion(document: AxcutDocument, kind: RegionKind, id: stri
}
}

const legacyRegionCount = (document: AxcutDocument, key: string): number => {
const stored = (document.legacyEditor as Record<string, unknown> | null)?.[key];
return Array.isArray(stored) ? stored.length : 0;
};
const withLegacyRegionsCleared = (document: AxcutDocument, key: string): AxcutDocument =>
document.legacyEditor
? { ...document, legacyEditor: { ...document.legacyEditor, [key]: [] } }
: document;

/**
* The edit regions "Clear timeline" empties: one entry per `RegionKind` but `audio`. A Record
* rather than a list, so a new kind is a compile error here until someone decides whether it
* is an edit. `audio` is deliberately absent, like everything that is not a region at all:
* clips, media, transcripts, captions and the pauses added words made are content the user
* put there on purpose. (`timeline.speedRanges` is not the speed lane's store; the lane and
* the export read `legacyEditor.speedRegions`.)
*/
const EDIT_REGIONS: Record<
Exclude<RegionKind, "audio">,
{ count: (d: AxcutDocument) => number; clear: (d: AxcutDocument) => AxcutDocument }
> = {
zoom: { count: (d) => d.zoomRanges.length, clear: (d) => ({ ...d, zoomRanges: [] }) },
annotation: { count: (d) => d.annotations.length, clear: (d) => ({ ...d, annotations: [] }) },
trim: {
count: (d) => d.timeline.trimRanges.length,
clear: (d) => ({ ...d, timeline: { ...d.timeline, trimRanges: [] } }),
},
speed: {
count: (d) => legacyRegionCount(d, "speedRegions"),
clear: (d) => withLegacyRegionsCleared(d, "speedRegions"),
},
cameraFullscreen: {
count: (d) => legacyRegionCount(d, "cameraFullscreenRegions"),
clear: (d) => withLegacyRegionsCleared(d, "cameraFullscreenRegions"),
},
};

/** How many edit regions the document holds, stored rows and not pills. Pure. */
export function countEditRegions(document: AxcutDocument): number {
return Object.values(EDIT_REGIONS).reduce((sum, kind) => sum + kind.count(document), 0);
}

/** The document with every edit region gone (see {@link EDIT_REGIONS}). Pure. */
export function clearEditRegions(document: AxcutDocument): AxcutDocument {
return Object.values(EDIT_REGIONS).reduce((doc, kind) => kind.clear(doc), document);
}

/**
* The document, with its clip list changed to this one.
*
Expand Down
2 changes: 2 additions & 0 deletions src/lib/ai-edition/store/documentWriteAudit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ const DECLARED: WritePath[] = [
w("src/lib/ai-edition/store/useTimeline.ts", "addTrim", "save", "gesture"),
w("src/lib/ai-edition/store/useTimeline.ts", "addZoom", "save", "gesture"),
w("src/lib/ai-edition/store/useTimeline.ts", "addZoomsBulk", "save", "gesture"),
// The Clear timeline button: every edit region in one write, so one undo step.
w("src/lib/ai-edition/store/useTimeline.ts", "clearTimeline", "save", "gesture"),
// The two drag commits. One undo step per gesture, recorded on release and only
// if the write lands — `historyBase` carries the pre-drag document.
w("src/lib/ai-edition/store/useTimeline.ts", "commitAnnotationChange", "save", "gesture"),
Expand Down
Loading
Loading