diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index c3eca8084d..a1c3fd2169 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -268,16 +268,11 @@ impl WindowFocusManager { let cap_main = CapWindowId::Main.get(app); let cap_settings = CapWindowId::Settings.get(app); - let has_cap_main = cap_main - .as_ref() - .and_then(|v| Some(v.is_minimized().ok()? || !v.is_visible().ok()?)) - .unwrap_or(true); - let has_cap_settings = cap_settings - .and_then(|v| Some(v.is_minimized().ok()? || !v.is_visible().ok()?)) - .unwrap_or(true); - - // Close the overlay if the cap main and settings are not available. - if has_cap_main && has_cap_settings { + let main_window_available = cap_main.is_some(); + let settings_window_available = cap_settings.is_some(); + + // Close the overlay if both cap windows are gone. + if !main_window_available && !settings_window_available { window.hide().ok(); break; } diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs index d07632be35..c7268e5bb4 100644 --- a/apps/desktop/src-tauri/src/tray.rs +++ b/apps/desktop/src-tauri/src/tray.rs @@ -1,5 +1,7 @@ -use crate::windows::ShowCapWindow; -use crate::{RecordingStarted, RecordingStopped, RequestOpenSettings, recording}; +use crate::{ + RecordingStarted, RecordingStopped, RequestOpenRecordingPicker, RequestOpenSettings, recording, + recording_settings::RecordingTargetMode, windows::ShowCapWindow, +}; use std::sync::{ Arc, @@ -18,6 +20,9 @@ use tauri_specta::Event; pub enum TrayItem { OpenCap, + RecordDisplay, + RecordWindow, + RecordArea, PreviousRecordings, PreviousScreenshots, OpenSettings, @@ -29,6 +34,9 @@ impl From for MenuId { fn from(value: TrayItem) -> Self { match value { TrayItem::OpenCap => "open_cap", + TrayItem::RecordDisplay => "record_display", + TrayItem::RecordWindow => "record_window", + TrayItem::RecordArea => "record_area", TrayItem::PreviousRecordings => "previous_recordings", TrayItem::PreviousScreenshots => "previous_screenshots", TrayItem::OpenSettings => "open_settings", @@ -45,6 +53,9 @@ impl TryFrom for TrayItem { fn try_from(value: MenuId) -> Result { match value.0.as_str() { "open_cap" => Ok(TrayItem::OpenCap), + "record_display" => Ok(TrayItem::RecordDisplay), + "record_window" => Ok(TrayItem::RecordWindow), + "record_area" => Ok(TrayItem::RecordArea), "previous_recordings" => Ok(TrayItem::PreviousRecordings), "previous_screenshots" => Ok(TrayItem::PreviousScreenshots), "open_settings" => Ok(TrayItem::OpenSettings), @@ -59,7 +70,28 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { let menu = Menu::with_items( app, &[ - &MenuItem::with_id(app, TrayItem::OpenCap, "New Recording", true, None::<&str>)?, + &MenuItem::with_id( + app, + TrayItem::OpenCap, + "Open Main Window", + true, + None::<&str>, + )?, + &MenuItem::with_id( + app, + TrayItem::RecordDisplay, + "Record Display", + true, + None::<&str>, + )?, + &MenuItem::with_id( + app, + TrayItem::RecordWindow, + "Record Window", + true, + None::<&str>, + )?, + &MenuItem::with_id(app, TrayItem::RecordArea, "Record Area", true, None::<&str>)?, &PredefinedMenuItem::separator(app)?, // &MenuItem::with_id( // app, @@ -109,6 +141,24 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { .await; }); } + Ok(TrayItem::RecordDisplay) => { + let _ = RequestOpenRecordingPicker { + target_mode: Some(RecordingTargetMode::Display), + } + .emit(&app_handle); + } + Ok(TrayItem::RecordWindow) => { + let _ = RequestOpenRecordingPicker { + target_mode: Some(RecordingTargetMode::Window), + } + .emit(&app_handle); + } + Ok(TrayItem::RecordArea) => { + let _ = RequestOpenRecordingPicker { + target_mode: Some(RecordingTargetMode::Area), + } + .emit(&app_handle); + } Ok(TrayItem::PreviousRecordings) => { let _ = RequestOpenSettings { page: "recordings".to_string(), diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index fc8ab48541..d6c29095cd 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -224,6 +224,8 @@ impl ShowCapWindow { } if let Some(window) = self.id(app).get(app) { + window.show().ok(); + window.unminimize().ok(); window.set_focus().ok(); return Ok(window); } @@ -636,7 +638,7 @@ impl ShowCapWindow { .maximized(false) .resizable(false) .fullscreen(false) - .shadow(true) + .shadow(!cfg!(windows)) .always_on_top(true) .transparent(true) .visible_on_all_workspaces(true) diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx index f1cb10010a..31b49cb337 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx @@ -4,8 +4,8 @@ import { cx } from "cva"; import { type Component, type ComponentProps, + createEffect, createSignal, - onMount, Show, } from "solid-js"; import { trackEvent } from "~/utils/analytics"; @@ -83,12 +83,10 @@ export function MicrophoneSelectBase(props: { const audioLevel = () => (1 - Math.max((dbs() ?? 0) + DB_SCALE, 0) / DB_SCALE) ** 0.5; - // Initialize audio input if needed - only once when component mounts - onMount(() => { + createEffect(() => { if (!props.value || !permissionGranted() || isInitialized()) return; setIsInitialized(true); - // Ensure the selected microphone is activated so levels flow in void handleMicrophoneChange({ name: props.value }); }); diff --git a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx index 7e92e7e3b0..82c008f325 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main/index.tsx @@ -53,6 +53,7 @@ import { type CaptureWindowWithThumbnail, commands, type DeviceOrModelID, + type RecordingTargetMode, type ScreenCaptureTarget, } from "~/utils/tauri"; import IconLucideAppWindowMac from "~icons/lucide/app-window-mac"; @@ -434,10 +435,13 @@ function Page() { createUpdateCheck(); onMount(async () => { - const targetMode = (window as any).__CAP__.initialTargetMode; + const { __CAP__ } = window as typeof window & { + __CAP__?: { initialTargetMode?: RecordingTargetMode | null }; + }; + const targetMode = __CAP__?.initialTargetMode ?? null; setOptions({ targetMode }); - if (rawOptions.targetMode) commands.openTargetSelectOverlays(null); - else commands.closeTargetSelectOverlays(); + if (targetMode) await commands.openTargetSelectOverlays(null); + else await commands.closeTargetSelectOverlays(); const currentWindow = getCurrentWindow(); @@ -616,6 +620,12 @@ function Page() { const setCamera = createCameraMutation(); onMount(() => { + if (rawOptions.micName) { + setMicInput + .mutateAsync(rawOptions.micName) + .catch((error) => console.error("Failed to set mic input:", error)); + } + if (rawOptions.cameraID && "ModelID" in rawOptions.cameraID) setCamera.mutate({ ModelID: rawOptions.cameraID.ModelID }); else if (rawOptions.cameraID && "DeviceID" in rawOptions.cameraID) diff --git a/apps/desktop/src/routes/editor/Player.tsx b/apps/desktop/src/routes/editor/Player.tsx index ad986e4b64..9b0245d12d 100644 --- a/apps/desktop/src/routes/editor/Player.tsx +++ b/apps/desktop/src/routes/editor/Player.tsx @@ -382,36 +382,42 @@ function PreviewCanvas() { {(currentFrame) => { const padding = 4; + const frameWidth = () => currentFrame().width; + const frameHeight = () => currentFrame().data.height; - const containerAspect = () => { - if (containerBounds.width && containerBounds.height) { - return ( - (containerBounds.width - padding * 2) / - (containerBounds.height - padding * 2) - ); - } + const availableWidth = () => + Math.max((containerBounds.width ?? 0) - padding * 2, 0); + const availableHeight = () => + Math.max((containerBounds.height ?? 0) - padding * 2, 0); - return 1; + const containerAspect = () => { + const width = availableWidth(); + const height = availableHeight(); + if (width === 0 || height === 0) return 1; + return width / height; }; - const frameAspect = () => - currentFrame().width / currentFrame().data.height; + const frameAspect = () => { + const width = frameWidth(); + const height = frameHeight(); + if (width === 0 || height === 0) return containerAspect(); + return width / height; + }; const size = () => { + let width: number; + let height: number; if (frameAspect() < containerAspect()) { - const height = (containerBounds.height ?? 0) - padding * 1; - - return { - width: height * frameAspect(), - height, - }; + height = availableHeight(); + width = height * frameAspect(); + } else { + width = availableWidth(); + height = width / frameAspect(); } - const width = (containerBounds.width ?? 0) - padding * 2; - return { - width, - height: width / frameAspect(), + width: Math.min(width, frameWidth()), + height: Math.min(height, frameHeight()), }; }; @@ -419,15 +425,15 @@ function PreviewCanvas() {
); diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index a3b3afd5df..633eac79c0 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -376,7 +376,14 @@ impl output_pipeline::AudioSource for SystemAudioSource { } fn stop(&mut self) -> impl Future> { - let res = self.capturer.pause().map_err(Into::into); - async { res } + let res = self.capturer.pause(); + + async move { + if let Err(err) = res { + warn!("system audio capturer pause failed: {err}"); + } + + Ok(()) + } } } diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs index c6d2e9fa42..75ef41659c 100644 --- a/crates/recording/src/studio_recording.rs +++ b/crates/recording/src/studio_recording.rs @@ -23,7 +23,7 @@ use std::{ time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; use tokio::sync::watch; -use tracing::{Instrument, debug, error_span, info, trace}; +use tracing::{Instrument, debug, error_span, info, trace, warn}; #[allow(clippy::large_enum_variant)] enum ActorState { @@ -311,12 +311,20 @@ impl Pipeline { cursor.actor.stop(); } + let system_audio = match system_audio.transpose() { + Ok(value) => value, + Err(err) => { + warn!("system audio pipeline failed during stop: {err:#}"); + None + } + }; + Ok(FinishedPipeline { start_time: self.start_time, screen: screen.context("screen")?, microphone: microphone.transpose().context("microphone")?, camera: camera.transpose().context("camera")?, - system_audio: system_audio.transpose().context("system_audio")?, + system_audio, cursor: self.cursor, }) }