diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 787bb4c..fe96489 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -130,90 +130,89 @@ fn msg(e: CmdError) -> String { #[derive(Debug, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum EditRequest { - AddClips { - entries: Vec, - }, + #[serde(rename_all = "camelCase")] + AddClips { entries: Vec }, + #[serde(rename_all = "camelCase")] InsertClips { track_index: usize, at_frame: i32, entries: Vec, }, - MoveClips { - moves: Vec, - }, - RemoveClips { - clip_ids: Vec, - }, - SplitClip { - clip_id: String, - at_frame: i32, - }, - TrimClips { - edits: Vec, - }, + #[serde(rename_all = "camelCase")] + MoveClips { moves: Vec }, + #[serde(rename_all = "camelCase")] + RemoveClips { clip_ids: Vec }, + #[serde(rename_all = "camelCase")] + SplitClip { clip_id: String, at_frame: i32 }, + #[serde(rename_all = "camelCase")] + TrimClips { edits: Vec }, + #[serde(rename_all = "camelCase")] SetClipProperties { clip_ids: Vec, properties: ClipPropertiesDto, }, + #[serde(rename_all = "camelCase")] SetKeyframes { clip_id: String, property: KeyframePropertyDto, payload: KeyframePayloadDto, }, + #[serde(rename_all = "camelCase")] StampKeyframe { clip_id: String, property: KeyframePropertyDto, frame: i32, }, + #[serde(rename_all = "camelCase")] RemoveKeyframe { clip_id: String, property: KeyframePropertyDto, frame: i32, }, + #[serde(rename_all = "camelCase")] MoveKeyframe { clip_id: String, property: KeyframePropertyDto, from_frame: i32, to_frame: i32, }, + #[serde(rename_all = "camelCase")] SetKeyframeInterpolation { clip_id: String, property: KeyframePropertyDto, frame: i32, interpolation: Interpolation, }, + #[serde(rename_all = "camelCase")] RippleDeleteRanges { track_index: usize, ranges: Vec, }, - RippleDeleteClips { - clip_ids: Vec, - }, - AddTexts { - entries: Vec, - }, - Link { - clip_ids: Vec, - }, - Unlink { - clip_ids: Vec, - }, - RemoveTracks { - track_indexes: Vec, - }, - InsertTrack { - kind: ClipType, - }, + #[serde(rename_all = "camelCase")] + RippleDeleteClips { clip_ids: Vec }, + #[serde(rename_all = "camelCase")] + AddTexts { entries: Vec }, + #[serde(rename_all = "camelCase")] + Link { clip_ids: Vec }, + #[serde(rename_all = "camelCase")] + Unlink { clip_ids: Vec }, + #[serde(rename_all = "camelCase")] + RemoveTracks { track_indexes: Vec }, + #[serde(rename_all = "camelCase")] + InsertTrack { kind: ClipType }, + #[serde(rename_all = "camelCase")] SetTrackProps { track_index: usize, muted: Option, hidden: Option, sync_locked: Option, }, + #[serde(rename_all = "camelCase")] CreateFolder { name: String, parent_folder_id: Option, }, + #[serde(rename_all = "camelCase")] MoveToFolder { asset_ids: Vec, folder_id: Option, @@ -588,3 +587,27 @@ impl KeyframePayloadDto { }) } } + +#[cfg(test)] +mod edit_request_serde_tests { + use super::EditRequest; + + // Regression: the front end sends camelCase keys (clipIds/clipId/atFrame…). + // serde's enum-level `rename_all` does NOT rename struct-variant fields, so + // each variant needs its own `rename_all`; without it RemoveClips/SplitClip/ + // … failed to deserialize ("missing field `clip_ids`") and delete/split/etc. + // silently did nothing. + #[test] + fn deserializes_camelcase_multiword_commands() { + serde_json::from_str::(r#"{"type":"removeClips","clipIds":["a"]}"#) + .expect("removeClips camelCase"); + serde_json::from_str::(r#"{"type":"splitClip","clipId":"a","atFrame":5}"#) + .expect("splitClip camelCase"); + serde_json::from_str::( + r#"{"type":"insertClips","trackIndex":0,"atFrame":0,"entries":[]}"#, + ) + .expect("insertClips camelCase"); + serde_json::from_str::(r#"{"type":"rippleDeleteClips","clipIds":["a"]}"#) + .expect("rippleDeleteClips camelCase"); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c134255..5c824a0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -26,6 +26,7 @@ "minHeight": 480, "titleBarStyle": "Overlay", "hiddenTitle": true, + "trafficLightPosition": { "x": 18, "y": 24 }, "backgroundColor": "#0A0A0A", "dragDropEnabled": false } diff --git a/web/src/components/timeline/clipRenderer.ts b/web/src/components/timeline/clipRenderer.ts index aeb8916..200070d 100644 --- a/web/src/components/timeline/clipRenderer.ts +++ b/web/src/components/timeline/clipRenderer.ts @@ -148,11 +148,12 @@ export function drawClip( ctx.fillRect(x, y, CLIP.stripWidth, height); ctx.restore(); - // 5. Border (ClipRenderer:121-132). + // 5. Border (ClipRenderer:121-132). Selected = a clear blue 2px outline (the + // old near-white border read as grey on the clip body and was easy to miss). roundRectPath(ctx, x, y, width, height, r); if (opts.isSelected) { - ctx.strokeStyle = "rgba(255,255,255,0.9)"; - ctx.lineWidth = 1.5; + ctx.strokeStyle = "rgba(56,139,253,1)"; + ctx.lineWidth = 2; } else { ctx.strokeStyle = BORDER.primary; ctx.lineWidth = 0.5; diff --git a/web/src/store/mediaActions.ts b/web/src/store/mediaActions.ts index 8d3acc7..aeb3114 100644 --- a/web/src/store/mediaActions.ts +++ b/web/src/store/mediaActions.ts @@ -40,7 +40,7 @@ export async function importFolderViaDialog(): Promise { }); if (typeof selected !== "string") return; // cancelled store.setImporting(true); - await api.importFolder(selected, false); + await api.importFolder(selected, true); await refreshMedia(); } catch (error: unknown) { store.setError(getErrorMessage(error)); diff --git a/web/src/styles/tokens.css b/web/src/styles/tokens.css index 808246b..5ef12a8 100644 --- a/web/src/styles/tokens.css +++ b/web/src/styles/tokens.css @@ -130,8 +130,8 @@ /* Window chrome: macOS traffic-light safe area for the Overlay title bar (titleBarStyle:Overlay keeps the close/min/zoom buttons floating over the top-left of the content). Top-of-window UI must clear this region. */ - --titlebar-safe-left: 78px; - --titlebar-safe-top: 30px; + --titlebar-safe-left: 82px; + --titlebar-safe-top: 44px; /* §1.12 Animation (AppTheme.swift:282-285) */ --anim-hover: 150ms;