diff --git a/Cargo.lock b/Cargo.lock index b6bfca7c9..af457a3c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1740,7 +1740,7 @@ dependencies = [ [[package]] name = "desktop" -version = "0.3.0-beta.10" +version = "0.3.1-alpha.25" dependencies = [ "anyhow", "arboard", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 8f1238752..586efc31d 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "desktop" -version = "0.3.0-beta.10" -description = "Beautiful, shareable screen recordings." +version = "0.3.1-alpha.25" +description = "Beautiful screen recordings, owned by you." authors = ["you"] edition = "2021" diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 0229f3204..f1c3041ec 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1355,49 +1355,82 @@ struct RecordingMetaChanged { #[tauri::command(async)] #[specta::specta] -fn get_recording_meta(app: AppHandle, id: String, file_type: String) -> RecordingMeta { +fn get_recording_meta( + app: AppHandle, + id: String, + file_type: String, +) -> Result { let meta_path = match file_type.as_str() { "recording" => recording_path(&app, &id), "screenshot" => screenshot_path(&app, &id), - _ => panic!("Invalid file type: {}", file_type), + _ => return Err("Invalid file type".to_string()), }; - RecordingMeta::load_for_project(&meta_path).unwrap() + RecordingMeta::load_for_project(&meta_path) + .map_err(|e| format!("Failed to load recording meta: {}", e)) } + #[tauri::command] #[specta::specta] fn list_recordings(app: AppHandle) -> Result, String> { let recordings_dir = recordings_path(&app); + // First check if directory exists + if !recordings_dir.exists() { + return Ok(Vec::new()); + } + let mut result = std::fs::read_dir(&recordings_dir) .map_err(|e| format!("Failed to read recordings directory: {}", e))? .filter_map(|entry| { - let entry = entry.ok()?; + let entry = match entry { + Ok(e) => e, + Err(_) => return None, + }; + let path = entry.path(); - if path.is_dir() && path.extension().and_then(|s| s.to_str()) == Some("cap") { - let id = path.file_stem()?.to_str()?.to_string(); - let meta = get_recording_meta(app.clone(), id.clone(), "recording".to_string()); - Some((id, path.clone(), meta)) - } else { - None + + // Multiple validation checks + if !path.is_dir() { + return None; + } + + let extension = match path.extension().and_then(|s| s.to_str()) { + Some("cap") => "cap", + _ => return None, + }; + + let id = match path.file_stem().and_then(|s| s.to_str()) { + Some(stem) => stem.to_string(), + None => return None, + }; + + // Try to get recording meta, skip if it fails + match get_recording_meta(app.clone(), id.clone(), "recording".to_string()) { + Ok(meta) => Some((id, path.clone(), meta)), + Err(_) => None, } }) .collect::>(); // Sort the result by creation date of the actual file, newest first result.sort_by(|a, b| { - b.1.metadata() - .and_then(|m| m.created()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH) - .cmp( - &a.1.metadata() - .and_then(|m| m.created()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH), - ) + let b_time = + b.1.metadata() + .and_then(|m| m.created()) + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + + let a_time = + a.1.metadata() + .and_then(|m| m.created()) + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + + b_time.cmp(&a_time) }); Ok(result) } + #[tauri::command] #[specta::specta] fn list_screenshots(app: AppHandle) -> Result, String> { @@ -1410,7 +1443,11 @@ fn list_screenshots(app: AppHandle) -> Result meta, + Err(_) => return None, // Skip this entry if metadata can't be loaded + }; // Find the nearest .png file inside the .cap folder let png_path = std::fs::read_dir(&path) @@ -1934,7 +1971,30 @@ pub async fn run() { .expect("error while running tauri application") .run(|handle, event| match event { #[cfg(target_os = "macos")] - tauri::RunEvent::Reopen { .. } => open_main_window(handle.clone()), + tauri::RunEvent::Reopen { .. } => { + // Check if any editor or settings window is open + let has_editor_or_settings = handle + .webview_windows() + .iter() + .any(|(label, _)| label.starts_with("editor-") || label.as_str() == "settings"); + + if has_editor_or_settings { + // Find and focus the editor or settings window + if let Some(window) = handle + .webview_windows() + .iter() + .find(|(label, _)| { + label.starts_with("editor-") || label.as_str() == "settings" + }) + .map(|(_, window)| window.clone()) + { + window.set_focus().ok(); + } + } else { + // No editor or settings window open, show main window + open_main_window(handle.clone()); + } + } _ => {} }); } @@ -2004,7 +2064,9 @@ async fn create_editor_instance_impl(app: &AppHandle, video_id: String) -> Arc PathBuf { - app.path().app_data_dir().unwrap().join("recordings") + let path = app.path().app_data_dir().unwrap().join("recordings"); + std::fs::create_dir_all(&path).unwrap_or_default(); + path } fn recording_path(app: &AppHandle, recording_id: &str) -> PathBuf { @@ -2012,7 +2074,9 @@ fn recording_path(app: &AppHandle, recording_id: &str) -> PathBuf { } fn screenshots_path(app: &AppHandle) -> PathBuf { - app.path().app_data_dir().unwrap().join("screenshots") + let path = app.path().app_data_dir().unwrap().join("screenshots"); + std::fs::create_dir_all(&path).unwrap_or_default(); + path } fn screenshot_path(app: &AppHandle, screenshot_id: &str) -> PathBuf { diff --git a/apps/desktop/src/routes/(window-chrome)/settings/apps/s3-config.tsx b/apps/desktop/src/routes/(window-chrome)/settings/apps/s3-config.tsx index ac0dbe2a6..76d78920c 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/apps/s3-config.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/apps/s3-config.tsx @@ -38,6 +38,7 @@ export default function S3ConfigPage() { const [loading, setLoading] = createSignal(true); const [deleting, setDeleting] = createSignal(false); const [hasConfig, setHasConfig] = createSignal(false); + const [testing, setTesting] = createSignal(false); const resetForm = () => { setProvider(DEFAULT_CONFIG.provider); @@ -182,6 +183,66 @@ export default function S3ConfigPage() { } }; + const handleTest = async () => { + setTesting(true); + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5500); // 5.5s timeout (slightly longer than backend) + + const response = await makeAuthenticatedRequest( + "/api/desktop/s3/config/test", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + provider: provider(), + accessKeyId: accessKeyId(), + secretAccessKey: secretAccessKey(), + endpoint: endpoint(), + bucketName: bucketName(), + region: region(), + }), + signal: controller.signal, + } + ); + + clearTimeout(timeoutId); + + if (response) { + await commands.globalMessageDialog( + "S3 configuration test successful! Connection is working." + ); + } + } catch (error) { + console.error("Failed to test S3 config:", error); + let errorMessage = + "Failed to connect to S3. Please check your settings and try again."; + + if (error instanceof Error) { + if (error.name === "AbortError") { + errorMessage = + "Connection test timed out after 5 seconds. Please check your endpoint URL and network connection."; + } else if ("response" in error) { + try { + const errorResponse = (error as { response: Response }).response; + const errorData = await errorResponse.json(); + if (errorData?.error) { + errorMessage = errorData.error; + } + } catch (e) { + // If we can't parse the error response, use the default message + } + } + } + + await commands.globalMessageDialog(errorMessage); + } finally { + setTesting(false); + } + }; + const renderInput = ( label: string, value: () => string, @@ -294,36 +355,60 @@ export default function S3ConfigPage() {
-
- {hasConfig() && ( +
+
+ {hasConfig() && ( + + )} - )} +
diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index f61416f95..7441fb176 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -2,7 +2,7 @@ import { createQuery } from "@tanstack/solid-query"; import { For, Show, Suspense, createSignal } from "solid-js"; import { convertFileSrc } from "@tauri-apps/api/core"; -import { commands, events } from "~/utils/tauri"; +import { commands, events, type RecordingMeta } from "~/utils/tauri"; type MediaEntry = { id: string; @@ -18,10 +18,7 @@ export default function Recordings() { queryFn: async () => { const result = await commands .listRecordings() - .catch( - () => - Promise.resolve([]) as ReturnType - ); + .catch(() => [] as [string, string, RecordingMeta][]); const recordings = await Promise.all( result.map(async (file) => { @@ -62,7 +59,9 @@ export default function Recordings() { 0} fallback={ -

No recordings found

+

+ No recordings found +

} > diff --git a/apps/desktop/src/routes/(window-chrome)/setup.tsx b/apps/desktop/src/routes/(window-chrome)/setup.tsx index e6ecb6cab..6f3405d8f 100644 --- a/apps/desktop/src/routes/(window-chrome)/setup.tsx +++ b/apps/desktop/src/routes/(window-chrome)/setup.tsx @@ -459,7 +459,7 @@ function Startup(props: { onClose: () => void }) { Welcome to Cap

- Beautiful, shareable screen recordings. + Beautiful screen recordings, owned by you.

diff --git a/apps/desktop/src/routes/(window-chrome)/signin.tsx b/apps/desktop/src/routes/(window-chrome)/signin.tsx index c73eaaf6c..6b805e285 100644 --- a/apps/desktop/src/routes/(window-chrome)/signin.tsx +++ b/apps/desktop/src/routes/(window-chrome)/signin.tsx @@ -61,7 +61,7 @@ const signInAction = action(async () => { getCurrentWindow() .setFocus() - .catch(() => { }); + .catch(() => {}); return redirect("/"); } catch (error) { @@ -102,7 +102,7 @@ export default function Page() {

Sign in to Cap

-

Beautiful, shareable screen recordings.

+

Beautiful screen recordings, owned by you.

{submission.pending ? (