Skip to content

Commit 8649b7b

Browse files
committed
wip: Thumbnail progress state
1 parent 9281903 commit 8649b7b

File tree

7 files changed

+423
-133
lines changed

7 files changed

+423
-133
lines changed

Diff for: apps/desktop/src-tauri/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ tauri-plugin-fs = "2.0.0-rc.0"
5353
futures-intrusive = "0.5.0"
5454
anyhow.workspace = true
5555
mp4 = "0.14.0"
56-
futures = "0.3.30"
56+
futures = "0.3"
5757
axum = { version = "0.7.5", features = ["ws"] }
5858
tracing = "0.1.40"
5959
indexmap = "2.5.0"

Diff for: apps/desktop/src-tauri/src/lib.rs

+78-60
Original file line numberDiff line numberDiff line change
@@ -1540,7 +1540,7 @@ fn show_previous_recordings_window(app: AppHandle) {
15401540
let state = app.state::<FakeWindowBounds>();
15411541

15421542
loop {
1543-
sleep(Duration::from_millis(1000 / 60)).await;
1543+
sleep(Duration::from_millis(1000 / 10)).await;
15441544

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

15511551
let window_position = window.outer_position().unwrap();
1552-
let mouse_position = window.cursor_position().unwrap(); // TODO(Ilya): Panics on Windows
1552+
let mouse_position = window.cursor_position().unwrap();
15531553
let scale_factor = window.scale_factor().unwrap();
15541554

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

15741574
window.set_ignore_cursor_events(ignore).ok();
1575+
1576+
if !ignore {
1577+
if !window.is_focused().unwrap_or(false) {
1578+
window.set_focus().ok();
1579+
}
1580+
} else {
1581+
if window.is_focused().unwrap_or(false) {
1582+
window.set_ignore_cursor_events(true).ok();
1583+
}
1584+
}
15751585
}
15761586
});
15771587
}
@@ -1729,6 +1739,13 @@ async fn open_settings_window(app: AppHandle, page: String) {
17291739
CapWindow::Settings { page: Some(page) }.show(&app);
17301740
}
17311741

1742+
#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)]
1743+
pub struct UploadProgress {
1744+
stage: String,
1745+
progress: f64,
1746+
message: String,
1747+
}
1748+
17321749
#[tauri::command]
17331750
#[specta::specta]
17341751
async fn upload_rendered_video(
@@ -1738,7 +1755,6 @@ async fn upload_rendered_video(
17381755
pre_created_video: Option<PreCreatedVideo>,
17391756
) -> Result<UploadResult, String> {
17401757
let Ok(Some(mut auth)) = AuthStore::get(&app) else {
1741-
// Sign out and redirect to sign in
17421758
AuthStore::set(&app, None).map_err(|e| e.to_string())?;
17431759
return Ok(UploadResult::NotAuthenticated);
17441760
};
@@ -1777,60 +1793,34 @@ async fn upload_rendered_video(
17771793
let editor_instance = upsert_editor_instance(&app, video_id.clone()).await;
17781794
let mut meta = editor_instance.meta();
17791795

1780-
let share_link = if let Some(sharing) = meta.sharing {
1796+
if let Some(sharing) = meta.sharing {
17811797
notifications::send_notification(
17821798
&app,
17831799
notifications::NotificationType::ShareableLinkCopied,
17841800
);
1785-
sharing.link
1786-
} else if let Some(pre_created) = pre_created_video {
1787-
// Use the pre-created video information
1788-
let output_path = match get_rendered_video_impl(editor_instance.clone(), project).await {
1789-
Ok(path) => path,
1790-
Err(e) => return Err(format!("Failed to get rendered video: {}", e)),
1791-
};
1792-
1793-
match upload_video(
1794-
&app,
1795-
video_id.clone(),
1796-
output_path,
1797-
false,
1798-
Some(pre_created.config),
1799-
)
1800-
.await
1801-
{
1802-
Ok(_) => {
1803-
meta.sharing = Some(SharingMeta {
1804-
link: pre_created.link.clone(),
1805-
id: pre_created.id.clone(),
1806-
});
1807-
meta.save_for_project();
1808-
RecordingMetaChanged { id: video_id }.emit(&app).ok();
1801+
Ok(UploadResult::Success(sharing.link))
1802+
} else {
1803+
// Emit initial rendering progress
1804+
UploadProgress {
1805+
stage: "rendering".to_string(),
1806+
progress: 0.0,
1807+
message: "Preparing video...".to_string(),
1808+
}
1809+
.emit(&app)
1810+
.ok();
18091811

1810-
// Don't send notification here if it was pre-created
1811-
let general_settings = GeneralSettingsStore::get(&app)?;
1812-
if !general_settings
1813-
.map(|settings| settings.auto_create_shareable_link)
1814-
.unwrap_or(false)
1815-
{
1816-
notifications::send_notification(
1817-
&app,
1818-
notifications::NotificationType::ShareableLinkCopied,
1819-
);
1812+
let output_path = match get_rendered_video_impl(editor_instance.clone(), project).await {
1813+
Ok(path) => {
1814+
// Emit rendering complete
1815+
UploadProgress {
1816+
stage: "rendering".to_string(),
1817+
progress: 1.0,
1818+
message: "Rendering complete".to_string(),
18201819
}
1821-
pre_created.link
1822-
}
1823-
Err(e) => {
1824-
notifications::send_notification(
1825-
&app,
1826-
notifications::NotificationType::UploadFailed,
1827-
);
1828-
return Err(e);
1820+
.emit(&app)
1821+
.ok();
1822+
path
18291823
}
1830-
}
1831-
} else {
1832-
let output_path = match get_rendered_video_impl(editor_instance.clone(), project).await {
1833-
Ok(path) => path,
18341824
Err(e) => {
18351825
notifications::send_notification(
18361826
&app,
@@ -1840,8 +1830,34 @@ async fn upload_rendered_video(
18401830
}
18411831
};
18421832

1843-
match upload_video(&app, video_id.clone(), output_path, false, None).await {
1833+
// Start upload progress
1834+
UploadProgress {
1835+
stage: "uploading".to_string(),
1836+
progress: 0.0,
1837+
message: "Starting upload...".to_string(),
1838+
}
1839+
.emit(&app)
1840+
.ok();
1841+
1842+
let result = match upload_video(
1843+
&app,
1844+
video_id.clone(),
1845+
output_path,
1846+
false,
1847+
pre_created_video.map(|v| v.config),
1848+
)
1849+
.await
1850+
{
18441851
Ok(uploaded_video) => {
1852+
// Emit upload complete
1853+
UploadProgress {
1854+
stage: "uploading".to_string(),
1855+
progress: 1.0,
1856+
message: "Upload complete!".to_string(),
1857+
}
1858+
.emit(&app)
1859+
.ok();
1860+
18451861
meta.sharing = Some(SharingMeta {
18461862
link: uploaded_video.link.clone(),
18471863
id: uploaded_video.id.clone(),
@@ -1853,22 +1869,23 @@ async fn upload_rendered_video(
18531869
&app,
18541870
notifications::NotificationType::ShareableLinkCopied,
18551871
);
1856-
uploaded_video.link
1872+
1873+
#[cfg(target_os = "macos")]
1874+
platform::write_string_to_pasteboard(&uploaded_video.link);
1875+
1876+
Ok(UploadResult::Success(uploaded_video.link))
18571877
}
18581878
Err(e) => {
18591879
notifications::send_notification(
18601880
&app,
18611881
notifications::NotificationType::UploadFailed,
18621882
);
1863-
return Err(e);
1883+
Err(e)
18641884
}
1865-
}
1866-
};
1867-
1868-
#[cfg(target_os = "macos")]
1869-
platform::write_string_to_pasteboard(&share_link);
1885+
};
18701886

1871-
Ok(UploadResult::Success(share_link))
1887+
result
1888+
}
18721889
}
18731890

18741891
#[tauri::command]
@@ -2468,7 +2485,8 @@ pub async fn run() {
24682485
RequestOpenSettings,
24692486
NewNotification,
24702487
AuthenticationInvalid,
2471-
audio_meter::AudioInputLevelChange
2488+
audio_meter::AudioInputLevelChange,
2489+
UploadProgress,
24722490
])
24732491
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
24742492
.typ::<ProjectConfiguration>()

Diff for: apps/desktop/src-tauri/src/upload.rs

+57-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
// credit @filleduchaos
22

3+
use futures::stream;
34
use image::codecs::jpeg::JpegEncoder;
45
use image::ImageReader;
56
use reqwest::{multipart::Form, StatusCode};
67
use std::path::PathBuf;
78
use tauri::AppHandle;
9+
use tauri_specta::Event;
810
use tokio::task;
911

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

14+
use crate::UploadProgress;
1215
use serde::{Deserialize, Serialize};
1316
use specta::Type;
1417

@@ -140,11 +143,51 @@ pub async fn upload_video(
140143
let file_bytes = tokio::fs::read(&file_path)
141144
.await
142145
.map_err(|e| format!("Failed to read file: {}", e))?;
143-
let file_part = reqwest::multipart::Part::bytes(file_bytes)
144-
.file_name(file_name.clone())
145-
.mime_str("video/mp4")
146-
.map_err(|e| format!("Error setting MIME type: {}", e))?;
147-
form = form.part("file", file_part);
146+
147+
let total_size = file_bytes.len() as f64;
148+
149+
// Wrap file_bytes in an Arc for shared ownership
150+
let file_bytes = std::sync::Arc::new(file_bytes);
151+
152+
// Create a stream that reports progress
153+
let file_part =
154+
{
155+
let progress_counter = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
156+
let app_handle = app.clone();
157+
let file_bytes = file_bytes.clone();
158+
159+
let stream = stream::iter((0..file_bytes.len()).step_by(1024 * 1024).map(
160+
move |start| {
161+
let end = (start + 1024 * 1024).min(file_bytes.len());
162+
let chunk = file_bytes[start..end].to_vec();
163+
164+
let current = progress_counter
165+
.fetch_add(chunk.len() as u64, std::sync::atomic::Ordering::SeqCst)
166+
as f64;
167+
168+
// Emit progress every chunk
169+
UploadProgress {
170+
stage: "uploading".to_string(),
171+
progress: current / total_size,
172+
message: format!("{:.0}%", (current / total_size * 100.0)),
173+
}
174+
.emit(&app_handle)
175+
.ok();
176+
177+
Ok::<Vec<u8>, std::io::Error>(chunk)
178+
},
179+
));
180+
181+
reqwest::multipart::Part::stream_with_length(
182+
reqwest::Body::wrap_stream(stream),
183+
total_size as u64,
184+
)
185+
.file_name(file_name.clone())
186+
.mime_str("video/mp4")
187+
.map_err(|e| format!("Error setting MIME type: {}", e))?
188+
};
189+
190+
let mut form = form.part("file", file_part);
148191

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

184227
if response.status().is_success() {
228+
// Final progress update
229+
UploadProgress {
230+
stage: "uploading".to_string(),
231+
progress: 1.0,
232+
message: "100%".to_string(),
233+
}
234+
.emit(app)
235+
.ok();
236+
185237
println!("Video uploaded successfully");
186238

187239
if let Some(Ok(screenshot_response)) = screenshot_result {

Diff for: apps/desktop/src-tauri/src/windows.rs

+1
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ impl CapWindow {
275275
.shadow(false)
276276
.always_on_top(true)
277277
.visible_on_all_workspaces(true)
278+
.accept_first_mouse(true)
278279
.content_protected(true)
279280
.inner_size(
280281
350.0,

Diff for: apps/desktop/src/routes/editor/Header.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ function ExportButton() {
115115
<Dialog.Root
116116
open={state.open}
117117
onOpenChange={(o) => {
118-
if (!o) setState(reconcile({ ...state, open: false }));
118+
if (state.type !== "inProgress" && !o) {
119+
setState(reconcile({ ...state, open: false }));
120+
}
119121
}}
120122
>
121123
<DialogContent

0 commit comments

Comments
 (0)