diff --git a/src/components/ai-edition/v4/V4Timeline.geometry.test.tsx b/src/components/ai-edition/v4/V4Timeline.geometry.test.tsx
index d237428f5..f3a067bca 100644
--- a/src/components/ai-edition/v4/V4Timeline.geometry.test.tsx
+++ b/src/components/ai-edition/v4/V4Timeline.geometry.test.tsx
@@ -64,7 +64,7 @@ function clip(startSec: number, endSec: number) {
}
/** The asset every clip above points at. No `cameraTrack`: this recording has no webcam,
- * which is what Add Full Camera is gated on. */
+ * 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. */
@@ -221,11 +221,29 @@ describe("V4Timeline create-from-toolbar", () => {
expect(durationOf(tl)).toBeCloseTo(0.25, 3);
});
- // The toolbar button is gone, but Add Full Camera is still reachable via the `C`
- // shortcut, and an empty lane advertises it — so on a camera-less project it was
- // still inviting a `C` press that `addCameraFullscreen` refuses (#353). It borrows
- // the Layout pane's "No Webcam" wording instead, so the two surfaces agree about
- // the same project.
+ // #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
+ // so before it is clicked instead of looking like it worked.
+ it("disables Add Full Camera when no clip on the timeline has a camera", () => {
+ renderTimeline();
+ expect(screen.getByTitle("buttons.addCameraFullscreen")).toBeDisabled();
+ });
+
+ it("enables Add Full Camera as soon as a clip's asset carries one", () => {
+ renderTimeline(undefined, undefined, [
+ {
+ ...NO_CAMERA_ASSET,
+ cameraTrack: { sourcePath: "/tmp/cam.webm", startMs: 0, offsetMs: 0, visible: true },
+ },
+ ]);
+ expect(screen.getByTitle("buttons.addCameraFullscreen")).toBeEnabled();
+ });
+
+ // The disabled button is only half the promise: an empty lane advertises the shortcut
+ // that fills it, so on a camera-less project it was still inviting a `C` press that
+ // `addCameraFullscreen` now refuses. It borrows the Layout pane's "No Webcam" wording
+ // instead, so the two surfaces agree about the same project.
it("does not advertise the C shortcut on a lane that cannot be filled", () => {
renderTimeline();
expect(screen.getByText("layout.noWebcam")).toBeInTheDocument();
diff --git a/src/components/ai-edition/v4/V4Timeline.tsx b/src/components/ai-edition/v4/V4Timeline.tsx
index a6300bb47..493f17d5c 100644
--- a/src/components/ai-edition/v4/V4Timeline.tsx
+++ b/src/components/ai-edition/v4/V4Timeline.tsx
@@ -431,9 +431,9 @@ export function V4Timeline({
const clips = tl.clips;
// A camera-fullscreen region grows the webcam overlay, so on a project with no webcam
// it renders nothing in the preview and nothing in the export. `addCameraFullscreen`
- // refuses to write one (see useTimeline) — this is why the empty lane's hint below
- // swaps to "no webcam" instead of advertising the `C` shortcut. Same question, same
- // helper as the Layout pane: is a camera attached anywhere on this timeline?
+ // refuses to write one (see useTimeline) — this makes the control say so before it is
+ // clicked instead of looking like it worked. Same question, same helper as the Layout
+ // pane: is a camera attached anywhere on this timeline?
const hasAnyCamera = useMemo(() => hasAnyClipWithCamera(tl.assets, clips), [tl.assets, clips]);
const total = useMemo(
() =>
@@ -1359,6 +1359,17 @@ export function V4Timeline({
>
+
) : (
// Media is an ARRANGING surface: add, remove, reorder. Nothing here
@@ -1452,7 +1463,8 @@ export function V4Timeline({
{renderPills(zoomPills, t("hints.pressZoom"))}
{/* Advertising "Press C" on a project with no webcam invites a keystroke
- that `addCameraFullscreen` now refuses (#353). */}
+ that `addCameraFullscreen` now refuses (#353). The toolbar button is
+ already disabled; this keeps the lane from contradicting it. */}
{renderPills(
cameraFullscreenPills,
hasAnyCamera ? t("hints.pressCameraFullscreen") : ts("layout.noWebcam"),
diff --git a/src/i18n/locales/ar/timeline.json b/src/i18n/locales/ar/timeline.json
index e434620eb..4412a0a0b 100644
--- a/src/i18n/locales/ar/timeline.json
+++ b/src/i18n/locales/ar/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "تفعيل التركيز التلقائي لجميع التكبيرات (الكاميرا تتبع المؤشر)",
"addTrim": "إضافة قص (T)",
"addAnnotation": "إضافة شرح (A)",
- "addSpeed": "إضافة سرعة (S)"
+ "addSpeed": "إضافة سرعة (S)",
+ "addCameraFullscreen": "إضافة كاميرا كاملة الشاشة (C)"
},
"hints": {
"pressZoom": "اضغط Z لإضافة تكبير",
diff --git a/src/i18n/locales/en/timeline.json b/src/i18n/locales/en/timeline.json
index cd15921ef..c41966115 100644
--- a/src/i18n/locales/en/timeline.json
+++ b/src/i18n/locales/en/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "Auto-Focus all zooms (camera follows the cursor)",
"addTrim": "Add Trim (T)",
"addAnnotation": "Add Annotation (A)",
- "addSpeed": "Add Speed (S)"
+ "addSpeed": "Add Speed (S)",
+ "addCameraFullscreen": "Add Full Camera (C)"
},
"hints": {
"pressZoom": "Press Z to add zoom",
diff --git a/src/i18n/locales/es/timeline.json b/src/i18n/locales/es/timeline.json
index c05eebf89..989289e00 100644
--- a/src/i18n/locales/es/timeline.json
+++ b/src/i18n/locales/es/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "Activar enfoque automático para todos los zooms (la cámara sigue el cursor)",
"addTrim": "Agregar recorte (T)",
"addAnnotation": "Agregar anotación (A)",
- "addSpeed": "Agregar velocidad (S)"
+ "addSpeed": "Agregar velocidad (S)",
+ "addCameraFullscreen": "Agregar cámara a pantalla completa (C)"
},
"hints": {
"pressZoom": "Presiona Z para agregar zoom",
diff --git a/src/i18n/locales/fr/timeline.json b/src/i18n/locales/fr/timeline.json
index 3147db21d..a35b8858a 100644
--- a/src/i18n/locales/fr/timeline.json
+++ b/src/i18n/locales/fr/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "Activer la mise au point automatique pour tous les zooms (la caméra suit le curseur)",
"addTrim": "Ajouter une coupe (T)",
"addAnnotation": "Ajouter une annotation (A)",
- "addSpeed": "Ajouter une vitesse (S)"
+ "addSpeed": "Ajouter une vitesse (S)",
+ "addCameraFullscreen": "Ajouter Caméra plein écran (C)"
},
"hints": {
"pressZoom": "Appuyez sur Z pour ajouter un zoom",
diff --git a/src/i18n/locales/it/timeline.json b/src/i18n/locales/it/timeline.json
index 468764831..09bb116ee 100644
--- a/src/i18n/locales/it/timeline.json
+++ b/src/i18n/locales/it/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "Attiva la messa a fuoco automatica per tutti gli zoom (la fotocamera segue il cursore)",
"addTrim": "Aggiungi taglio (T)",
"addAnnotation": "Aggiungi annotazione (A)",
- "addSpeed": "Aggiungi velocità (S)"
+ "addSpeed": "Aggiungi velocità (S)",
+ "addCameraFullscreen": "Aggiungi Camera a schermo intero (C)"
},
"hints": {
"pressZoom": "Premi Z per aggiungere zoom",
diff --git a/src/i18n/locales/ja-JP/timeline.json b/src/i18n/locales/ja-JP/timeline.json
index 7ebcc6b36..68911ba91 100644
--- a/src/i18n/locales/ja-JP/timeline.json
+++ b/src/i18n/locales/ja-JP/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "すべてのズームでオートフォーカスをオンにする(カメラがカーソルに追従)",
"addTrim": "トリムを追加 (T)",
"addAnnotation": "注釈を追加 (A)",
- "addSpeed": "再生速度を追加 (S)"
+ "addSpeed": "再生速度を追加 (S)",
+ "addCameraFullscreen": "フルスクリーンカメラを追加 (C)"
},
"hints": {
"pressZoom": "Zキーを押してズームを追加",
diff --git a/src/i18n/locales/ko-KR/timeline.json b/src/i18n/locales/ko-KR/timeline.json
index 986d173ce..8a100ee6d 100644
--- a/src/i18n/locales/ko-KR/timeline.json
+++ b/src/i18n/locales/ko-KR/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "모든 줌에 자동 초점 켜기 (카메라가 커서를 따라갑니다)",
"addTrim": "트림 추가 (T)",
"addAnnotation": "주석 추가 (A)",
- "addSpeed": "속도 추가 (S)"
+ "addSpeed": "속도 추가 (S)",
+ "addCameraFullscreen": "전체 화면 카메라 추가 (C)"
},
"hints": {
"pressZoom": "Z를 눌러 줌 추가",
diff --git a/src/i18n/locales/pt-BR/timeline.json b/src/i18n/locales/pt-BR/timeline.json
index ed9c42bd7..5359feba9 100644
--- a/src/i18n/locales/pt-BR/timeline.json
+++ b/src/i18n/locales/pt-BR/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "Ativar foco automático para todos os zooms (a câmera segue o cursor)",
"addTrim": "Adicionar Recorte (T)",
"addAnnotation": "Adicionar Anotação (A)",
- "addSpeed": "Adicionar Velocidade (S)"
+ "addSpeed": "Adicionar Velocidade (S)",
+ "addCameraFullscreen": "Adicionar Câmera em Tela Cheia (C)"
},
"hints": {
"pressZoom": "Pressione Z para adicionar zoom",
diff --git a/src/i18n/locales/ru/timeline.json b/src/i18n/locales/ru/timeline.json
index 9ce1261fb..387086953 100644
--- a/src/i18n/locales/ru/timeline.json
+++ b/src/i18n/locales/ru/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "Включить автофокус для всех зумов (камера следует за курсором)",
"addTrim": "Добавить обрезку (T)",
"addAnnotation": "Добавить аннотацию (A)",
- "addSpeed": "Изменить скорость (S)"
+ "addSpeed": "Изменить скорость (S)",
+ "addCameraFullscreen": "Добавить камеру на весь экран (C)"
},
"hints": {
"pressZoom": "Нажмите Z для добавления масштабирования",
diff --git a/src/i18n/locales/tr/timeline.json b/src/i18n/locales/tr/timeline.json
index c56c0613b..d5a531f3e 100644
--- a/src/i18n/locales/tr/timeline.json
+++ b/src/i18n/locales/tr/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "Tüm yakınlaştırmalarda Otomatik Odağı aç (kamera imleci takip eder)",
"addTrim": "Kırpma Ekle (T)",
"addAnnotation": "Açıklama Ekle (A)",
- "addSpeed": "Hız Ekle (S)"
+ "addSpeed": "Hız Ekle (S)",
+ "addCameraFullscreen": "Tam Ekran Kamera Ekle (C)"
},
"hints": {
"pressZoom": "Yakınlaştırma eklemek için Z tuşuna basın",
diff --git a/src/i18n/locales/vi/timeline.json b/src/i18n/locales/vi/timeline.json
index fe6011b27..1d963e585 100644
--- a/src/i18n/locales/vi/timeline.json
+++ b/src/i18n/locales/vi/timeline.json
@@ -8,7 +8,8 @@
"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ỏ)",
"addTrim": "Thêm Cắt (T)",
"addAnnotation": "Thêm Chú thích (A)",
- "addSpeed": "Thêm Tốc độ (S)"
+ "addSpeed": "Thêm Tốc độ (S)",
+ "addCameraFullscreen": "Thêm Camera Toàn màn hình (C)"
},
"hints": {
"pressZoom": "Nhấn Z để thêm thu phóng",
diff --git a/src/i18n/locales/zh-CN/timeline.json b/src/i18n/locales/zh-CN/timeline.json
index bd405fa89..1451e6d31 100644
--- a/src/i18n/locales/zh-CN/timeline.json
+++ b/src/i18n/locales/zh-CN/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "为所有缩放开启自动对焦(摄像头跟随光标)",
"addTrim": "添加剪辑 (T)",
"addAnnotation": "添加标注 (A)",
- "addSpeed": "添加速度 (S)"
+ "addSpeed": "添加速度 (S)",
+ "addCameraFullscreen": "添加全屏摄像头 (C)"
},
"hints": {
"pressZoom": "按 Z 添加缩放",
diff --git a/src/i18n/locales/zh-TW/timeline.json b/src/i18n/locales/zh-TW/timeline.json
index 6692ad24a..7f4ba9874 100644
--- a/src/i18n/locales/zh-TW/timeline.json
+++ b/src/i18n/locales/zh-TW/timeline.json
@@ -8,7 +8,8 @@
"autoFocusAllOff": "為所有縮放開啟自動對焦(攝影機跟隨游標)",
"addTrim": "新增剪輯 (T)",
"addAnnotation": "新增標註 (A)",
- "addSpeed": "新增速度 (S)"
+ "addSpeed": "新增速度 (S)",
+ "addCameraFullscreen": "新增全螢幕攝影機 (C)"
},
"hints": {
"pressZoom": "按 Z 新增縮放",
diff --git a/src/lib/ai-edition/store/useTimeline.ts b/src/lib/ai-edition/store/useTimeline.ts
index 67f2ecdfb..7823459ec 100644
--- a/src/lib/ai-edition/store/useTimeline.ts
+++ b/src/lib/ai-edition/store/useTimeline.ts
@@ -407,9 +407,9 @@ export function useTimeline() {
// a missing `webcamRect`) and nothing in the export — it just sits in
// `legacyEditor.cameraFullscreenRegions` forever. The agent's `addCameraFullscreen`
// tool already refuses this and says why (electron/ai-edition/agent-tools.ts,
- // `noCameraUnderSpan`); the gate lives HERE rather than at each caller so the `C`
- // shortcut and any future entry point are covered by construction.
- // `hasAnyClipWithCamera` is the consolidated answer to "does this
+ // `noCameraUnderSpan`); the gate lives HERE rather than at each button so both UI
+ // entry points — the toolbar and the `C` shortcut — and any future one are covered
+ // by construction. `hasAnyClipWithCamera` is the consolidated answer to "does this
// project have a camera at all", used the same way by the Layout pane.
const addCameraFullscreen = useCallback(
async (durationSec = DEFAULT_NEW_REGION_SEC) => {