From 1d93482209cbcf3102c6fb241704885a46269a5e Mon Sep 17 00:00:00 2001 From: uku Date: Tue, 13 May 2025 00:01:05 +0200 Subject: [PATCH] feat: show toast after upload is finished --- Cargo.lock | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/main.rs | 120 +++++++++++++++++--------------- src/relm.rs | 78 ++++++++++++++++++++- 4 files changed, 336 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98614cd..2785721 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "arboard" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" +dependencies = [ + "clipboard-win", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "x11rb", +] + [[package]] name = "ashpd" version = "0.11.0" @@ -192,6 +208,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + [[package]] name = "color-eyre" version = "0.6.4" @@ -340,6 +365,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "5.4.0" @@ -579,6 +610,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1136,6 +1177,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1280,6 +1327,7 @@ dependencies = [ "bitflags", "block2", "objc2", + "objc2-core-graphics", "objc2-foundation", ] @@ -1294,6 +1342,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags", + "dispatch2 0.3.0", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -1311,6 +1372,17 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-surface" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.36.7" @@ -1384,6 +1456,29 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1555,6 +1650,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.5.0" @@ -1752,6 +1856,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.7" @@ -1761,7 +1878,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -2032,7 +2149,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -2280,6 +2397,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" name = "tyrolienne" version = "0.1.0" dependencies = [ + "arboard", "color-eyre", "dirs", "futures", @@ -2580,6 +2698,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2612,6 +2745,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2624,6 +2763,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2636,6 +2781,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2660,6 +2811,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2672,6 +2829,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2684,6 +2847,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2696,6 +2865,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2732,6 +2907,23 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 3141852..1c030ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +arboard = { version = "3.5.0", default-features = false } color-eyre = "0.6.4" dirs = "6.0.0" futures = "0.3.31" diff --git a/src/main.rs b/src/main.rs index cd0c291..abfffe7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, folder: Option, dialog: Controller, + toast: Controller, } 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::().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::().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, ) -> AsyncComponentParts { + 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(), diff --git a/src/relm.rs b/src/relm.rs index 89e5bfa..4a230f4 100644 --- a/src/relm.rs +++ b/src/relm.rs @@ -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, + ) -> relm4::ComponentParts { + 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) { + 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()); + } + } +}