Skip to content

Commit

Permalink
wip: Thumbnail progress state
Browse files Browse the repository at this point in the history
  • Loading branch information
richiemcilroy committed Nov 15, 2024
1 parent 9281903 commit 8649b7b
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 133 deletions.
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ tauri-plugin-fs = "2.0.0-rc.0"
futures-intrusive = "0.5.0"
anyhow.workspace = true
mp4 = "0.14.0"
futures = "0.3.30"
futures = "0.3"
axum = { version = "0.7.5", features = ["ws"] }
tracing = "0.1.40"
indexmap = "2.5.0"
Expand Down
138 changes: 78 additions & 60 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1540,7 +1540,7 @@ fn show_previous_recordings_window(app: AppHandle) {
let state = app.state::<FakeWindowBounds>();

loop {
sleep(Duration::from_millis(1000 / 60)).await;
sleep(Duration::from_millis(1000 / 10)).await;

let map = state.0.read().await;
let Some(windows) = map.get("prev-recordings") else {
Expand All @@ -1549,7 +1549,7 @@ fn show_previous_recordings_window(app: AppHandle) {
};

let window_position = window.outer_position().unwrap();
let mouse_position = window.cursor_position().unwrap(); // TODO(Ilya): Panics on Windows
let mouse_position = window.cursor_position().unwrap();
let scale_factor = window.scale_factor().unwrap();

let mut ignore = true;
Expand All @@ -1572,6 +1572,16 @@ fn show_previous_recordings_window(app: AppHandle) {
}

window.set_ignore_cursor_events(ignore).ok();

if !ignore {
if !window.is_focused().unwrap_or(false) {
window.set_focus().ok();
}
} else {
if window.is_focused().unwrap_or(false) {
window.set_ignore_cursor_events(true).ok();
}
}
}
});
}
Expand Down Expand Up @@ -1729,6 +1739,13 @@ async fn open_settings_window(app: AppHandle, page: String) {
CapWindow::Settings { page: Some(page) }.show(&app);
}

#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)]
pub struct UploadProgress {
stage: String,
progress: f64,
message: String,
}

#[tauri::command]
#[specta::specta]
async fn upload_rendered_video(
Expand All @@ -1738,7 +1755,6 @@ async fn upload_rendered_video(
pre_created_video: Option<PreCreatedVideo>,
) -> Result<UploadResult, String> {
let Ok(Some(mut auth)) = AuthStore::get(&app) else {
// Sign out and redirect to sign in
AuthStore::set(&app, None).map_err(|e| e.to_string())?;
return Ok(UploadResult::NotAuthenticated);
};
Expand Down Expand Up @@ -1777,60 +1793,34 @@ async fn upload_rendered_video(
let editor_instance = upsert_editor_instance(&app, video_id.clone()).await;
let mut meta = editor_instance.meta();

let share_link = if let Some(sharing) = meta.sharing {
if let Some(sharing) = meta.sharing {
notifications::send_notification(
&app,
notifications::NotificationType::ShareableLinkCopied,
);
sharing.link
} else if let Some(pre_created) = pre_created_video {
// Use the pre-created video information
let output_path = match get_rendered_video_impl(editor_instance.clone(), project).await {
Ok(path) => path,
Err(e) => return Err(format!("Failed to get rendered video: {}", e)),
};

match upload_video(
&app,
video_id.clone(),
output_path,
false,
Some(pre_created.config),
)
.await
{
Ok(_) => {
meta.sharing = Some(SharingMeta {
link: pre_created.link.clone(),
id: pre_created.id.clone(),
});
meta.save_for_project();
RecordingMetaChanged { id: video_id }.emit(&app).ok();
Ok(UploadResult::Success(sharing.link))
} else {
// Emit initial rendering progress
UploadProgress {
stage: "rendering".to_string(),
progress: 0.0,
message: "Preparing video...".to_string(),
}
.emit(&app)
.ok();

// Don't send notification here if it was pre-created
let general_settings = GeneralSettingsStore::get(&app)?;
if !general_settings
.map(|settings| settings.auto_create_shareable_link)
.unwrap_or(false)
{
notifications::send_notification(
&app,
notifications::NotificationType::ShareableLinkCopied,
);
let output_path = match get_rendered_video_impl(editor_instance.clone(), project).await {
Ok(path) => {
// Emit rendering complete
UploadProgress {
stage: "rendering".to_string(),
progress: 1.0,
message: "Rendering complete".to_string(),
}
pre_created.link
}
Err(e) => {
notifications::send_notification(
&app,
notifications::NotificationType::UploadFailed,
);
return Err(e);
.emit(&app)
.ok();
path
}
}
} else {
let output_path = match get_rendered_video_impl(editor_instance.clone(), project).await {
Ok(path) => path,
Err(e) => {
notifications::send_notification(
&app,
Expand All @@ -1840,8 +1830,34 @@ async fn upload_rendered_video(
}
};

match upload_video(&app, video_id.clone(), output_path, false, None).await {
// Start upload progress
UploadProgress {
stage: "uploading".to_string(),
progress: 0.0,
message: "Starting upload...".to_string(),
}
.emit(&app)
.ok();

let result = match upload_video(
&app,
video_id.clone(),
output_path,
false,
pre_created_video.map(|v| v.config),
)
.await
{
Ok(uploaded_video) => {
// Emit upload complete
UploadProgress {
stage: "uploading".to_string(),
progress: 1.0,
message: "Upload complete!".to_string(),
}
.emit(&app)
.ok();

meta.sharing = Some(SharingMeta {
link: uploaded_video.link.clone(),
id: uploaded_video.id.clone(),
Expand All @@ -1853,22 +1869,23 @@ async fn upload_rendered_video(
&app,
notifications::NotificationType::ShareableLinkCopied,
);
uploaded_video.link

#[cfg(target_os = "macos")]
platform::write_string_to_pasteboard(&uploaded_video.link);

Ok(UploadResult::Success(uploaded_video.link))
}
Err(e) => {
notifications::send_notification(
&app,
notifications::NotificationType::UploadFailed,
);
return Err(e);
Err(e)
}
}
};

#[cfg(target_os = "macos")]
platform::write_string_to_pasteboard(&share_link);
};

Ok(UploadResult::Success(share_link))
result
}
}

#[tauri::command]
Expand Down Expand Up @@ -2468,7 +2485,8 @@ pub async fn run() {
RequestOpenSettings,
NewNotification,
AuthenticationInvalid,
audio_meter::AudioInputLevelChange
audio_meter::AudioInputLevelChange,
UploadProgress,
])
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
.typ::<ProjectConfiguration>()
Expand Down
62 changes: 57 additions & 5 deletions apps/desktop/src-tauri/src/upload.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// credit @filleduchaos

use futures::stream;
use image::codecs::jpeg::JpegEncoder;
use image::ImageReader;
use reqwest::{multipart::Form, StatusCode};
use std::path::PathBuf;
use tauri::AppHandle;
use tauri_specta::Event;
use tokio::task;

use crate::web_api::{self, ManagerExt};

use crate::UploadProgress;
use serde::{Deserialize, Serialize};
use specta::Type;

Expand Down Expand Up @@ -140,11 +143,51 @@ pub async fn upload_video(
let file_bytes = tokio::fs::read(&file_path)
.await
.map_err(|e| format!("Failed to read file: {}", e))?;
let file_part = reqwest::multipart::Part::bytes(file_bytes)
.file_name(file_name.clone())
.mime_str("video/mp4")
.map_err(|e| format!("Error setting MIME type: {}", e))?;
form = form.part("file", file_part);

let total_size = file_bytes.len() as f64;

// Wrap file_bytes in an Arc for shared ownership
let file_bytes = std::sync::Arc::new(file_bytes);

// Create a stream that reports progress
let file_part =
{
let progress_counter = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let app_handle = app.clone();
let file_bytes = file_bytes.clone();

let stream = stream::iter((0..file_bytes.len()).step_by(1024 * 1024).map(
move |start| {
let end = (start + 1024 * 1024).min(file_bytes.len());
let chunk = file_bytes[start..end].to_vec();

let current = progress_counter
.fetch_add(chunk.len() as u64, std::sync::atomic::Ordering::SeqCst)
as f64;

// Emit progress every chunk
UploadProgress {
stage: "uploading".to_string(),
progress: current / total_size,
message: format!("{:.0}%", (current / total_size * 100.0)),
}
.emit(&app_handle)
.ok();

Ok::<Vec<u8>, std::io::Error>(chunk)
},
));

reqwest::multipart::Part::stream_with_length(
reqwest::Body::wrap_stream(stream),
total_size as u64,
)
.file_name(file_name.clone())
.mime_str("video/mp4")
.map_err(|e| format!("Error setting MIME type: {}", e))?
};

let mut form = form.part("file", file_part);

// Prepare screenshot upload
let screenshot_path = file_path
Expand Down Expand Up @@ -182,6 +225,15 @@ pub async fn upload_video(
video_upload.map_err(|e| format!("Failed to send upload file request: {}", e))?;

if response.status().is_success() {
// Final progress update
UploadProgress {
stage: "uploading".to_string(),
progress: 1.0,
message: "100%".to_string(),
}
.emit(app)
.ok();

println!("Video uploaded successfully");

if let Some(Ok(screenshot_response)) = screenshot_result {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ impl CapWindow {
.shadow(false)
.always_on_top(true)
.visible_on_all_workspaces(true)
.accept_first_mouse(true)
.content_protected(true)
.inner_size(
350.0,
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/routes/editor/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ function ExportButton() {
<Dialog.Root
open={state.open}
onOpenChange={(o) => {
if (!o) setState(reconcile({ ...state, open: false }));
if (state.type !== "inProgress" && !o) {
setState(reconcile({ ...state, open: false }));
}
}}
>
<DialogContent
Expand Down
Loading

1 comment on commit 8649b7b

@vercel
Copy link

@vercel vercel bot commented on 8649b7b Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.