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
This commit is contained in:
uku 2025-05-11 23:21:28 +02:00
parent 601ee85b5f
commit 94562b995a
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
4 changed files with 174 additions and 36 deletions

View file

@ -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())

View file

@ -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<PathBuf>,
folder: ZiplineFolder,
folder: Option<ZiplineFolder>,
dialog: Controller<Dialog>,
}
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::<Vec<_>>();
gtk_folders.insert(0, GtkZiplineFolder::none());
let store = gio::ListStore::new::<GtkZiplineFolder>();
store.extend_from_slice(&gtk_folders);
@ -186,9 +195,18 @@ impl SimpleComponent for Tyrolienne {
fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender<Self>) {
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<Config> {
Ok(config)
}
fn the_process(app: &Tyrolienne) -> Result<()> {
tracing::info!("uploading to folder '{}'...", app.folder.name);
fn the_process(app: &Tyrolienne) -> Result<String> {
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(())
))
}

82
src/relm.rs Normal file
View file

@ -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<Self>,
) -> relm4::ComponentParts<Self> {
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<Self>) {
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<Self>) {
widgets.dialog.set_heading(Some(&self.heading));
widgets.dialog.set_body(&self.body);
if self.visible {
widgets.dialog.present(Some(&self.window));
}
}
}

View file

@ -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<Vec<ZiplineFolder>, reqwest::Error> {
pub fn get_folders(config: &Config) -> Result<Vec<ZiplineFolder>> {
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<ZiplineUploadResponse, reqwest::Error> {
) -> Result<ZiplineUploadResponse> {
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<ZiplineFileInfo, reqwest::Error> {
pub fn get_file_details(config: &Config, id: &str) -> Result<ZiplineFileInfo> {
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)
}
}