Skip to content
8 changes: 8 additions & 0 deletions apps/desktop/src-tauri/src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ pub(crate) fn sync_macos_dock_visibility(app: &tauri::AppHandle) {

#[cfg(target_os = "macos")]
fn macos_permission_status(permission: &OSPermission, initial_check: bool) -> OSPermissionStatus {
#[cfg(debug_assertions)]
if matches!(
permission,
OSPermission::ScreenRecording | OSPermission::Accessibility
) {
return OSPermissionStatus::Granted;
}

match permission {
OSPermission::ScreenRecording => {
let granted = scap_screencapturekit::has_permission();
Expand Down
14 changes: 11 additions & 3 deletions apps/desktop/src/routes/editor/Timeline/CaptionsTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export function CaptionsTrack(props: {
const minDuration = () =>
Math.max(MIN_SEGMENT_SECS, secsPerPixel() * MIN_SEGMENT_PIXELS);

const captionSegments = () => project.timeline?.captionSegments ?? [];
const captionSegments = createMemo(() =>
(project.timeline?.captionSegments ?? []).filter(
(s) => s.start < totalDuration(),
),
);
const selectedCaptionIndices = createMemo(() => {
const selection = editorState.timeline.selection;
if (!selection || selection.type !== "caption") return null;
Expand Down Expand Up @@ -157,7 +161,8 @@ export function CaptionsTrack(props: {
return indices.has(i());
});

const segmentWidth = () => segment.end - segment.start;
const segmentWidth = () =>
Math.min(segment.end, totalDuration()) - segment.start;

return (
<SegmentRoot
Expand All @@ -169,7 +174,10 @@ export function CaptionsTrack(props: {
isSelected() ? "border-green-7" : "border-transparent",
)}
innerClass="ring-green-6"
segment={segment}
segment={{
start: segment.start,
end: Math.min(segment.end, totalDuration()),
}}
onMouseDown={(e) => {
e.stopPropagation();
if (editorState.timeline.interactMode === "split") {
Expand Down
14 changes: 11 additions & 3 deletions apps/desktop/src/routes/editor/Timeline/KeyboardTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export function KeyboardTrack(props: {
const minDuration = () =>
Math.max(MIN_SEGMENT_SECS, secsPerPixel() * MIN_SEGMENT_PIXELS);

const keyboardSegments = () => project.timeline?.keyboardSegments ?? [];
const keyboardSegments = createMemo(() =>
(project.timeline?.keyboardSegments ?? []).filter(
(s) => s.start < totalDuration(),
),
);
Comment on lines +35 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Keyboard segments crossing the trim boundary not visually clamped

KeyboardTrack filters out segments where start >= totalDuration(), but keyboard segments whose start < totalDuration() and end > totalDuration() are rendered with their full unclamped width via the segment={segment} prop on SegmentRoot. This means they can visually overflow past the trim boundary, inconsistent with how CaptionsTrack clamps its segments. If a keyboard segment straddles the trim point, consider passing a clamped end to SegmentRoot the same way captions does:

segment={{ start: segment.start, end: Math.min(segment.end, totalDuration()) }}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/editor/Timeline/KeyboardTrack.tsx
Line: 35-39

Comment:
**Keyboard segments crossing the trim boundary not visually clamped**

`KeyboardTrack` filters out segments where `start >= totalDuration()`, but keyboard segments whose `start < totalDuration()` and `end > totalDuration()` are rendered with their full unclamped width via the `segment={segment}` prop on `SegmentRoot`. This means they can visually overflow past the trim boundary, inconsistent with how `CaptionsTrack` clamps its segments. If a keyboard segment straddles the trim point, consider passing a clamped `end` to `SegmentRoot` the same way captions does:

```
segment={{ start: segment.start, end: Math.min(segment.end, totalDuration()) }}
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, this is already handled in the PR. KeyboardTrack applies the same clamp as CaptionsTrack — both segmentWidth and the segment prop passed to SegmentRoot use Math.min(segment.end, totalDuration()), so keyboard segments that straddle the trim boundary are visually clamped at the trim point.

const selectedKeyboardIndices = createMemo(() => {
const selection = editorState.timeline.selection;
if (!selection || selection.type !== "keyboard") return null;
Expand Down Expand Up @@ -149,7 +153,8 @@ export function KeyboardTrack(props: {
return indices.has(i());
});

const segmentWidth = () => segment.end - segment.start;
const segmentWidth = () =>
Math.min(segment.end, totalDuration()) - segment.start;

return (
<SegmentRoot
Expand All @@ -161,7 +166,10 @@ export function KeyboardTrack(props: {
isSelected() ? "border-sky-7" : "border-transparent",
)}
innerClass="ring-sky-6"
segment={segment}
segment={{
start: segment.start,
end: Math.min(segment.end, totalDuration()),
}}
onMouseDown={(e) => {
e.stopPropagation();
if (editorState.timeline.interactMode === "split") {
Expand Down
14 changes: 6 additions & 8 deletions crates/audio/src/latency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,15 +685,13 @@ mod macos {
let mut latency_secs = total_frames as f64 / effective_rate;

match transport_kind {
OutputTransportKind::Airplay => {
if latency_secs < AIRPLAY_MIN_LATENCY_SECS {
latency_secs = AIRPLAY_MIN_LATENCY_SECS;
}
OutputTransportKind::Airplay if latency_secs < AIRPLAY_MIN_LATENCY_SECS => {
latency_secs = AIRPLAY_MIN_LATENCY_SECS;
}
OutputTransportKind::Wireless | OutputTransportKind::ContinuityWireless => {
if latency_secs < WIRELESS_MIN_LATENCY_SECS {
latency_secs = WIRELESS_MIN_LATENCY_SECS;
}
OutputTransportKind::Wireless | OutputTransportKind::ContinuityWireless
if latency_secs < WIRELESS_MIN_LATENCY_SECS =>
{
latency_secs = WIRELESS_MIN_LATENCY_SECS;
}
_ => {}
}
Expand Down
40 changes: 19 additions & 21 deletions crates/rendering-skia/src/layers/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,31 +277,29 @@ impl RecordableLayer for BackgroundLayer {

// Handle image loading if needed
match &new_background {
Background::Image { path } | Background::Wallpaper { path } => {
if self.image_path.as_ref() != Some(path) || self.loaded_image.is_none() {
// For now, we'll do synchronous loading. In a real implementation,
// this should be async or cached at a higher level
match std::fs::read(path) {
Ok(image_data) => {
let data = skia_safe::Data::new_copy(&image_data);
if let Some(image) = Image::from_encoded(&data) {
self.loaded_image = Some(image);
self.image_path = Some(path.clone());
} else {
tracing::error!("Failed to decode image: {:?}", path);
return Err(SkiaRenderingError::Other(anyhow::anyhow!(
"Failed to decode image"
)));
}
}
Err(e) => {
tracing::error!("Failed to load image: {:?}, error: {}", path, e);
Background::Image { path } | Background::Wallpaper { path }
if self.image_path.as_ref() != Some(path) || self.loaded_image.is_none() =>
{
match std::fs::read(path) {
Ok(image_data) => {
let data = skia_safe::Data::new_copy(&image_data);
if let Some(image) = Image::from_encoded(&data) {
self.loaded_image = Some(image);
self.image_path = Some(path.clone());
} else {
tracing::error!("Failed to decode image: {:?}", path);
return Err(SkiaRenderingError::Other(anyhow::anyhow!(
"Failed to load image: {}",
e
"Failed to decode image"
)));
}
}
Err(e) => {
tracing::error!("Failed to load image: {:?}, error: {}", path, e);
return Err(SkiaRenderingError::Other(anyhow::anyhow!(
"Failed to load image: {}",
e
)));
}
}
}
_ => {}
Expand Down
2 changes: 1 addition & 1 deletion crates/rendering/src/decoder/multi_position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ impl DecoderPoolManager {
.iter()
.map(|(&frame, &count)| (frame, count))
.collect();
hotspots.sort_by(|a, b| b.1.cmp(&a.1));
hotspots.sort_by_key(|b| std::cmp::Reverse(b.1));

let top_hotspots: Vec<f32> = hotspots
.into_iter()
Expand Down
2 changes: 1 addition & 1 deletion crates/scap-targets/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ fn main() {
})
.collect::<Vec<_>>();

relevant_windows.sort_by(|a, b| b.1.cmp(&a.1));
relevant_windows.sort_by_key(|b| std::cmp::Reverse(b.1));

// Print current topmost window info
if let Some((topmost_window, level)) = relevant_windows.first()
Expand Down
2 changes: 1 addition & 1 deletion crates/scap-targets/src/platform/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ impl WindowImpl {
})
.collect::<Vec<_>>();

windows_with_level.sort_by(|a, b| b.1.cmp(&a.1));
windows_with_level.sort_by_key(|b| std::cmp::Reverse(b.1));

windows_with_level.first().map(|(window, _)| *window)
}
Expand Down
Loading