implement public api actions for download feature

This commit is contained in:
yggverse 2024-12-11 01:03:46 +02:00
parent 6f1761dcd7
commit 054afd270a
8 changed files with 230 additions and 53 deletions

View File

@ -25,12 +25,12 @@ use crate::Profile;
use gtk::{ use gtk::{
gdk::Texture, gdk::Texture,
gdk_pixbuf::Pixbuf, gdk_pixbuf::Pixbuf,
gio::{Cancellable, SocketClientEvent}, gio::SocketClientEvent,
glib::{ glib::{
gformat, GString, Priority, Regex, RegexCompileFlags, RegexMatchFlags, Uri, UriFlags, gformat, GString, Priority, Regex, RegexCompileFlags, RegexMatchFlags, Uri, UriFlags,
UriHideFlags, UriHideFlags,
}, },
prelude::{CancellableExt, EditableExt, FileExt, SocketClientExt, WidgetExt}, prelude::{EditableExt, FileExt, SocketClientExt},
}; };
use sqlite::Transaction; use sqlite::Transaction;
use std::{rc::Rc, time::Duration}; use std::{rc::Rc, time::Duration};
@ -480,7 +480,7 @@ impl Page {
&cancellable, &cancellable,
{ {
let cancellable = cancellable.clone(); let cancellable = cancellable.clone();
move |file, download_status| { move |file, action| {
match file.replace( match file.replace(
None, None,
false, false,
@ -499,52 +499,28 @@ impl Page {
0 // initial totals 0 // initial totals
), ),
( (
// on chunk
{ {
let download_status = download_status.clone(); let action = action.clone();
move |_, total| { move |_, total| action.update.activate(
// Update loading progress &format!("Received {total} bytes...")
download_status.set_label( )
&format!("Received {total} bytes...")
);
}
}, },
// on complete
{ {
let cancellable = cancellable.clone(); let action = action.clone();
move |result| match result { move |result| match result {
Ok((_, total)) => { Ok((_, total)) => action.complete.activate(
// Update loading progress &format!("Download ({total} bytes) completed!")
download_status.set_label( ),
&format!("Download ({total} bytes) completed!") Err(e) => action.cancel.activate(&e.to_string())
);
}
Err(e) => {
// cancel uncompleted async operations
// * this will also toggle download widget actions
cancellable.cancel();
// update status message
download_status.set_label(&e.to_string());
download_status.set_css_classes(&["error"]);
// cleanup
let _ = file.delete(Cancellable::NONE); // @TODO
}
} }
} }
) )
); );
}, },
Err(e) => { Err(e) => {
// cancel uncompleted async operations action.cancel.activate(&e.to_string())
// * this will also toggle download widget actions
cancellable.cancel();
// update status message
download_status.set_label(&e.to_string());
download_status.set_css_classes(&["error"]);
// cleanup
let _ = file.delete(Cancellable::NONE); // @TODO
} }
} }
} }

View File

@ -12,7 +12,7 @@ use gtk::{
gio::{Cancellable, File}, gio::{Cancellable, File},
glib::Uri, glib::Uri,
prelude::{BoxExt, IsA, WidgetExt}, prelude::{BoxExt, IsA, WidgetExt},
Box, Label, Orientation, Box, Orientation,
}; };
use std::{rc::Rc, time::Duration}; use std::{rc::Rc, time::Duration};
@ -53,7 +53,7 @@ impl Content {
&self, &self,
initial_filename: &str, initial_filename: &str,
cancellable: &Cancellable, cancellable: &Cancellable,
on_choose: impl Fn(File, Label) + 'static, on_choose: impl Fn(File, Rc<status::download::Action>) + 'static,
) -> Status { ) -> Status {
self.clean(); self.clean();
let status = Status::new_download(initial_filename, cancellable, on_choose); let status = Status::new_download(initial_filename, cancellable, on_choose);

View File

@ -1,14 +1,10 @@
mod download; pub mod download;
mod failure; mod failure;
mod identity; mod identity;
mod loading; mod loading;
use crate::app::browser::window::tab::item::Action;
use adw::StatusPage; use adw::StatusPage;
use gtk::{ use gtk::gio::{Cancellable, File};
gio::{Cancellable, File},
Label,
};
use std::{rc::Rc, time::Duration}; use std::{rc::Rc, time::Duration};
pub struct Status { pub struct Status {
@ -22,7 +18,7 @@ impl Status {
pub fn new_download( pub fn new_download(
initial_filename: &str, initial_filename: &str,
cancellable: &Cancellable, cancellable: &Cancellable,
on_choose: impl Fn(File, Label) + 'static, on_choose: impl Fn(File, Rc<download::Action>) + 'static,
) -> Self { ) -> Self {
Self { Self {
gobject: download::new(initial_filename, cancellable, on_choose), gobject: download::new(initial_filename, cancellable, on_choose),
@ -42,7 +38,7 @@ impl Status {
/// ///
/// Useful as placeholder for 60 status code /// Useful as placeholder for 60 status code
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
pub fn new_identity(action: Rc<Action>) -> Self { pub fn new_identity(action: Rc<crate::app::browser::window::tab::item::Action>) -> Self {
Self { Self {
gobject: identity::new_gobject(action), gobject: identity::new_gobject(action),
} }

View File

@ -1,9 +1,11 @@
mod action;
mod cancel; mod cancel;
mod choose; mod choose;
mod open; mod open;
mod progress; mod progress;
mod status; mod status;
pub use action::Action;
use cancel::Cancel; use cancel::Cancel;
use choose::Choose; use choose::Choose;
use open::Open; use open::Open;
@ -14,7 +16,7 @@ use adw::StatusPage;
use gtk::{ use gtk::{
gio::{Cancellable, File}, gio::{Cancellable, File},
prelude::{BoxExt, CancellableExt, WidgetExt}, prelude::{BoxExt, CancellableExt, WidgetExt},
Box, FileDialog, FileLauncher, Label, Orientation, Window, Box, FileDialog, FileLauncher, Orientation, Window,
}; };
use std::rc::Rc; use std::rc::Rc;
@ -32,19 +34,57 @@ const TITLE: &str = "Download";
pub fn new( pub fn new(
initial_filename: &str, initial_filename: &str,
cancellable: &Cancellable, cancellable: &Cancellable,
on_choose: impl Fn(File, Label) + 'static, on_choose: impl Fn(File, Rc<Action>) + 'static,
) -> StatusPage { ) -> StatusPage {
// Init components // Init components
let dialog = FileDialog::builder().initial_name(initial_filename).build(); let dialog = FileDialog::builder().initial_name(initial_filename).build();
let file_launcher = FileLauncher::new(File::NONE); let file_launcher = FileLauncher::new(File::NONE);
let action = Rc::new(Action::new()); // public callback API
let cancel = Rc::new(Cancel::new()); let cancel = Rc::new(Cancel::new());
let choose = Rc::new(Choose::new()); let choose = Rc::new(Choose::new());
let open = Rc::new(Open::new()); let open = Rc::new(Open::new());
let progress = Rc::new(Progress::new()); let progress = Rc::new(Progress::new());
let status = Rc::new(Status::new()); let status = Rc::new(Status::new());
// Init events // Init action events
action.cancel.on_activate({
let cancel = cancel.clone();
let cancellable = cancellable.clone();
let progress = progress.clone();
let status = status.clone();
move |_, message| {
cancellable.cancel();
progress.disable();
status.set_warning(&message);
cancel.button.set_visible(false);
}
});
action.complete.on_activate({
let cancel = cancel.clone();
let cancellable = cancellable.clone();
let open = open.clone();
let progress = progress.clone();
let status = status.clone();
move |_, message| {
cancellable.cancel();
progress.disable();
status.set_success(&message);
cancel.button.set_visible(false);
open.button.set_visible(true);
}
});
action.update.on_activate({
let status = status.clone();
move |_, message| {
status.set_default(&message);
}
});
// Init widget events
cancel.on_activate({ cancel.on_activate({
let cancellable = cancellable.clone(); let cancellable = cancellable.clone();
let progress = progress.clone(); let progress = progress.clone();
@ -75,6 +115,7 @@ pub fn new(
button.set_sensitive(false); button.set_sensitive(false);
dialog.save(Window::NONE, Some(&cancellable), { dialog.save(Window::NONE, Some(&cancellable), {
// delegate shared references // delegate shared references
let action = action.clone();
let cancel = cancel.clone(); let cancel = cancel.clone();
let file_launcher = file_launcher.clone(); let file_launcher = file_launcher.clone();
let progress = progress.clone(); let progress = progress.clone();
@ -96,7 +137,7 @@ pub fn new(
// hide self // hide self
button.set_visible(false); button.set_visible(false);
// callback // callback
on_choose(file, status.label.clone()) on_choose(file, action)
} }
Err(e) => { Err(e) => {
// update destination file // update destination file

View File

@ -0,0 +1,29 @@
mod cancel;
mod complete;
mod update;
use cancel::Cancel;
use complete::Complete;
use update::Update;
use std::rc::Rc;
/// Callback API for `Download` widget
pub struct Action {
pub cancel: Rc<Cancel>,
pub complete: Rc<Complete>,
pub update: Rc<Update>,
}
impl Action {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
cancel: Rc::new(Cancel::new()),
complete: Rc::new(Complete::new()),
update: Rc::new(Update::new()),
}
}
}

View File

@ -0,0 +1,45 @@
// Defaults
use gtk::{
gio::SimpleAction,
glib::{uuid_string_random, SignalHandlerId},
prelude::{ActionExt, StaticVariantType, ToVariant},
};
/// Cancel [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html)
pub struct Cancel {
pub action: SimpleAction,
}
impl Cancel {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
action: SimpleAction::new(&uuid_string_random(), Some(&String::static_variant_type())),
}
}
// Actions
pub fn activate(&self, message: &str) {
self.action.activate(Some(&message.to_variant()));
}
/// Formatted action connector for external implementation
pub fn on_activate(
&self,
callback: impl Fn(&SimpleAction, String) + 'static,
) -> SignalHandlerId {
self.action.connect_activate(move |this, message| {
callback(
this,
message
.expect("Variant required to call this action")
.get::<String>()
.expect("Parameter does not match `String` type"),
)
})
}
}

View File

@ -0,0 +1,45 @@
// Defaults
use gtk::{
gio::SimpleAction,
glib::{uuid_string_random, SignalHandlerId},
prelude::{ActionExt, StaticVariantType, ToVariant},
};
/// Complete [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html)
pub struct Complete {
pub action: SimpleAction,
}
impl Complete {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
action: SimpleAction::new(&uuid_string_random(), Some(&String::static_variant_type())),
}
}
// Actions
pub fn activate(&self, message: &str) {
self.action.activate(Some(&message.to_variant()));
}
/// Formatted action connector for external implementation
pub fn on_activate(
&self,
callback: impl Fn(&SimpleAction, String) + 'static,
) -> SignalHandlerId {
self.action.connect_activate(move |this, message| {
callback(
this,
message
.expect("Variant required to call this action")
.get::<String>()
.expect("Parameter does not match `String` type"),
)
})
}
}

View File

@ -0,0 +1,45 @@
// Defaults
use gtk::{
gio::SimpleAction,
glib::{uuid_string_random, SignalHandlerId},
prelude::{ActionExt, StaticVariantType, ToVariant},
};
/// Update [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html)
pub struct Update {
pub action: SimpleAction,
}
impl Update {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
action: SimpleAction::new(&uuid_string_random(), Some(&String::static_variant_type())),
}
}
// Actions
pub fn activate(&self, message: &str) {
self.action.activate(Some(&message.to_variant()));
}
/// Formatted action connector for external implementation
pub fn on_activate(
&self,
callback: impl Fn(&SimpleAction, String) + 'static,
) -> SignalHandlerId {
self.action.connect_activate(move |this, message| {
callback(
this,
message
.expect("Variant required to call this action")
.get::<String>()
.expect("Parameter does not match `String` type"),
)
})
}
}