Skip to content

Commit

Permalink
Merge branch 'CapSoftware:main' into next-windows
Browse files Browse the repository at this point in the history
  • Loading branch information
ItsEeleeya authored Nov 28, 2024
2 parents f3213ab + a069ca8 commit dadaf6f
Show file tree
Hide file tree
Showing 39 changed files with 1,049 additions and 105 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
108 changes: 86 additions & 22 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecordingMeta, String> {
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<Vec<(String, PathBuf, RecordingMeta)>, 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::<Vec<_>>();

// 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<Vec<(String, PathBuf, RecordingMeta)>, String> {
Expand All @@ -1410,7 +1443,11 @@ fn list_screenshots(app: AppHandle) -> Result<Vec<(String, PathBuf, RecordingMet
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(), "screenshot".to_string());
let meta =
match get_recording_meta(app.clone(), id.clone(), "screenshot".to_string()) {
Ok(meta) => 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)
Expand Down Expand Up @@ -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());
}
}
_ => {}
});
}
Expand Down Expand Up @@ -2004,15 +2064,19 @@ async fn create_editor_instance_impl(app: &AppHandle, video_id: String) -> Arc<E

// use EditorInstance.project_path instead of this
fn recordings_path(app: &AppHandle) -> 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 {
recordings_path(app).join(format!("{}.cap", recording_id))
}

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 {
Expand Down
115 changes: 100 additions & 15 deletions apps/desktop/src/routes/(window-chrome)/settings/apps/s3-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -294,36 +355,60 @@ export default function S3ConfigPage() {
</div>

<div class="flex-shrink-0 p-4 border-t">
<div
class={`flex ${
hasConfig() ? "justify-between" : "justify-end"
} items-center`}
>
{hasConfig() && (
<div class="flex justify-between items-center">
<div class="flex gap-2">
{hasConfig() && (
<Button
variant="destructive"
onClick={handleDelete}
disabled={deleting() || loading() || saving() || testing()}
class={
deleting() || loading() || saving() || testing()
? "opacity-50 cursor-not-allowed"
: ""
}
>
{deleting() ? "Removing..." : "Remove Config"}
</Button>
)}
<Button
variant="destructive"
onClick={handleDelete}
disabled={deleting() || loading() || saving()}
variant="secondary"
onClick={handleTest}
disabled={
saving() ||
loading() ||
deleting() ||
testing() ||
!accessKeyId() ||
!secretAccessKey() ||
!bucketName()
}
class={
deleting() || loading() || saving()
saving() ||
loading() ||
deleting() ||
testing() ||
!accessKeyId() ||
!secretAccessKey() ||
!bucketName()
? "opacity-50 cursor-not-allowed"
: ""
}
>
{deleting() ? "Removing..." : "Remove Config"}
{testing() ? "Testing..." : "Test Connection"}
</Button>
)}
</div>
<Button
variant="primary"
onClick={handleSave}
disabled={saving() || loading() || deleting()}
disabled={saving() || loading() || deleting() || testing()}
class={
saving() || loading() || deleting()
saving() || loading() || deleting() || testing()
? "opacity-50 cursor-not-allowed"
: ""
}
>
{saving() ? "Exporting..." : "Export"}
{saving() ? "Saving..." : "Save"}
</Button>
</div>
</div>
Expand Down
11 changes: 5 additions & 6 deletions apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,10 +18,7 @@ export default function Recordings() {
queryFn: async () => {
const result = await commands
.listRecordings()
.catch(
() =>
Promise.resolve([]) as ReturnType<typeof commands.listRecordings>
);
.catch(() => [] as [string, string, RecordingMeta][]);

const recordings = await Promise.all(
result.map(async (file) => {
Expand Down Expand Up @@ -62,7 +59,9 @@ export default function Recordings() {
<Show
when={fetchRecordings.data && fetchRecordings.data.length > 0}
fallback={
<p class="text-center text-[--text-tertiary]">No recordings found</p>
<p class="text-center text-[--text-tertiary]">
No recordings found
</p>
}
>
<For each={fetchRecordings.data}>
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/routes/(window-chrome)/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ function Startup(props: { onClose: () => void }) {
Welcome to Cap
</h1>
<p class="text-2xl text-gray-50 opacity-80 max-w-md mx-auto drop-shadow-[0_0_20px_rgba(0,0,0,0.2)]">
Beautiful, shareable screen recordings.
Beautiful screen recordings, owned by you.
</p>
</div>

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/routes/(window-chrome)/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const signInAction = action(async () => {

getCurrentWindow()
.setFocus()
.catch(() => { });
.catch(() => {});

return redirect("/");
} catch (error) {
Expand Down Expand Up @@ -102,7 +102,7 @@ export default function Page() {
<div class="space-y-[0.375rem] flex-1">
<IconCapLogo class="size-[3rem]" />
<h1 class="text-[1rem] font-[700]">Sign in to Cap</h1>
<p class="text-gray-400">Beautiful, shareable screen recordings.</p>
<p class="text-gray-400">Beautiful screen recordings, owned by you.</p>
</div>
{submission.pending ? (
<Button variant="secondary" onClick={() => submission.clear()}>
Expand Down
Loading

0 comments on commit dadaf6f

Please sign in to comment.