Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"core:window:allow-set-theme",
"core:window:allow-set-progress-bar",
"core:window:allow-set-effects",
"core:window:allow-set-ignore-cursor-events",
"core:webview:default",
"core:webview:allow-create-webview-window",
"core:app:allow-version",
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src-tauri/src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ impl CameraPreviewManager {
LocalSet::new().block_on(
&rt,
renderer.run(window, default_state, reconfigure_rx, camera_rx),
)
);
info!("DONE");
});

self.preview = Some(InitializedCameraPreview { reconfigure });
Expand Down
57 changes: 52 additions & 5 deletions apps/desktop/src-tauri/src/hotkeys.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{RequestStartRecording, recording, windows::ShowCapWindow};
use crate::{
RequestOpenRecordingPicker, RequestStartRecording, recording,
recording_settings::RecordingTargetMode, windows::ShowCapWindow,
};
use global_hotkey::HotKeyState;
use serde::{Deserialize, Serialize};
use specta::Type;
Expand Down Expand Up @@ -40,14 +43,22 @@ impl From<Hotkey> for Shortcut {
}
}

#[derive(Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
#[derive(Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::enum_variant_names)]
pub enum HotkeyAction {
StartRecording,
StartStudioRecording,
StartInstantRecording,
StopRecording,
RestartRecording,
// TakeScreenshot,
OpenRecordingPicker,
OpenRecordingPickerDisplay,
OpenRecordingPickerWindow,
OpenRecordingPickerArea,
// Needed for deserialization of deprecated actions
#[serde(other)]
Other,
}

#[derive(Serialize, Deserialize, Type, Default)]
Expand Down Expand Up @@ -120,14 +131,50 @@ pub fn init(app: &AppHandle) {

async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), String> {
match action {
HotkeyAction::StartRecording => {
let _ = RequestStartRecording.emit(&app);
HotkeyAction::StartStudioRecording => {
let _ = RequestStartRecording {
mode: cap_recording::RecordingMode::Studio,
}
.emit(&app);
Ok(())
}
HotkeyAction::StartInstantRecording => {
let _ = RequestStartRecording {
mode: cap_recording::RecordingMode::Instant,
}
.emit(&app);
Ok(())
}
HotkeyAction::StopRecording => recording::stop_recording(app.clone(), app.state()).await,
HotkeyAction::RestartRecording => {
recording::restart_recording(app.clone(), app.state()).await
}
HotkeyAction::OpenRecordingPicker => {
let _ = RequestOpenRecordingPicker { target_mode: None }.emit(&app);
Ok(())
}
HotkeyAction::OpenRecordingPickerDisplay => {
let _ = RequestOpenRecordingPicker {
target_mode: Some(RecordingTargetMode::Display),
}
.emit(&app);
Ok(())
}
HotkeyAction::OpenRecordingPickerWindow => {
let _ = RequestOpenRecordingPicker {
target_mode: Some(RecordingTargetMode::Window),
}
.emit(&app);
Ok(())
}
HotkeyAction::OpenRecordingPickerArea => {
let _ = RequestOpenRecordingPicker {
target_mode: Some(RecordingTargetMode::Area),
}
.emit(&app);
Ok(())
}
HotkeyAction::Other => Ok(()),
}
}

Expand Down
90 changes: 76 additions & 14 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod permissions;
mod platform;
mod presets;
mod recording;
mod recording_settings;
mod target_select_overlay;
mod tray;
mod upload;
Expand Down Expand Up @@ -55,7 +56,7 @@ use scap::{
capturer::Capturer,
frame::{Frame, VideoFrame},
};
use scap_targets::{DisplayId, WindowId, bounds::LogicalBounds};
use scap_targets::{Display, DisplayId, WindowId, bounds::LogicalBounds};
use serde::{Deserialize, Serialize};
use serde_json::json;
use specta::Type;
Expand Down Expand Up @@ -84,8 +85,11 @@ use upload::{S3UploadMeta, create_or_get_video, upload_image, upload_video};
use web_api::ManagerExt as WebManagerExt;
use windows::{CapWindowId, EditorWindowIds, ShowCapWindow, set_window_transparent};

use crate::camera::CameraPreviewManager;
use crate::upload::build_video_meta;
use crate::{
camera::CameraPreviewManager,
recording_settings::{RecordingSettingsStore, RecordingTargetMode},
};
use crate::{recording::start_recording, upload::build_video_meta};

#[allow(clippy::large_enum_variant)]
pub enum RecordingState {
Expand Down Expand Up @@ -245,8 +249,7 @@ async fn set_camera_input(
state: MutableState<'_, App>,
id: Option<DeviceOrModelID>,
) -> Result<(), String> {
let app = state.read().await;
let camera_feed = app.camera_feed.clone();
let camera_feed = state.read().await.camera_feed.clone();

match id {
None => {
Expand Down Expand Up @@ -300,11 +303,18 @@ pub struct RecordingStarted;
pub struct RecordingStopped;

#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
pub struct RequestStartRecording;
pub struct RequestStartRecording {
pub mode: RecordingMode,
}

#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
pub struct RequestNewScreenshot;

#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
pub struct RequestOpenRecordingPicker {
pub target_mode: Option<RecordingTargetMode>,
}

#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
pub struct RequestOpenSettings {
page: String,
Expand Down Expand Up @@ -1900,6 +1910,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
RecordingStarted,
RecordingStopped,
RequestStartRecording,
RequestOpenRecordingPicker,
RequestNewScreenshot,
RequestOpenSettings,
NewNotification,
Expand All @@ -1910,14 +1921,15 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
recording::RecordingEvent,
RecordingDeleted,
target_select_overlay::TargetUnderCursor,
hotkeys::OnEscapePress
hotkeys::OnEscapePress,
])
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
.typ::<ProjectConfiguration>()
.typ::<AuthStore>()
.typ::<presets::PresetsStore>()
.typ::<hotkeys::HotkeysStore>()
.typ::<general_settings::GeneralSettingsStore>()
.typ::<recording_settings::RecordingSettingsStore>()
.typ::<cap_flags::Flags>();

#[cfg(debug_assertions)]
Expand Down Expand Up @@ -1970,7 +1982,13 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
.map(PathBuf::from)
else {
let app = app.clone();
tokio::spawn(async move { ShowCapWindow::Main.show(&app).await });
tokio::spawn(async move {
ShowCapWindow::Main {
init_target_mode: None,
}
.show(&app)
.await
});
return;
};

Expand Down Expand Up @@ -2109,7 +2127,11 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
} else {
println!("Permissions granted, showing main window");

let _ = ShowCapWindow::Main.show(&app).await;
let _ = ShowCapWindow::Main {
init_target_mode: None,
}
.show(&app)
.await;
}
}
});
Expand All @@ -2118,13 +2140,40 @@ pub async fn run(recording_logging_handle: LoggingHandle) {

tray::create_tray(&app).unwrap();

RequestNewScreenshot::listen_any_spawn(&app, |_, app| async move {
if let Err(e) = take_screenshot(app.clone(), app.state()).await {
eprintln!("Failed to take screenshot: {e}");
RequestStartRecording::listen_any_spawn(&app, async |event, app| {
let settings = RecordingSettingsStore::get(&app)
.ok()
.flatten()
.unwrap_or_default();

let _ = set_mic_input(app.state(), settings.mic_name).await;
let _ = set_camera_input(app.clone(), app.state(), settings.camera_id).await;

let _ = start_recording(
app.clone(),
app.state(),
recording::StartRecordingInputs {
capture_target: settings.target.unwrap_or_else(|| {
ScreenCaptureTarget::Display {
id: Display::primary().id(),
}
}),
capture_system_audio: settings.system_audio,
mode: event.mode,
},
)
.await;
});

RequestOpenRecordingPicker::listen_any_spawn(&app, async |event, app| {
let _ = ShowCapWindow::Main {
init_target_mode: event.target_mode,
}
.show(&app)
.await;
});

RequestOpenSettings::listen_any_spawn(&app, |payload, app| async move {
RequestOpenSettings::listen_any_spawn(&app, async |payload, app| {
let _ = ShowCapWindow::Settings {
page: Some(payload.page),
}
Expand All @@ -2149,6 +2198,15 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
match window_id {
CapWindowId::Main => {
let app = app.clone();

for (id, window) in app.webview_windows() {
if let Ok(CapWindowId::TargetSelectOverlay { .. }) =
CapWindowId::from_str(&id)
{
let _ = window.close();
}
}

tokio::spawn(async move {
let state = app.state::<ArcLock<App>>();
let app_state = &mut *state.write().await;
Expand Down Expand Up @@ -2274,7 +2332,11 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
} else {
let handle = _handle.clone();
tokio::spawn(async move {
let _ = ShowCapWindow::Main.show(&handle).await;
let _ = ShowCapWindow::Main {
init_target_mode: None,
}
.show(&handle)
.await;
});
}
}
Expand Down
14 changes: 11 additions & 3 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ use tauri_specta::Event;
use tracing::{error, info};

use crate::{
App, CurrentRecordingChanged, MutableState, NewStudioRecordingAdded, RecordingStopped,
VideoUploadInfo,
App, CurrentRecordingChanged, MutableState, NewStudioRecordingAdded, RecordingState,
RecordingStopped, VideoUploadInfo,
audio::AppSounds,
auth::AuthStore,
create_screenshot,
Expand Down Expand Up @@ -209,6 +209,10 @@ pub async fn start_recording(
state_mtx: MutableState<'_, App>,
inputs: StartRecordingInputs,
) -> Result<(), String> {
if !matches!(state_mtx.read().await.recording_state, RecordingState::None) {
return Err("Recording already in progress".to_string());
}

let id = uuid::Uuid::new_v4().to_string();
let general_settings = GeneralSettingsStore::get(&app).ok().flatten();
let general_settings = general_settings.as_ref();
Expand Down Expand Up @@ -634,7 +638,11 @@ pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> R
match settings.post_deletion_behaviour {
PostDeletionBehaviour::DoNothing => {}
PostDeletionBehaviour::ReopenRecordingWindow => {
let _ = ShowCapWindow::Main.show(&app).await;
let _ = ShowCapWindow::Main {
init_target_mode: None,
}
.show(&app)
.await;
}
}
}
Expand Down
60 changes: 60 additions & 0 deletions apps/desktop/src-tauri/src/recording_settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use cap_recording::{RecordingMode, feeds::camera::DeviceOrModelID, sources::ScreenCaptureTarget};
use serde_json::json;
use tauri::{AppHandle, Wry};
use tauri_plugin_store::StoreExt;

#[derive(serde::Serialize, serde::Deserialize, specta::Type, Debug, Clone, Copy)]
#[serde(rename_all = "camelCase")]
pub enum RecordingTargetMode {
Display,
Window,
Area,
}

#[derive(serde::Serialize, serde::Deserialize, specta::Type, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct RecordingSettingsStore {
pub target: Option<ScreenCaptureTarget>,
pub mic_name: Option<String>,
pub camera_id: Option<DeviceOrModelID>,
pub mode: Option<RecordingMode>,
pub system_audio: bool,
}

impl RecordingSettingsStore {
const KEY: &'static str = "recording_settings";

pub fn get(app: &AppHandle<Wry>) -> Result<Option<Self>, String> {
match app.store("store").map(|s| s.get(Self::KEY)) {
Ok(Some(store)) => {
// Handle potential deserialization errors gracefully
match serde_json::from_value(store) {
Ok(settings) => Ok(Some(settings)),
Err(e) => Err(format!("Failed to deserialize general settings store: {e}")),
}
}
_ => Ok(None),
}
}

// i don't trust anyone to not overwrite the whole store lols
pub fn update(app: &AppHandle, update: impl FnOnce(&mut Self)) -> Result<(), String> {
let Ok(store) = app.store("store") else {
return Err("Store not found".to_string());
};

let mut settings = Self::get(app)?.unwrap_or_default();
update(&mut settings);
store.set(Self::KEY, json!(settings));
store.save().map_err(|e| e.to_string())
}

fn save(&self, app: &AppHandle) -> Result<(), String> {
let Ok(store) = app.store("store") else {
return Err("Store not found".to_string());
};

store.set(Self::KEY, json!(self));
store.save().map_err(|e| e.to_string())
}
}
Loading
Loading