diff --git a/apps/desktop/src-tauri/src/auth.rs b/apps/desktop/src-tauri/src/auth.rs index eb04d0b8..6666c292 100644 --- a/apps/desktop/src-tauri/src/auth.rs +++ b/apps/desktop/src-tauri/src/auth.rs @@ -1,8 +1,10 @@ use serde::{Deserialize, Serialize}; use specta::Type; -use tauri::{AppHandle, Manager, Wry}; +use tauri::{AppHandle, Manager, Runtime, Wry}; use tauri_plugin_store::{with_store, StoreCollection}; +use web_api::ManagerExt; + use crate::web_api; #[derive(Serialize, Deserialize, Type)] @@ -19,9 +21,9 @@ pub struct Plan { } impl AuthStore { - pub fn get(app: &AppHandle) -> Result, String> { + pub fn get(app: &AppHandle) -> Result, String> { let stores = app - .try_state::>() + .try_state::>() .ok_or("Store not found")?; with_store(app.clone(), stores, "store", |store| { let Some(store) = store.get("auth").cloned() else { @@ -39,11 +41,10 @@ impl AuthStore { return Err("User not authenticated".to_string()); }; - let response = web_api::do_authed_request(&auth, |client| { - client.get(web_api::make_url("/api/desktop/plan")) - }) - .await - .map_err(|e| e.to_string())?; + let response = app + .authed_api_request(|client| client.get(web_api::make_url("/api/desktop/plan"))) + .await + .map_err(|e| e.to_string())?; if response.status() == reqwest::StatusCode::UNAUTHORIZED { println!("Authentication expired. Please log in again."); @@ -88,3 +89,6 @@ impl AuthStore { .map_err(|e| e.to_string()) } } + +#[derive(specta::Type, serde::Serialize, tauri_specta::Event, Debug, Clone)] +pub struct AuthenticationInvalid; diff --git a/apps/desktop/src-tauri/src/events.rs b/apps/desktop/src-tauri/src/events.rs new file mode 100644 index 00000000..e69de29b diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 82fccb3c..51e04e27 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -8,6 +8,7 @@ mod flags; mod general_settings; mod hotkeys; mod macos; +mod main_window; mod notifications; mod permissions; mod recording; @@ -29,6 +30,7 @@ use cap_utils::create_named_pipe; use display::{list_capture_windows, Bounds, CaptureTarget, FPS}; use general_settings::GeneralSettingsStore; use image::{ImageBuffer, Rgba}; +use main_window::create_main_window; use mp4::Mp4Reader; use num_traits::ToBytes; use objc2_app_kit::NSScreenSaverWindowLevel; @@ -1435,47 +1437,12 @@ async fn list_audio_devices() -> Result, ()> { #[tauri::command(async)] #[specta::specta] fn open_main_window(app: AppHandle) { - if let Some(window) = app.get_webview_window("main") { - println!("Main window already exists, setting focus"); - window.set_focus().ok(); - return; - } - let permissions = permissions::do_permissions_check(false); if !permissions.screen_recording.permitted() || !permissions.accessibility.permitted() { return; } - println!("Creating new main window"); - let Some(window) = WebviewWindow::builder(&app, "main", tauri::WebviewUrl::App("/".into())) - .title("Cap") - .inner_size(300.0, 375.0) - .resizable(false) - .maximized(false) - .shadow(true) - .accept_first_mouse(true) - .transparent(true) - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .theme(Some(tauri::Theme::Light)) - .build() - .ok() - else { - println!("Failed to create main window"); - return; - }; - - println!("Creating overlay titlebar"); - window.create_overlay_titlebar().unwrap(); - #[cfg(target_os = "macos")] - { - println!("Setting traffic lights inset for macOS"); - window.set_traffic_lights_inset(14.0, 22.0).unwrap(); - } - - println!("Showing main window"); - window.show().unwrap(); - println!("Main window opened successfully"); + create_main_window(app); } #[tauri::command] @@ -1579,37 +1546,37 @@ async fn open_settings_window(app: AppHandle, page: String) { #[tauri::command] #[specta::specta] async fn upload_rendered_video( - _app: AppHandle, + app: AppHandle, video_id: String, project: ProjectConfiguration, ) -> Result { - let Ok(Some(mut auth)) = AuthStore::get(&_app) else { + let Ok(Some(mut auth)) = AuthStore::get(&app) else { println!("not authenticated!"); return Ok(UploadResult::NotAuthenticated); }; - notifications::send_notification(&_app, notifications::NotificationType::ShareableLinkCopied); + notifications::send_notification(&app, notifications::NotificationType::ShareableLinkCopied); // Check if user has an upgraded plan if !auth.is_upgraded() { // Fetch and update plan information - if let Err(e) = AuthStore::fetch_and_update_plan(&_app).await { + if let Err(e) = AuthStore::fetch_and_update_plan(&app).await { println!("Failed to update plan information: {}", e); return Ok(UploadResult::PlanCheckFailed); } // Refresh auth information after update - auth = AuthStore::get(&_app).unwrap().unwrap(); + auth = AuthStore::get(&app).unwrap().unwrap(); // Re-check upgraded status after refresh if !auth.is_upgraded() { // Open upgrade window instead of returning an error - open_upgrade_window(_app).await; + open_upgrade_window(app).await; return Ok(UploadResult::UpgradeRequired); } } - let editor_instance = upsert_editor_instance(&_app, video_id.clone()).await; + let editor_instance = upsert_editor_instance(&app, video_id.clone()).await; let mut meta = editor_instance.meta(); @@ -1627,12 +1594,12 @@ async fn upload_rendered_video( } }; - let uploaded_video = upload_video(video_id.clone(), &auth, output_path, false).await?; + let uploaded_video = upload_video(&app, video_id.clone(), output_path, false).await?; - let general_settings = GeneralSettingsStore::get(&_app)?; + let general_settings = GeneralSettingsStore::get(&app)?; if let Some(settings) = general_settings { if settings.upload_individual_files { - let video_dir = _app + let video_dir = app .path() .app_data_dir() .unwrap() @@ -1650,7 +1617,7 @@ async fn upload_rendered_video( let file_name = file_path.file_name().unwrap().to_str().unwrap(); let result = if is_audio { upload_individual_file( - &auth, + &app, file_path.clone(), uploaded_video.config.clone(), file_name, @@ -1659,7 +1626,7 @@ async fn upload_rendered_video( .await } else { upload_individual_file( - &auth, + &app, file_path.clone(), uploaded_video.config.clone(), file_name, @@ -1684,7 +1651,7 @@ async fn upload_rendered_video( id: uploaded_video.id.clone(), }); meta.save_for_project(); - RecordingMetaChanged { id: video_id }.emit(&_app).ok(); + RecordingMetaChanged { id: video_id }.emit(&app).ok(); uploaded_video.link }; @@ -1773,7 +1740,7 @@ async fn upload_screenshot( sharing.link.clone() } else { // Upload the screenshot - let uploaded = upload_image(&auth, screenshot_path.clone()) + let uploaded = upload_image(&app, screenshot_path.clone()) .await .map_err(|e| e.to_string())?; diff --git a/apps/desktop/src-tauri/src/main_window.rs b/apps/desktop/src-tauri/src/main_window.rs new file mode 100644 index 00000000..0fd4f7fb --- /dev/null +++ b/apps/desktop/src-tauri/src/main_window.rs @@ -0,0 +1,41 @@ +use tauri::{AppHandle, Manager, WebviewWindow}; + +pub fn create_main_window(app: AppHandle) -> WebviewWindow { + if let Some(window) = app.get_webview_window("main") { + println!("Main window already exists, setting focus"); + window.set_focus().ok(); + return window; + } + + println!("Creating new main window"); + let window = WebviewWindow::builder(&app, "main", tauri::WebviewUrl::App("/".into())) + .title("Cap") + .inner_size(300.0, 375.0) + .resizable(false) + .maximized(false) + .shadow(true) + .accept_first_mouse(true) + .transparent(true) + .hidden_title(true) + .title_bar_style(tauri::TitleBarStyle::Overlay) + .theme(Some(tauri::Theme::Light)) + .build() + .unwrap(); + + #[cfg(target_os = "macos")] + { + use tauri_plugin_decorum::WebviewWindowExt; + + println!("Creating overlay titlebar"); + window.create_overlay_titlebar().unwrap(); + + println!("Setting traffic lights inset for macOS"); + window.set_traffic_lights_inset(14.0, 22.0).unwrap(); + } + + println!("Showing main window"); + window.show().unwrap(); + println!("Main window opened successfully"); + + window +} diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 475cd8b8..e47f4208 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -4,9 +4,13 @@ use image::codecs::jpeg::JpegEncoder; use image::ImageReader; use reqwest::{multipart::Form, Client, StatusCode}; use std::path::PathBuf; +use tauri::AppHandle; use tokio::task; -use crate::{auth::AuthStore, web_api}; +use crate::{ + auth::AuthStore, + web_api::{self, ManagerExt}, +}; #[derive(serde::Deserialize, Clone)] pub struct S3UploadMeta { @@ -72,8 +76,8 @@ pub struct UploadedAudio { } pub async fn upload_video( + app: &AppHandle, video_id: String, - auth: &AuthStore, file_path: PathBuf, is_individual: bool, ) -> Result { @@ -86,7 +90,7 @@ pub async fn upload_video( .to_string(); let client = reqwest::Client::new(); - let s3_config = get_s3_config(&client, &auth, false).await?; + let s3_config = get_s3_config(&app, false).await?; let file_key = if is_individual { format!( @@ -107,7 +111,7 @@ pub async fn upload_video( }, )?; - let (upload_url, mut form) = presigned_s3_url(body, &auth).await?; + let (upload_url, mut form) = presigned_s3_url(app, body).await?; let file_bytes = tokio::fs::read(&file_path) .await @@ -128,7 +132,7 @@ pub async fn upload_video( .join("display.jpg"); let screenshot_upload = if screenshot_path.exists() { - Some(prepare_screenshot_upload(&s3_config, &auth, screenshot_path).await?) + Some(prepare_screenshot_upload(&app, &s3_config, screenshot_path).await?) } else { None }; @@ -190,7 +194,7 @@ pub async fn upload_video( )) } -pub async fn upload_image(auth: &AuthStore, file_path: PathBuf) -> Result { +pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result { let file_name = file_path .file_name() .and_then(|name| name.to_str()) @@ -199,7 +203,7 @@ pub async fn upload_image(auth: &AuthStore, file_path: PathBuf) -> Result Result Result Result { +pub async fn upload_audio(app: &AppHandle, file_path: PathBuf) -> Result { let file_name = file_path .file_name() .and_then(|name| name.to_str()) @@ -266,7 +270,7 @@ pub async fn upload_audio(auth: &AuthStore, file_path: PathBuf) -> Result Result Result Result { +async fn get_s3_config(app: &AppHandle, is_screenshot: bool) -> Result { let origin = "http://tauri.localhost"; let config_url = web_api::make_url(if is_screenshot { format!( @@ -350,7 +350,8 @@ async fn get_s3_config( ) }); - let response = web_api::do_authed_request(auth, |client| client.get(config_url)) + let response = app + .authed_api_request(|client| client.get(config_url)) .await .map_err(|e| format!("Failed to send request to Next.js handler: {}", e))?; @@ -374,16 +375,17 @@ async fn get_s3_config( } async fn presigned_s3_url( + app: &AppHandle, body: S3VideoUploadBody, - auth: &AuthStore, ) -> Result<(String, Form), String> { - let response = web_api::do_authed_request(auth, |client| { - client - .post(web_api::make_url("/api/upload/signed")) - .json(&serde_json::json!(body)) - }) - .await - .map_err(|e| format!("Failed to send request to Next.js handler: {}", e))?; + let response = app + .authed_api_request(|client| { + client + .post(web_api::make_url("/api/upload/signed")) + .json(&serde_json::json!(body)) + }) + .await + .map_err(|e| format!("Failed to send request to Next.js handler: {}", e))?; if response.status() == StatusCode::UNAUTHORIZED { return Err("Failed to authenticate request; please log in again".into()); @@ -415,20 +417,17 @@ async fn presigned_s3_url( } async fn presigned_s3_url_image( + app: &AppHandle, body: S3ImageUploadBody, - auth: &AuthStore, ) -> Result<(String, Form), String> { - let response = web_api::do_authed_request(auth, |client| { - client - .post(web_api::make_url("/api/upload/signed")) - .json(&serde_json::json!(body)) - }) - .await - .map_err(|e| format!("Failed to send request to Next.js handler: {}", e))?; - - if response.status() == StatusCode::UNAUTHORIZED { - return Err("Failed to authenticate request; please log in again".into()); - } + let response = app + .authed_api_request(|client| { + client + .post(web_api::make_url("/api/upload/signed")) + .json(&serde_json::json!(body)) + }) + .await + .map_err(|e| format!("Failed to send request to Next.js handler: {}", e))?; let presigned_post_data = response .json::() @@ -456,20 +455,17 @@ async fn presigned_s3_url_image( } async fn presigned_s3_url_audio( + app: &AppHandle, body: S3AudioUploadBody, - auth: &AuthStore, ) -> Result<(String, Form), String> { - let response = web_api::do_authed_request(auth, |client| { - client - .post(web_api::make_url("/api/upload/signed")) - .json(&serde_json::json!(body)) - }) - .await - .map_err(|e| format!("Failed to send request to Next.js handler: {}", e))?; - - if response.status() == StatusCode::UNAUTHORIZED { - return Err("Failed to authenticate request; please log in again".into()); - } + let response = app + .authed_api_request(|client| { + client + .post(web_api::make_url("/api/upload/signed")) + .json(&serde_json::json!(body)) + }) + .await + .map_err(|e| format!("Failed to send request to Next.js handler: {}", e))?; let presigned_post_data = response .json::() @@ -559,7 +555,7 @@ fn build_audio_upload_body( } pub async fn upload_individual_file( - auth: &AuthStore, + app: &AppHandle, file_path: PathBuf, s3_config: S3UploadMeta, file_name: &str, @@ -581,10 +577,10 @@ pub async fn upload_individual_file( let (upload_url, mut form) = if is_audio { let audio_body = build_audio_upload_body(&file_path, base_upload_body)?; - presigned_s3_url_audio(audio_body, &auth).await? + presigned_s3_url_audio(app, audio_body).await? } else { let video_body = build_video_upload_body(&file_path, base_upload_body)?; - presigned_s3_url(video_body, &auth).await? + presigned_s3_url(app, video_body).await? }; let file_content = tokio::fs::read(&file_path) @@ -623,8 +619,8 @@ pub async fn upload_individual_file( } async fn prepare_screenshot_upload( + app: &AppHandle, s3_config: &S3UploadMeta, - auth: &AuthStore, screenshot_path: PathBuf, ) -> Result<(String, Form), String> { let file_name = screenshot_path @@ -646,7 +642,7 @@ async fn prepare_screenshot_upload( }, }; - let (upload_url, mut form) = presigned_s3_url_image(body, auth).await?; + let (upload_url, mut form) = presigned_s3_url_image(app, body).await?; let compressed_image = compress_image(screenshot_path).await?; diff --git a/apps/desktop/src-tauri/src/web_api.rs b/apps/desktop/src-tauri/src/web_api.rs index 766c1147..a0b4c5fa 100644 --- a/apps/desktop/src-tauri/src/web_api.rs +++ b/apps/desktop/src-tauri/src/web_api.rs @@ -1,4 +1,12 @@ -use crate::auth::AuthStore; +use reqwest::StatusCode; +use tauri::{Emitter, Manager, Runtime}; +use tauri_specta::Event; + +use crate::{ + auth::{AuthStore, AuthenticationInvalid}, + main_window::create_main_window, + open_main_window, +}; pub fn make_url(pathname: impl AsRef) -> String { let server_url_base = dotenvy_macro::dotenv!("NEXT_PUBLIC_URL"); @@ -12,7 +20,7 @@ pub async fn do_request( build(client).send().await } -pub async fn do_authed_request( +async fn do_authed_request( auth: &AuthStore, build: impl FnOnce(reqwest::Client) -> reqwest::RequestBuilder, ) -> Result { @@ -23,3 +31,39 @@ pub async fn do_authed_request( .send() .await } + +pub trait ManagerExt: Manager { + async fn authed_api_request( + &self, + build: impl FnOnce(reqwest::Client) -> reqwest::RequestBuilder, + ) -> Result; +} + +impl + Emitter, R: Runtime> ManagerExt for T { + async fn authed_api_request( + &self, + build: impl FnOnce(reqwest::Client) -> reqwest::RequestBuilder, + ) -> Result { + let Some(auth) = AuthStore::get(self.app_handle())? else { + println!("Not logged in"); + + AuthenticationInvalid.emit(self); + + return Err("Unauthorized".to_string()); + }; + + let response = do_authed_request(&auth, build) + .await + .map_err(|e| e.to_string())?; + + if response.status() == StatusCode::UNAUTHORIZED { + println!("Authentication expired. Please log in again."); + + AuthenticationInvalid.emit(self); + + return Err("Unauthorized".to_string()); + } + + Ok(response) + } +}