feat: show toast after upload is finished

This commit is contained in:
uku 2025-05-13 00:01:05 +02:00
parent 3b75134572
commit 1d93482209
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
4 changed files with 336 additions and 59 deletions

View file

@ -6,7 +6,7 @@ use std::{borrow::Cow, path::PathBuf, time::Duration};
use color_eyre::eyre::{OptionExt, Result, bail};
use gobject::GtkZiplineFolder;
use relm::{Dialog, DialogInput};
use relm::{Dialog, DialogInput, Toast, ToastInput};
use relm4::{
Component, ComponentController, Controller, RelmApp, Sender,
adw::{self, prelude::*},
@ -51,7 +51,7 @@ enum Message {
enum ProgressMessage {
SetTotal(usize),
Progress(usize),
Finish,
Finish(String),
Error(String),
}
@ -69,6 +69,7 @@ struct Tyrolienne {
video_path: Option<PathBuf>,
folder: Option<ZiplineFolder>,
dialog: Controller<Dialog>,
toast: Controller<Toast>,
}
impl Tyrolienne {
@ -100,63 +101,68 @@ impl AsyncComponent for Tyrolienne {
set_title: Some("Tyrolienne"),
set_default_width: 500,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 0,
adw::HeaderBar {},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 32,
set_margin_top: 32,
set_margin_bottom: 32,
set_margin_start: 32,
set_margin_end: 32,
gtk::ListBox {
set_selection_mode: gtk::SelectionMode::None,
set_css_classes: &["boxed-list"],
adw::ActionRow {
set_activatable: true,
set_css_classes: &["property"],
set_title: "Video file",
#[watch]
set_subtitle: &model.display_video_path(),
connect_activated => Message::OpenFilePicker,
},
adw::ComboRow {
set_title: "Folder",
set_model: Some(&folder_store),
set_expression: Some(&folder_expression),
connect_activated[sender] => move |r| {
if let Some(item) = r
.selected_item()
.and_then(|i| i.downcast::<GtkZiplineFolder>().ok())
{
sender.input(Message::SetFolder(item.as_folder()));
}
},
}
},
#[local_ref]
toast_overlay -> adw::ToastOverlay {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_spacing: 32,
set_margin_top: 32,
set_margin_bottom: 32,
set_margin_start: 32,
set_margin_end: 32,
gtk::ProgressBar {
#[watch]
set_fraction: model.progress as f64 / model.total as f64,
gtk::ListBox {
set_selection_mode: gtk::SelectionMode::None,
set_css_classes: &["boxed-list"],
adw::ActionRow {
set_activatable: true,
set_css_classes: &["property"],
set_title: "Video file",
#[watch]
set_subtitle: &model.display_video_path(),
connect_activated => Message::OpenFilePicker,
},
adw::ComboRow {
set_title: "Folder",
set_model: Some(&folder_store),
set_expression: Some(&folder_expression),
connect_activated[sender] => move |r| {
if let Some(item) = r
.selected_item()
.and_then(|i| i.downcast::<GtkZiplineFolder>().ok())
{
sender.input(Message::SetFolder(item.as_folder()));
}
},
}
},
gtk::Button {
#[watch]
set_label: if model.locked { "Uploading..." } else { "Send" },
#[watch]
set_sensitive: !model.locked && model.video_path.is_some(),
connect_clicked => Message::StartTheProcess,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
gtk::ProgressBar {
#[watch]
set_fraction: model.progress as f64 / model.total as f64,
},
gtk::Button {
#[watch]
set_label: if model.locked { "Uploading..." } else { "Send" },
#[watch]
set_sensitive: !model.locked && model.video_path.is_some(),
connect_clicked => Message::StartTheProcess,
}
}
}
}
@ -169,6 +175,8 @@ impl AsyncComponent for Tyrolienne {
root: Self::Root,
sender: relm4::AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let toast_overlay = adw::ToastOverlay::new();
let model = Tyrolienne {
config,
locked: false,
@ -177,7 +185,10 @@ impl AsyncComponent for Tyrolienne {
video_path: None,
folder: None,
dialog: Dialog::builder()
.launch(root.clone())
.launch(toast_overlay.clone())
.forward(sender.input_sender(), |_| Message::Nothing),
toast: Toast::builder()
.launch(toast_overlay.clone())
.forward(sender.input_sender(), |_| Message::Nothing),
};
@ -242,10 +253,7 @@ impl AsyncComponent for Tyrolienne {
shutdown
.register(async move {
match the_process(info, &out).await {
Ok(url) => {
tracing::info!("{url}");
out.emit(ProgressMessage::Finish);
}
Ok(url) => out.emit(ProgressMessage::Finish(url)),
Err(e) => out.emit(ProgressMessage::Error(e.to_string())),
}
})
@ -265,12 +273,14 @@ impl AsyncComponent for Tyrolienne {
match message {
ProgressMessage::SetTotal(total) => self.total = total,
ProgressMessage::Progress(prog) => self.progress += prog,
ProgressMessage::Finish | ProgressMessage::Error(_) => {
ProgressMessage::Finish(_) | ProgressMessage::Error(_) => {
self.locked = false;
self.progress = 0;
self.total = 1;
if let ProgressMessage::Error(e) = message {
if let ProgressMessage::Finish(url) = message {
self.toast.emit(ToastInput::Show(url));
} else if let ProgressMessage::Error(e) = message {
self.dialog.emit(DialogInput::Show {
heading: "An error occurred".into(),
body: e.to_string(),

View file

@ -5,7 +5,9 @@ use relm4::{
};
pub struct Dialog {
window: adw::ApplicationWindow,
// toast overlay looks like an odd choice but i think it's fitting considering it acts as a
// "box" for the main content
window: adw::ToastOverlay,
visible: bool,
heading: String,
body: String,
@ -22,7 +24,7 @@ pub struct DialogWidgets {
}
impl SimpleComponent for Dialog {
type Init = adw::ApplicationWindow;
type Init = adw::ToastOverlay;
type Root = adw::AlertDialog;
type Widgets = DialogWidgets;
type Input = DialogInput;
@ -80,3 +82,75 @@ impl SimpleComponent for Dialog {
}
}
}
pub struct Toast {
root: adw::ToastOverlay,
visible: bool,
text: String,
}
#[derive(Debug)]
pub enum ToastInput {
Show(String),
Copy,
Dismiss,
}
#[relm4::component(pub)]
impl SimpleComponent for Toast {
type Input = ToastInput;
type Output = ();
type Init = adw::ToastOverlay;
view! {
#[name = "toast"]
adw::Toast {
#[watch]
set_title: &model.text,
set_button_label: Some("Copy"),
connect_button_clicked => ToastInput::Copy,
connect_dismissed => ToastInput::Dismiss,
}
}
fn init(
init: Self::Init,
root: Self::Root,
_sender: relm4::ComponentSender<Self>,
) -> relm4::ComponentParts<Self> {
let model = Self {
root: init,
visible: false,
text: String::new(),
};
let widgets = view_output!();
relm4::ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender<Self>) {
match message {
ToastInput::Show(text) => {
self.visible = true;
self.text = text;
}
ToastInput::Copy => {
if let Err(e) =
arboard::Clipboard::new().and_then(|mut c| c.set_text(self.text.clone()))
{
tracing::error!("could not copy url to clipboard: {e}");
self.text = "Could not copy".into();
} else {
self.visible = false;
}
}
ToastInput::Dismiss => self.visible = false,
}
}
fn post_view() {
if model.visible {
model.root.add_toast(widgets.toast.clone());
}
}
}