Skip to content

集成:Codex 多功能分支 + 本轮编辑阻断修复(删除/拖入落点/蓝选/预览/菜单/递归导入)#141

Closed
appergb wants to merge 52 commits into
mainfrom
codex/integration-20260624
Closed

集成:Codex 多功能分支 + 本轮编辑阻断修复(删除/拖入落点/蓝选/预览/菜单/递归导入)#141
appergb wants to merge 52 commits into
mainfrom
codex/integration-20260624

Conversation

@appergb

@appergb appergb commented Jun 24, 2026

Copy link
Copy Markdown
Owner

这是什么

Codex 的集成分支 codex/integration-20260624(把 #77/78/79/105/108/120/121/122/123 等功能分支合在一起)+ 本轮我完成 Codex 未做完的、用户点名的编辑阻断修复。领先 main 49 个提交。

✅ 本轮完成的用户反馈修复(abab0c0)

反馈 处理
clips 删不掉 删除前过滤成真实存在的 clip id(单个 stale id 会让后端整批 RemoveClips 失败)+ Tauri 下删后 forceRefresh
拖入音频自动到末尾 按鼠标释放点落点(start=落点帧、轨=落点轨);落点重叠则换空闲同类轨、无则新建轨,不覆盖
选中高亮发灰 选中边框改醒目蓝 2px
预览拖动严重卡顿 140ms 尾随防抖 → 实时帧 + ~12fps 速率闸门,拖动即时出帧、仍受限不爆量
右键菜单固定左上(0,0) 按光标定位 + 视口翻转;并修渲染期 onClose(#108 两阻塞)
文件夹只导顶层 递归导入整棵目录树
Generate 点了没反应 弹「即将推出」toast(后端 stub)
主页红绿灯位置/间距 红绿灯下移 + titlebar 安全区下移

docs/UNFINISHED-EDITING-AND-MEDIA.md 附完整音视频编辑/媒体/MCP 未完成清单。

⚠️ 透明披露:打包进来的功能 PR 仍有已知 Bug(未在本轮修)

本分支打包了多个仍处 CHANGES_REQUESTED 的 PR,以下问题随之带入,需在合 main 前定夺:

建议

本轮的编辑阻断修复(删除/拖入/蓝选/预览/菜单/递归)都是干净、可单独受益的。但整条集成分支体量大且夹带上述未修 Bug,建议 owner 评估:① 接受这些为后续项、整体合入;或 ② 先把本轮编辑修复单独拆出合并,功能 PR 继续由 Codex 修好再逐个合。

本地验证:tsc ✅ · 52 前端测试 ✅ · vite build ✅(Rust 侧由 CI 跑)。

🤖 Generated with Claude Code

cuic19053-hue and others added 30 commits June 22, 2026 23:56
Add FolderTile + breadcrumb + in-library drag-to-folder so users can
organize assets hierarchically (mirrors upstream FolderTileView).

Backend (src-tauri/src/media.rs):
- MediaItemDto gains folderId (mirrors MediaManifestEntry.folder_id)
- MediaListDto gains folders: Vec<MediaFolder> (mirrors manifest.folders)
- get_media / import_* now return the folder tree in one round-trip

Frontend:
- types.ts: MediaItem.folderId, new MediaFolder interface, MediaList.folders,
  EditRequest gains createFolder + moveToFolder variants
- editActions.ts: createFolder() + moveToFolder() helpers
- mediaStore.ts: folders field + setCatalog setter; refreshMedia pulls both
- uiStore.ts: mediaPanelCurrentFolderId + setMediaPanelCurrentFolderId
- dict.ts: 9 new i18n keys (zh-CN + en) for folder UX
- MediaPanel.tsx: MediaTab filters by current folder, renders breadcrumb,
  "New Folder" button, and a grid of FolderTile + MediaCard. FolderTile
  supports double-click-to-enter and HTML5 drag-drop (asset -> folder
  reparents via moveToFolder).

Closes #58.
…:1 calibration (#40)

Settings:
- Restructure from single-page scroll to sidebar + detail layout (mirrors
  upstream SettingsView.swift): 180px sidebar with icon+label rows and an
  active capsule on the left edge.
- 8 pane entries (General, Appearance, Import, AI, MCP Instructions, Storage,
  Notifications, About) — the 7-pane scope from the issue, with General and
  Appearance kept as separate panes (upstream merges them under "general"
  but OpenTake's existing split is preserved).
- New MCPInstructionsPane: surfaces the built-in MCP server URL
  (http://127.0.0.1:19789/mcp) with copy button, plus one-line install
  commands for Claude Code, Codex, Cursor, and Claude Desktop. Mirrors
  upstream Help/MCPInstructionsPane.swift, consolidated into Settings per
  the issue.
- New StoragePane: cache + search-index fields (simplified placeholder;
  runtime statistics require Rust commands not yet wired).
- New NotificationsPane: generation-complete toggle (front-end-only for now).

Home (1:1 calibration with upstream ProjectCard.swift / HomeView.swift):
- ProjectCard: hover scale 1.02 -> 1.03 (match upstream).
- ProjectCard: title moved inside the thumbnail with a 60px bottom gradient
  overlay (upstream pattern), replacing the below-card title.
- ProjectCard: relative time (today / yesterday / N days ago / N weeks ago /
  N months ago) replaces the raw path display, using the existing
  RecentProject.openedAt timestamp.
- ProjectCard: delete button rounded to a circle (upstream glassEffect
  pattern).
- NewProjectCard: hover scale 1.02 -> 1.03.

i18n: 30+ new keys (zh-CN + en) for MCP, Storage, Notifications, and
relative-time strings.

Closes #40.
…card) (#39)

Adds an end-to-end "extract audio" path so users can save a video's
soundtrack as a standalone audio file from the media panel.

Backend (Rust):
- opentake-media: `MediaEngine::extract_audio` + `extract_audio_file`
  helper drive ffmpeg via the existing `ffmpeg_path()` CLI wrapper.
  `-y -i <in> -vn` plus codec args picked by output extension:
  .m4a/.aac → AAC 192k, .mp3 → libmp3lame 192k, .wav → pcm_s16le.
- src-tauri/media: new `extract_audio` Tauri command resolves a media
  id to its `MediaSource::External` absolute path, validates the file
  exists, then delegates to the engine. Returns the output path.
- src-tauri/lib: register `media::extract_audio` in `generate_handler!`.

Frontend (React/TS):
- api.ts: `extractAudio(mediaId, outPath)` wrapper; rejects outside
  Tauri (no ffmpeg available).
- MediaPanel.tsx: MediaCard gains a star-shaped "Extract Audio" button
  on the top-left, shown only when hovering a video that carries an
  audio track. Click opens a native save dialog (m4a/mp3/wav filters),
  invokes `extract_audio`, and surfaces a transient success/failure
  feedback message. `stopPropagation`+`preventDefault` keep the click
  from selecting the card.
- i18n dict.ts: 6 new keys (zh-CN + en) for the button title, hint,
  success, failure, and no-audio messages.

Closes #39.
Closes #93.

- New ClipContextMenu component with Split / Delete / Link-Unlink
- TimelineContainer: onContextMenu hit-tests the clip, selects it if
  needed, and opens the menu; closes on outside click or Escape
- i18n: contextMenu.split/delete/link/unlink (zh-CN + en)
Adds the standard clipboard shortcuts that were completely missing. Only
⌘C/⌘X/⌘V were absent — the unmodified C/V already switch tools (razor /
pointer), and the mod-prefixed branch had no handlers.

Frontend only:
- clipboardStore: new Zustand store holding deep snapshots of the selected
  clips plus the source first-frame, so a paste can re-place the group
  relative to the current playhead. UI-only, never persisted.
- editActions: copyClips / cutClips / pasteClipsAtPlayhead.
  - copy: snapshot selected clips + their track index + min startFrame.
  - cut: copy then deleteSelectedClips.
  - paste: offset each clip's startFrame by `activeFrame - sourceFirstFrame`,
    clear addLinkedAudio so the paste stands alone (mirrors upstream
    `pasteClipsAtPlayhead` link re-reflection), and select the new clips.
    Clips whose source track no longer exists are skipped.
- useKeyboardShortcuts: wire ⌘C / ⌘X / ⌘V inside the existing `if (mod)`
  block — no conflict with the unmodified C/V tool switches.
- i18n: 4 new keys (zh-CN + en) for copy / cut / paste / clipboardEmpty.

Closes #94.
…oast

Address review feedback on PR #105:

1. Rebased onto latest main (resolved import conflict: kept both trimToPlayheadEdits and useClipboardStore).

2. copyClips now expands link groups: if a selected clip has a linkGroupId, all linked companions are included in the clipboard (mirrors upstream copyClips), so a paste reproduces the video+audio pair.

3. pasteClipsAtPlayhead now re-establishes link groups after addClips: clips that shared a linkGroupId in the clipboard are re-linked via linkClips, preserving video+audio linkage.

4. Empty-clipboard paste now shows a toast (edit.clipboardEmpty) instead of silently doing nothing. Added toast mechanism to uiStore + Toast component in App.tsx.
1. 吸附迟滞 + 多探针
   - snap.ts: findSnapDelta 扩展接受 currentlySnapped + probeOffsets,
     返回 probeOffset,支持 sticky band 跨 pointer 事件保持
   - TimelineContainer.tsx: 新增 snapStateRef 跨事件保持吸附状态;
     onPointerMove move 分支收集所有 companions 的 start+end 作为探针组,
     改用 findSnapDelta(不再传 null);onPointerUp 清空 snapStateRef

2. 链接 offset 角标
   - clip.ts: 新增 linkOffsetForClip 计算链接组内帧偏移(相对 lead clip)
   - clipRenderer.ts: 新增 drawOffsetBadge 绘制红色圆角徽章 "+N"/"-N"
   - timelineCanvas.ts: clip 绘制参数增加 linkOffset,调用 drawOffsetBadge

3. 音量橡皮筋
   - clipRenderer.ts: 新增 drawVolumeEnvelope 绘制 volumeTrack 折线 + kf 圆点
     (半径 5px,黄色填充白色边框);拖拽时 ghost dot 跟随光标
   - hitTest.ts: 新增 audioVolumeKfHit 命中测试(8px 容差)
   - TimelineContainer.tsx: 新增 audioVolumeKf DragState + 拖拽逻辑;
     Cmd+click 空白处调 stampKeyframe
   - editActions.ts: moveKeyframe / stampKeyframe 实现为前端 wrapper
     (read-modify-write over setKeyframes,因后端仅暴露 SetKeyframes)

验证:pnpm tsc --noEmit 通过;pnpm build 通过;52 项测试全通过
后端:
- 新增 EditCommand::SwapMedia 变体,替换 clip 的 media_ref
- 校验新媒体存在于 manifest,若时长不足自动截断 duration + 调整 trim_end
- 保留所有编辑属性(transform/crop/keyframe tracks/grade/masks/effects/fade)
- media_type 隐含 source_clip_type(spec "sync media_type" 场景)
- 新增 EditRequest::SwapMedia DTO + into_command 映射
- 6 个单元测试:等长替换/较短截断/媒体不存在/同步 media_type/clip 不存在/undo

前端:
- types.ts 新增 swapMedia EditRequest 变体
- editActions.ts 新增 swapMedia(clipId, mediaRef, options?) action
- Inspector 新增「替换媒体」section + 内联媒体选择器
- i18n 中英文翻译

Closes #101
Backend (opentake-ops + src-tauri):
- Extend ClipProperties with crop, fade_in/out_frames, fade_in/out_interpolation,
  flip_horizontal, flip_vertical
- set_clip_properties writes new fields; fade clamps to clip duration;
  flip_* writes to transform.flip_*
- ClipPropertiesDto mirrors fields with serde camelCase
- 5 unit tests: crop sets+clears track, fade frames+interp, fade clamps,
  flip writes to transform, multiple fields at once

Frontend (web):
- clip.ts: 1:1 port of Rust Clip::*_at sampling methods (opacity/volume/
  rotation/size/topLeft/crop), fadeMultiplier, db<->linear, generic
  sampleKeyframeTrack with number/AnimPair/Crop lerp
- Inspector.tsx: read activeFrame from uiStore; show sampled values at
  playhead; switch to ReadOnlyValue + AnimatedHint when a track is active
- 4 new sections: Position (top-left x/y), Crop (4 edge insets 0-1),
  Flip (2 checkboxes), Fade (in/out frames + interpolation selects)
- Fade section appears on both video and audio tabs
- types.ts: extend ClipPropertiesReq with camelCase fields
- dict.ts: i18n keys for new sections (zh-CN + en)

Closes #97
Backend (Rust):
- Add `opentake_ops::ops::duplicate::duplicate_clips` — deep-copies each
  clip (keyframe tracks / grade / chroma / masks / effects / text /
  transform / crop / fades via `Clip: Clone`), mints a fresh id, shifts
  `start_frame` by `offset_frames`, lands on `target_track_indexes[i]`,
  clears `link_group_id`, and clears the destination range overwrite-style
  first (mirrors `move_clips`). 11 unit tests cover original retention,
  link-group clearing, keyframe deep copy, grade/masks/effects deep copy,
  multi-track targets, relative spacing, overwrite blocking, frame
  clamping, missing-clip skip, incompatible-track skip, and text/transform.
- Add `EditCommand::DuplicateClips` variant + `duplicate_clips_cmd` apply
  dispatch with validation (empty ids / length mismatch / missing clips)
  and the standard transact wrapper (snapshot -> mutate -> commit-if-changed
  -> version++). 7 command-level tests (creates copy, deep copies keyframes,
  clears link_group_id, missing clip errors, length mismatch errors, empty
  ids errors, undoable).
- Add `EditRequest::DuplicateClips` DTO in `src-tauri/src/commands.rs` +
  `into_command` mapping (direct field pass-through).

Frontend (React + TypeScript):
- Add `duplicateClips` variant to `EditRequest` in `types.ts`.
- Add `duplicateClips()` action in `editActions.ts` (applyAndRefresh).
- `TimelineContainer.tsx`: add `isDuplicate` flag (Alt key detection at
  pointer-down), `DropTarget` discriminated union (`existing` | `newTrack`),
  `newTrackTypeFor` helper (audio -> "audio", else -> "video"). `onPointerMove`
  computes `dropTarget` (existing track via `trackAt`, or `newTrack` when
  below the last track bottom). `onPointerUp` branches: newTrack ->
  `edit.insertTrack` -> `forceRefresh` -> `edit.duplicateClips`/`moveClips`
  with the new track index; existing track -> group-floor-clamped move or
  duplicate.
- `timelineCanvas.ts`: extend `DragPaint` move variant with `isDuplicate?`
  and `newTrackType?`. Render a dashed new-track drop indicator below the
  last track; render the ghost at the new-track Y when `newTrackType` is
  set; pass `isDuplicate` to `drawClip`.
- `clipRenderer.ts`: add `isDuplicate?` to `DrawOpts`; draw a yellow "+"
  badge in the top-right corner when `ghost && isDuplicate` so the user
  sees the gesture will copy rather than move.

Closes #98.
# Conflicts:
#	src-tauri/src/media.rs
#	web/src/components/media/MediaPanel.tsx
#	web/src/i18n/dict.ts
#	web/src/lib/types.ts
#	web/src/store/editActions.ts
…0624

# Conflicts:
#	web/src/components/timeline/TimelineContainer.tsx
baiqing added 21 commits June 24, 2026 11:21
…0624

# Conflicts:
#	web/src/store/editActions.ts
…0624

# Conflicts:
#	crates/opentake-ops/src/command.rs
# Conflicts:
#	src-tauri/src/lib.rs
#	src-tauri/src/media.rs
#	web/src/components/media/MediaPanel.tsx
#	web/src/i18n/dict.ts
#	web/src/lib/api.ts
# Conflicts:
#	web/src/components/inspector/Inspector.tsx
# Conflicts:
#	web/src/components/timeline/TimelineContainer.tsx
#	web/src/lib/clip.ts
# Conflicts:
#	crates/opentake-ops/src/command.rs
#	web/src/components/timeline/clipRenderer.ts
#	web/src/components/timeline/timelineCanvas.ts
# Conflicts:
#	web/src/components/media/MediaPanel.tsx
#	web/src/i18n/dict.ts
…ponsive scrub + menu position

Completes the Codex integration's unfinished editing fixes (the user-reported
blockers) and a few media/home polish items.

- Delete reliability: filter the selection to clip ids that still exist before
  RemoveClips/RippleDelete (one stale id made the core reject the whole batch, so
  ⌫ silently did nothing), and force a mirror refresh after delete on Tauri.
- Drop placement: media dragged from the panel now lands AT the cursor — start
  frame = drop X, on the track under drop Y — via addMediaToTimelineAt; if that
  track would overlap an existing clip it falls to another free compatible track,
  or opens a new one, instead of appending to the end / overwriting. The drop is
  handled in TimelineContainer (which owns the geometry); TimelineRegion keeps
  only the drag-over affordance (no double add).
- Selection highlight: clips now get a clear blue 2px outline (the old near-white
  border read as grey and was easy to miss).
- Preview scrub: replace the 140ms trailing debounce with a live frame + ~12fps
  rate gate, so dragging the playhead updates the picture immediately instead of
  waiting until you stop — still bounded, no per-frame composite storm.
- Context menu: appears at the cursor (clientX/clientY + viewport-edge flip) and
  no longer calls onClose() during render (moved to an effect).
- Folder import: recursive=true so the whole directory tree is imported.
- Generate button: shows an "AI generation coming soon" toast (backend still stub)
  instead of doing nothing.
- macOS chrome: traffic lights nudged down (trafficLightPosition {x:18,y:24}) and
  --titlebar-safe-top/left bumped so the sidebar title clears them.
- docs/UNFINISHED-EDITING-AND-MEDIA.md: full audio/video editing + media/MCP gap
  list (what's done / partial / not done) for the remaining work.
…tters, popups not white, delete surfaces errors

Root-causes from a focused investigation of the broken integration build:

- Pause jumps back to a random time: after Pause flips isPlaying=false, a still-
  queued rAF tick (the cancel is a passive effect, runs a beat later) read the
  now-master AUDIO element's advanced currentTime and wrote a FUTURE frame into
  the playhead. Guard tick with `if (!isPlaying) return;` so the straggler can't
  move the playhead. (Regression from 35d9eab muting video + promoting audio to
  clock master.)
- Dragging the timeline stutters: the live 80ms rate-gate fired a full
  ffmpeg+wgpu+PNG composite ~12×/sec during a scrub and hogged the main thread.
  Revert to the resource-light 140ms trailing debounce (a proper smooth scrub
  needs the streaming engine #53). (Regression from abab0c0.)
- Popups render white instead of dark/transparent: `--bg-elevated` (and
  `--bg-hover`) were used by the clip context menu, toast and text panel but
  never defined in tokens.css, so the background fell back to transparent/white.
  Define them.
- Delete does nothing / silently fails: wrap removeClips / rippleDelete in
  try/catch so the selection is always cleared and any backend rejection is shown
  as a toast instead of being swallowed by the fire-and-forget `void` caller.

Selection (symptom: "can't select") was already restored in abab0c0 — current
HEAD has audioVolumeKfHit/onContextMenu intact. The "audio on a no-audio project"
case (ffprobe reporting an empty audio stream → auto-linked audio clip) is left
for a follow-up to avoid breaking real video-with-audio linking.
…actually apply

THE root cause of "the whole editing suite is broken". serde's enum-level
`#[serde(rename_all = "camelCase")]` renames variant NAMES but NOT the fields of
struct variants. So every EditRequest variant with a multi-word field
(`clip_ids`, `clip_id`, `at_frame`, `track_index`, `offset_frames`, …) expected
snake_case JSON while the front end sends camelCase — the `command` arg failed to
deserialize ("missing field `clip_ids`") and the action silently did nothing.

Only single-word-field commands (AddClips{entries}, MoveClips{moves},
TrimClips{edits}) worked, which is why drag-in/move/trim looked fine but Delete,
Split-at-playhead, Duplicate, Inspector SetClipProperties, all keyframe ops,
Link/Unlink, SwapMedia and folder ops never applied. This bug predates the
integration (main has it too) and was long misdiagnosed as "selection not
working".

Fix: add `#[serde(rename_all = "camelCase")]` to every struct variant of
EditRequest (verified: enum-level does not propagate to variant fields). Add a
regression test deserializing camelCase removeClips/splitClip/insertClips/
rippleDeleteClips.
@appergb

appergb commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

关闭:不以整包合 main。理由:

@appergb appergb closed this Jun 24, 2026
cuic19053-hue pushed a commit to cuic19053-hue/OpenTake that referenced this pull request Jun 24, 2026
…ector/…) + blue selection + recursive folder import + traffic-light position

THE root cause of "the whole editing suite is broken": serde's enum-level
`#[serde(rename_all = "camelCase")]` renames variant NAMES but NOT the fields of
struct variants, so every EditRequest variant with a multi-word field (clip_ids,
clip_id, at_frame, track_index, offset_frames, …) expected snake_case while the
front end sends camelCase — the `command` arg failed to deserialize ("missing
field `clip_ids`") and the action silently did nothing. Only single-word-field
commands (AddClips/MoveClips/TrimClips) worked, which is why drag-in/move/trim
looked fine but Delete, Split-at-playhead, Duplicate, Inspector SetClipProperties,
all keyframe ops, Link/Unlink and folder ops never applied. Long misdiagnosed as
"selection not working".

- src-tauri/src/commands.rs: add `#[serde(rename_all = "camelCase")]` to every
  EditRequest struct variant + a regression test deserializing camelCase
  removeClips/splitClip/insertClips/rippleDeleteClips.
- clipRenderer: selected clips get a clear blue 2px outline (old near-white
  border read as grey).
- mediaActions: import folder recursively so nested media is imported.
- tauri.conf/tokens: nudge the macOS traffic lights down + widen the titlebar
  safe area so the sidebar title clears them.

Self-contained subset extracted from the integration branch (appergb#141); the buggy
feature PRs and the preview/playback rewrite (appergb#142) are tracked separately.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants