From 94562b995a90c08b0679f6a7f5a58c9107eb7f28 Mon Sep 17 00:00:00 2001 From: uku Date: Sun, 11 May 2025 23:21:28 +0200 Subject: [PATCH] feat: better error handling errors during the upload are now shown in a separate user-friendly alert dialog, and you can also choose to upload the file to no folder --- src/gobject.rs | 9 ++++++ src/main.rs | 53 +++++++++++++++++++++++--------- src/relm.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/zipline.rs | 66 +++++++++++++++++++++++++++------------- 4 files changed, 174 insertions(+), 36 deletions(-) create mode 100644 src/relm.rs diff --git a/src/gobject.rs b/src/gobject.rs index 458e595..8f63e93 100644 --- a/src/gobject.rs +++ b/src/gobject.rs @@ -15,10 +15,19 @@ glib::wrapper! { } impl GtkZiplineFolder { + pub const NONE_ID: &str = "tyroliennesupersecretnoneid"; + pub fn r#type() -> Type { GtkZiplineFolderImpl::type_() } + pub fn none() -> Self { + glib::Object::builder() + .property("id", String::from(Self::NONE_ID)) + .property("name", String::from("None")) + .build() + } + pub fn from_folder(folder: &ZiplineFolder) -> Self { glib::Object::builder() .property("id", folder.id.clone()) diff --git a/src/main.rs b/src/main.rs index 7bfeabe..4f51691 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ mod gobject; +mod relm; mod zipline; use std::{borrow::Cow, path::PathBuf, time::Duration}; use color_eyre::eyre::{OptionExt, Result, bail}; use gobject::GtkZiplineFolder; +use relm::{Dialog, DialogInput}; use relm4::{ - ComponentParts, RelmApp, SimpleComponent, + Component, ComponentController, ComponentParts, Controller, RelmApp, SimpleComponent, adw::{self, prelude::*}, gtk::{self, gio, glib::clone}, }; @@ -40,6 +42,7 @@ enum Message { SetPath(PathBuf), SetFolder(ZiplineFolder), StartTheProcess, + Nothing, } struct Widgets { @@ -50,7 +53,8 @@ struct Widgets { struct Tyrolienne { config: Config, video_path: Option, - folder: ZiplineFolder, + folder: Option, + dialog: Controller, } impl Tyrolienne { @@ -84,7 +88,10 @@ impl SimpleComponent for Tyrolienne { let model = Tyrolienne { config, video_path: None, - folder: folders[0].clone(), + folder: None, + dialog: Dialog::builder() + .launch(root.clone()) + .forward(sender.input_sender(), |_| Message::Nothing), }; let file_picker_row = adw::ActionRow::builder() @@ -116,10 +123,12 @@ impl SimpleComponent for Tyrolienne { } )); - let gtk_folders = folders + let mut gtk_folders = folders .iter() .map(GtkZiplineFolder::from_folder) .collect::>(); + gtk_folders.insert(0, GtkZiplineFolder::none()); + let store = gio::ListStore::new::(); store.extend_from_slice(>k_folders); @@ -186,9 +195,18 @@ impl SimpleComponent for Tyrolienne { fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender) { match message { + Message::Nothing => {} Message::SetPath(path) => self.video_path = Some(path), - Message::SetFolder(folder) => self.folder = folder, - Message::StartTheProcess => the_process(self).unwrap(), + Message::SetFolder(folder) => { + self.folder = (folder.id != GtkZiplineFolder::NONE_ID).then_some(folder) + } + Message::StartTheProcess => match the_process(self) { + Ok(url) => tracing::info!("{url}"), + Err(e) => self.dialog.emit(DialogInput::Show { + heading: "An error occurred".into(), + body: e.to_string(), + }), + }, } } @@ -247,17 +265,25 @@ fn get_config() -> Result { Ok(config) } -fn the_process(app: &Tyrolienne) -> Result<()> { - tracing::info!("uploading to folder '{}'...", app.folder.name); +fn the_process(app: &Tyrolienne) -> Result { + if let Some(folder) = app.folder.as_ref() { + tracing::info!("uploading to folder '{}'...", folder.name); + } else { + tracing::info!("uploading video..."); + } - let res = zipline::upload_file(&app.config, &app.folder, app.video_path.as_ref().unwrap())?; + let res = zipline::upload_file( + &app.config, + app.folder.as_ref(), + app.video_path.as_ref().unwrap(), + )?; let zp_file = &res.files[0]; tracing::info!("recalculating thumbnails..."); zipline::recalc_thumbnails(&app.config)?; - std::thread::sleep(Duration::from_secs(5)); + std::thread::sleep(Duration::from_secs(2)); tracing::info!("fetching thumbnail url..."); @@ -267,12 +293,9 @@ fn the_process(app: &Tyrolienne) -> Result<()> { .ok_or_eyre("could not get thumbnail url")?; // TODO get w&h from video - let autocomp_url = format!( + Ok(format!( "https://autocompressor.net/av1?v={}&i={}&w=1920&h=1080", Encoded(&zp_file.url), Encoded(&thumbnail_url) - ); - tracing::info!("url: {autocomp_url}"); - - Ok(()) + )) } diff --git a/src/relm.rs b/src/relm.rs new file mode 100644 index 0000000..89e5bfa --- /dev/null +++ b/src/relm.rs @@ -0,0 +1,82 @@ +use relm4::{ + SimpleComponent, + adw::{self, prelude::*}, + gtk::glib::clone, +}; + +pub struct Dialog { + window: adw::ApplicationWindow, + visible: bool, + heading: String, + body: String, +} + +#[derive(Debug)] +pub enum DialogInput { + Show { heading: String, body: String }, + Dismiss, +} + +pub struct DialogWidgets { + dialog: adw::AlertDialog, +} + +impl SimpleComponent for Dialog { + type Init = adw::ApplicationWindow; + type Root = adw::AlertDialog; + type Widgets = DialogWidgets; + type Input = DialogInput; + type Output = (); + + fn init_root() -> Self::Root { + let dialog = adw::AlertDialog::builder().close_response("ok").build(); + dialog.add_response("ok", "OK"); + dialog + } + + fn init( + window: Self::Init, + root: Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = Self { + window, + visible: false, + heading: String::new(), + body: String::new(), + }; + + root.connect_response( + None, + clone!( + #[strong] + sender, + move |_, _| sender.input(DialogInput::Dismiss) + ), + ); + + let widgets = DialogWidgets { dialog: root }; + + relm4::ComponentParts { model, widgets } + } + + fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender) { + match message { + DialogInput::Show { heading, body } => { + self.heading = heading; + self.body = body; + self.visible = true; + } + DialogInput::Dismiss => self.visible = false, + } + } + + fn update_view(&self, widgets: &mut Self::Widgets, _sender: relm4::ComponentSender) { + widgets.dialog.set_heading(Some(&self.heading)); + widgets.dialog.set_body(&self.body); + + if self.visible { + widgets.dialog.present(Some(&self.window)); + } + } +} diff --git a/src/zipline.rs b/src/zipline.rs index 1db512e..601d216 100644 --- a/src/zipline.rs +++ b/src/zipline.rs @@ -1,6 +1,8 @@ use std::{path::Path, sync::LazyLock}; +use color_eyre::eyre::{Result, bail}; use reqwest::{ + StatusCode, blocking::{Client, multipart::Form}, header::AUTHORIZATION, }; @@ -44,54 +46,76 @@ impl ZiplineFileInfo { } } -pub fn get_folders(config: &Config) -> Result, reqwest::Error> { +pub fn get_folders(config: &Config) -> Result> { let url = format!("{}api/user/folders?noincl=true", config.fixed_url()); - CLIENT + let res = CLIENT .get(url) .header(AUTHORIZATION, &config.zipline_token) - .send()? - .json() + .send()?; + + if res.status() != StatusCode::OK { + bail!("an error occurred ({}): {}", res.status(), res.text()?); + } else { + res.json().map_err(Into::into) + } } pub fn upload_file( config: &Config, - folder: &ZiplineFolder, + folder: Option<&ZiplineFolder>, file_path: &Path, -) -> Result { +) -> Result { let url = format!("{}api/upload", config.fixed_url()); - let form = Form::new().file("file", file_path).unwrap(); // FIXME + let form = Form::new().file("file", file_path)?; - CLIENT + let mut req = CLIENT .post(url) .header(AUTHORIZATION, &config.zipline_token) - .header("x-zipline-folder", &folder.id) .header("x-zipline-format", "name") - .multipart(form) - .send()? - .json() + .multipart(form); + + if let Some(folder) = folder { + req = req.header("x-zipline-folder", &folder.id); + } + + let res = req.send()?; + + if res.status() != StatusCode::OK { + bail!("an error occurred ({}): {}", res.status(), res.text()?); + } else { + res.json().map_err(Into::into) + } } -pub fn recalc_thumbnails(config: &Config) -> Result<(), reqwest::Error> { +pub fn recalc_thumbnails(config: &Config) -> Result<()> { let url = format!("{}api/server/thumbnails", config.fixed_url()); - CLIENT + let res = CLIENT .post(url) .header(AUTHORIZATION, &config.zipline_token) .json(&[("rerun", false)]) - .send()? - .error_for_status()?; + .send()?; - Ok(()) + if res.status() != StatusCode::OK { + bail!("an error occurred ({}): {}", res.status(), res.text()?); + } else { + Ok(()) + } } -pub fn get_file_details(config: &Config, id: &str) -> Result { +pub fn get_file_details(config: &Config, id: &str) -> Result { let url = format!("{}api/user/files/{id}", config.fixed_url()); - CLIENT + let res = CLIENT .get(url) .header(AUTHORIZATION, &config.zipline_token) - .send()? - .json() + .send()?; + + if res.status() != StatusCode::OK { + bail!("an error occurred ({}): {}", res.status(), res.text()?); + } else { + res.json().map_err(Into::into) + } }