mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-04-02 09:35:28 +00:00
format request, begin download feature implementation
This commit is contained in:
parent
88b3adaf2d
commit
09f2a17f22
6 changed files with 533 additions and 187 deletions
|
|
@ -19,7 +19,7 @@ GTK 4 / Libadwaita client written in Rust
|
||||||
* [ ] [Audio](#audio)
|
* [ ] [Audio](#audio)
|
||||||
* [ ] [Video](#video)
|
* [ ] [Video](#video)
|
||||||
* [ ] Certificates
|
* [ ] Certificates
|
||||||
* [ ] Downloads
|
* [x] Downloads
|
||||||
* [ ] History
|
* [ ] History
|
||||||
* [ ] Proxy
|
* [ ] Proxy
|
||||||
* [ ] Session
|
* [ ] Session
|
||||||
|
|
@ -95,8 +95,10 @@ GTK 4 / Libadwaita client written in Rust
|
||||||
* [ ] [NPS](https://nightfall.city/nps/info/specification.txt)
|
* [ ] [NPS](https://nightfall.city/nps/info/specification.txt)
|
||||||
* [ ] Localhost
|
* [ ] Localhost
|
||||||
* [ ] `file://` - local file browser
|
* [ ] `file://` - local file browser
|
||||||
* [ ] System
|
* [ ] Request mode
|
||||||
* [ ] `config:` - low-level key/value settings editor
|
* [ ] `about:`
|
||||||
|
* [ ] `config` - low-level key/value settings editor
|
||||||
|
* [x] `download:` - save current request to file
|
||||||
* [x] `source:` - page source viewer (by [sourceview5](https://crates.io/crates/sourceview5))
|
* [x] `source:` - page source viewer (by [sourceview5](https://crates.io/crates/sourceview5))
|
||||||
|
|
||||||
### Media types
|
### Media types
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ mod error;
|
||||||
mod input;
|
mod input;
|
||||||
mod meta;
|
mod meta;
|
||||||
mod navigation;
|
mod navigation;
|
||||||
|
mod request;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
|
||||||
use client::Client;
|
use client::Client;
|
||||||
|
|
@ -13,6 +14,7 @@ use error::Error;
|
||||||
use input::Input;
|
use input::Input;
|
||||||
use meta::{Meta, Status};
|
use meta::{Meta, Status};
|
||||||
use navigation::Navigation;
|
use navigation::Navigation;
|
||||||
|
use request::Request;
|
||||||
use widget::Widget;
|
use widget::Widget;
|
||||||
|
|
||||||
use crate::app::browser::{
|
use crate::app::browser::{
|
||||||
|
|
@ -23,12 +25,12 @@ use crate::Profile;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
gdk::Texture,
|
gdk::Texture,
|
||||||
gdk_pixbuf::Pixbuf,
|
gdk_pixbuf::Pixbuf,
|
||||||
gio::SocketClientEvent,
|
gio::{Cancellable, SocketClientEvent},
|
||||||
glib::{
|
glib::{
|
||||||
gformat, GString, Priority, Regex, RegexCompileFlags, RegexMatchFlags, Uri, UriFlags,
|
gformat, GString, Priority, Regex, RegexCompileFlags, RegexMatchFlags, Uri, UriFlags,
|
||||||
UriHideFlags,
|
UriHideFlags,
|
||||||
},
|
},
|
||||||
prelude::{EditableExt, SocketClientExt},
|
prelude::{CancellableExt, EditableExt, FileExt, SocketClientExt, WidgetExt},
|
||||||
};
|
};
|
||||||
use sqlite::Transaction;
|
use sqlite::Transaction;
|
||||||
use std::{rc::Rc, time::Duration};
|
use std::{rc::Rc, time::Duration};
|
||||||
|
|
@ -188,19 +190,13 @@ impl Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return value from redirection holder
|
// Return value from redirection holder
|
||||||
redirect.request()
|
Request::from(&redirect.request())
|
||||||
} else {
|
} else {
|
||||||
// Reset redirect counter as request value taken from user input
|
// Reset redirect counter as request value taken from user input
|
||||||
self.meta.unset_redirect_count();
|
self.meta.unset_redirect_count();
|
||||||
|
|
||||||
// Return value from navigation entry
|
// Return value from navigation entry
|
||||||
self.navigation.request.widget.entry.text()
|
Request::from(&self.navigation.request.widget.entry.text())
|
||||||
};
|
|
||||||
|
|
||||||
// Detect source view mode, return `request` string prepared for route
|
|
||||||
let (request, is_source) = match request.strip_prefix("source:") {
|
|
||||||
Some(postfix) => (GString::from(postfix), true),
|
|
||||||
None => (request, false),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
|
|
@ -208,12 +204,20 @@ impl Page {
|
||||||
self.browser_action.update.activate(Some(&id));
|
self.browser_action.update.activate(Some(&id));
|
||||||
|
|
||||||
// Route by request
|
// Route by request
|
||||||
match Uri::parse(&request, UriFlags::NONE) {
|
match request {
|
||||||
Ok(uri) => {
|
Request::Default(ref uri) | Request::Download(ref uri) | Request::Source(ref uri) => {
|
||||||
// Route by scheme
|
// Route by scheme
|
||||||
match uri.scheme().as_str() {
|
match uri.scheme().as_str() {
|
||||||
"file" => todo!(),
|
"file" => todo!(),
|
||||||
"gemini" => self.load_gemini(uri, is_history, is_source),
|
"gemini" => {
|
||||||
|
let (uri, is_download, is_source) = match request {
|
||||||
|
Request::Default(uri) => (uri, false, false),
|
||||||
|
Request::Download(uri) => (uri, true, false),
|
||||||
|
Request::Source(uri) => (uri, false, true),
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
self.load_gemini(uri, is_download, is_source, is_history)
|
||||||
|
}
|
||||||
scheme => {
|
scheme => {
|
||||||
// Define common data
|
// Define common data
|
||||||
let status = Status::Failure;
|
let status = Status::Failure;
|
||||||
|
|
@ -228,9 +232,7 @@ impl Page {
|
||||||
self.content
|
self.content
|
||||||
.to_status_failure()
|
.to_status_failure()
|
||||||
.set_title(title)
|
.set_title(title)
|
||||||
.set_description(Some(
|
.set_description(Some(&format!("Scheme `{scheme}` not supported")));
|
||||||
gformat!("Protocol `{scheme}` not supported").as_str(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Update meta
|
// Update meta
|
||||||
self.meta.set_status(status).set_title(title);
|
self.meta.set_status(status).set_title(title);
|
||||||
|
|
@ -240,21 +242,24 @@ impl Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Request::Search(ref query) => {
|
||||||
// Try interpret URI manually
|
// Try interpret URI manually
|
||||||
if Regex::match_simple(
|
if Regex::match_simple(
|
||||||
r"^[^\/\s]+\.[\w]{2,}",
|
r"^[^\/\s]+\.[\w]{2,}",
|
||||||
request.clone(),
|
query,
|
||||||
RegexCompileFlags::DEFAULT,
|
RegexCompileFlags::DEFAULT,
|
||||||
RegexMatchFlags::DEFAULT,
|
RegexMatchFlags::DEFAULT,
|
||||||
) {
|
) {
|
||||||
// Seems request contain some host, try append default scheme
|
// Seems request contain some host, try append default scheme
|
||||||
let request = gformat!("gemini://{request}");
|
// * make sure new request conversable to valid URI
|
||||||
// Make sure new request conversable to valid URI
|
match Uri::parse(&format!("gemini://{query}"), UriFlags::NONE) {
|
||||||
match Uri::parse(&request, UriFlags::NONE) {
|
Ok(uri) => {
|
||||||
Ok(_) => {
|
|
||||||
// Update navigation entry
|
// Update navigation entry
|
||||||
self.navigation.request.widget.entry.set_text(&request);
|
self.navigation
|
||||||
|
.request
|
||||||
|
.widget
|
||||||
|
.entry
|
||||||
|
.set_text(&uri.to_string());
|
||||||
|
|
||||||
// Load page (without history record)
|
// Load page (without history record)
|
||||||
self.load(false);
|
self.load(false);
|
||||||
|
|
@ -265,19 +270,16 @@ impl Page {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Plain text given, make search request to default provider
|
// Plain text given, make search request to default provider
|
||||||
let request = gformat!(
|
self.navigation.request.widget.entry.set_text(&format!(
|
||||||
"gemini://tlgs.one/search?{}",
|
"gemini://tlgs.one/search?{}",
|
||||||
Uri::escape_string(&request, None, false)
|
Uri::escape_string(query, None, false)
|
||||||
);
|
));
|
||||||
|
|
||||||
// Update navigation entry
|
|
||||||
self.navigation.request.widget.entry.set_text(&request);
|
|
||||||
|
|
||||||
// Load page (without history record)
|
// Load page (without history record)
|
||||||
self.load(false);
|
self.load(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}; // Uri::parse
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&self) {
|
pub fn update(&self) {
|
||||||
|
|
@ -385,7 +387,7 @@ impl Page {
|
||||||
// Private helpers
|
// Private helpers
|
||||||
|
|
||||||
// @TODO move outside
|
// @TODO move outside
|
||||||
fn load_gemini(&self, uri: Uri, is_history: bool, is_source: bool) {
|
fn load_gemini(&self, uri: Uri, is_download: bool, is_source: bool, is_history: bool) {
|
||||||
// Init shared clones
|
// Init shared clones
|
||||||
let cancellable = self.client.cancellable();
|
let cancellable = self.client.cancellable();
|
||||||
let update = self.browser_action.update.clone();
|
let update = self.browser_action.update.clone();
|
||||||
|
|
@ -473,12 +475,93 @@ impl Page {
|
||||||
},
|
},
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-20
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-20
|
||||||
gemini::client::connection::response::meta::Status::Success => {
|
gemini::client::connection::response::meta::Status::Success => {
|
||||||
// Add history record
|
|
||||||
if is_history {
|
if is_history {
|
||||||
snap_history(navigation.clone());
|
snap_history(navigation.clone());
|
||||||
}
|
}
|
||||||
|
if is_download {
|
||||||
|
// Update meta
|
||||||
|
meta.set_status(Status::Success).set_title("Download");
|
||||||
|
|
||||||
// Route by MIME
|
// Update window
|
||||||
|
update.activate(Some(&id));
|
||||||
|
|
||||||
|
// Init download widget
|
||||||
|
content.to_status_download(
|
||||||
|
&uri_to_title(&uri), // grab default filename
|
||||||
|
&cancellable,
|
||||||
|
{
|
||||||
|
let cancellable = cancellable.clone();
|
||||||
|
move |file, download_status| {
|
||||||
|
match file.replace(
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
gtk::gio::FileCreateFlags::NONE,
|
||||||
|
Some(&cancellable)
|
||||||
|
) {
|
||||||
|
Ok(file_output_stream) => {
|
||||||
|
gemini::gio::file_output_stream::move_all_from_stream_async(
|
||||||
|
response.connection.stream(),
|
||||||
|
file_output_stream,
|
||||||
|
cancellable.clone(),
|
||||||
|
Priority::DEFAULT,
|
||||||
|
(
|
||||||
|
0x400, // 1024 bytes per chunk
|
||||||
|
None, // unlimited
|
||||||
|
0 // initial totals
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
let download_status = download_status.clone();
|
||||||
|
move |_, total| {
|
||||||
|
// Update loading progress
|
||||||
|
download_status.set_label(
|
||||||
|
&format!("Download: {total} bytes")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let cancellable = cancellable.clone();
|
||||||
|
move |result| match result {
|
||||||
|
Ok((_, total)) => {
|
||||||
|
// Update loading progress
|
||||||
|
download_status.set_label(
|
||||||
|
&format!("Download completed ({total} bytes total)")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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) => {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else { // browse
|
||||||
match response.meta.mime {
|
match response.meta.mime {
|
||||||
Some(gemini::client::connection::response::meta::Mime::TextGemini) => {
|
Some(gemini::client::connection::response::meta::Mime::TextGemini) => {
|
||||||
// Read entire input stream to buffer
|
// Read entire input stream to buffer
|
||||||
|
|
@ -562,7 +645,7 @@ impl Page {
|
||||||
Priority::DEFAULT,
|
Priority::DEFAULT,
|
||||||
0x400, // 1024 bytes per chunk, optional step for images download tracking
|
0x400, // 1024 bytes per chunk, optional step for images download tracking
|
||||||
0xA00000, // 10M bytes max to prevent memory overflow if server play with promises
|
0xA00000, // 10M bytes max to prevent memory overflow if server play with promises
|
||||||
move |(_, total)| {
|
move |_, total| {
|
||||||
// Update loading progress
|
// Update loading progress
|
||||||
status.set_description(
|
status.set_description(
|
||||||
Some(&gformat!("Download: {total} bytes"))
|
Some(&gformat!("Download: {total} bytes"))
|
||||||
|
|
@ -576,7 +659,7 @@ impl Page {
|
||||||
let update = update.clone();
|
let update = update.clone();
|
||||||
let uri = uri.clone();
|
let uri = uri.clone();
|
||||||
move |result| match result {
|
move |result| match result {
|
||||||
Ok(memory_input_stream) => {
|
Ok((memory_input_stream, _)) => {
|
||||||
Pixbuf::from_stream_async(
|
Pixbuf::from_stream_async(
|
||||||
&memory_input_stream,
|
&memory_input_stream,
|
||||||
Some(&cancellable),
|
Some(&cancellable),
|
||||||
|
|
@ -653,6 +736,7 @@ impl Page {
|
||||||
update.activate(Some(&id));
|
update.activate(Some(&id));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
|
||||||
gemini::client::connection::response::meta::Status::Redirect |
|
gemini::client::connection::response::meta::Status::Redirect |
|
||||||
|
|
@ -816,7 +900,7 @@ impl Page {
|
||||||
.set_title(title)
|
.set_title(title)
|
||||||
.set_description(Some(&match response.meta.data {
|
.set_description(Some(&match response.meta.data {
|
||||||
Some(data) => data.value,
|
Some(data) => data.value,
|
||||||
None => gformat!("Status code yet not supported"),
|
None => gformat!("Status code not supported"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update meta
|
// Update meta
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ use text::Text;
|
||||||
use crate::app::browser::window::{tab::item::Action as TabAction, Action as WindowAction};
|
use crate::app::browser::window::{tab::item::Action as TabAction, Action as WindowAction};
|
||||||
use gtk::{
|
use gtk::{
|
||||||
gdk::Paintable,
|
gdk::Paintable,
|
||||||
|
gio::{Cancellable, File},
|
||||||
glib::Uri,
|
glib::Uri,
|
||||||
prelude::{BoxExt, IsA, WidgetExt},
|
prelude::{BoxExt, IsA, WidgetExt},
|
||||||
Box, Orientation,
|
Box, Label, Orientation,
|
||||||
};
|
};
|
||||||
use std::{rc::Rc, time::Duration};
|
use std::{rc::Rc, time::Duration};
|
||||||
|
|
||||||
|
|
@ -45,6 +46,21 @@ impl Content {
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set new `content::Status` component for `Self` with new `status::Download` preset
|
||||||
|
///
|
||||||
|
/// * action removes previous children component from `Self`
|
||||||
|
pub fn to_status_download(
|
||||||
|
&self,
|
||||||
|
initial_filename: &str,
|
||||||
|
cancellable: &Cancellable,
|
||||||
|
on_choose: impl Fn(File, Label) + 'static,
|
||||||
|
) -> Status {
|
||||||
|
self.clean();
|
||||||
|
let status = Status::new_download(initial_filename, cancellable, on_choose);
|
||||||
|
self.gobject.append(status.gobject());
|
||||||
|
status
|
||||||
|
}
|
||||||
|
|
||||||
/// Set new `content::Status` component for `Self` with new `status::Failure` preset
|
/// Set new `content::Status` component for `Self` with new `status::Failure` preset
|
||||||
///
|
///
|
||||||
/// * action removes previous children component from `Self`
|
/// * action removes previous children component from `Self`
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
|
mod download;
|
||||||
mod failure;
|
mod failure;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod loading;
|
mod loading;
|
||||||
|
|
||||||
use crate::app::browser::window::tab::item::Action;
|
use crate::app::browser::window::tab::item::Action;
|
||||||
use adw::StatusPage;
|
use adw::StatusPage;
|
||||||
|
use gtk::{
|
||||||
|
gio::{Cancellable, File},
|
||||||
|
Label,
|
||||||
|
};
|
||||||
use std::{rc::Rc, time::Duration};
|
use std::{rc::Rc, time::Duration};
|
||||||
|
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
|
|
@ -13,6 +18,17 @@ pub struct Status {
|
||||||
impl Status {
|
impl Status {
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new download preset
|
||||||
|
pub fn new_download(
|
||||||
|
initial_filename: &str,
|
||||||
|
cancellable: &Cancellable,
|
||||||
|
on_choose: impl Fn(File, Label) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
gobject: download::new(initial_filename, cancellable, on_choose),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create new failure preset
|
/// Create new failure preset
|
||||||
///
|
///
|
||||||
/// Useful as placeholder widget for error handlers
|
/// Useful as placeholder widget for error handlers
|
||||||
|
|
|
||||||
194
src/app/browser/window/tab/item/page/content/status/download.rs
Normal file
194
src/app/browser/window/tab/item/page/content/status/download.rs
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
use adw::StatusPage;
|
||||||
|
use gtk::{
|
||||||
|
gio::{Cancellable, File},
|
||||||
|
prelude::{BoxExt, ButtonExt, CancellableExt, WidgetExt},
|
||||||
|
Align,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FileDialog,
|
||||||
|
FileLauncher,
|
||||||
|
Label,
|
||||||
|
Orientation,
|
||||||
|
Spinner, // use adw::Spinner; @TODO adw 1.6 / ubuntu 24.10+
|
||||||
|
Window,
|
||||||
|
};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
const MARGIN: i32 = 16;
|
||||||
|
const SPINNER_SIZE: i32 = 32; // 16-64
|
||||||
|
|
||||||
|
/// Create new [StatusPage](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.StatusPage.html)
|
||||||
|
/// with progress indication and UI controls
|
||||||
|
/// * applies callback function once on destination [File](https://docs.gtk.org/gio/iface.File.html) selected
|
||||||
|
/// * requires external IOStream read/write implementation, depending of protocol driver in use
|
||||||
|
pub fn new(
|
||||||
|
initial_filename: &str,
|
||||||
|
cancellable: &Cancellable,
|
||||||
|
on_choose: impl Fn(File, Label) + 'static,
|
||||||
|
) -> StatusPage {
|
||||||
|
// Init file chooser dialog
|
||||||
|
let dialog = FileDialog::builder().initial_name(initial_filename).build();
|
||||||
|
|
||||||
|
// Init file launcher dialog
|
||||||
|
let file_launcher = FileLauncher::new(File::NONE);
|
||||||
|
|
||||||
|
// Init spinner component
|
||||||
|
let spinner = Spinner::builder()
|
||||||
|
.height_request(SPINNER_SIZE)
|
||||||
|
.visible(false)
|
||||||
|
.width_request(SPINNER_SIZE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Init `status` feature
|
||||||
|
// * indicates current download state in text label
|
||||||
|
let status = Label::builder()
|
||||||
|
.label("Choose location to download")
|
||||||
|
.margin_top(MARGIN)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Init `cancel` feature
|
||||||
|
// * applies shared `Cancellable`
|
||||||
|
let cancel = Button::builder()
|
||||||
|
.css_classes(["error"])
|
||||||
|
.halign(Align::Center)
|
||||||
|
.label("Cancel")
|
||||||
|
.margin_top(MARGIN)
|
||||||
|
.visible(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
cancel.connect_clicked({
|
||||||
|
let cancellable = cancellable.clone();
|
||||||
|
let spinner = spinner.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
move |this| {
|
||||||
|
// apply cancellable
|
||||||
|
cancellable.cancel();
|
||||||
|
|
||||||
|
// deactivate `spinner`
|
||||||
|
spinner.set_visible(false);
|
||||||
|
spinner.stop();
|
||||||
|
|
||||||
|
// update `status`
|
||||||
|
status.set_css_classes(&["warning"]);
|
||||||
|
status.set_label("Operation cancelled");
|
||||||
|
|
||||||
|
// hide self
|
||||||
|
this.set_visible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Init `open` feature
|
||||||
|
// * open selected file on download complete
|
||||||
|
let open = Button::builder()
|
||||||
|
.css_classes(["accent"])
|
||||||
|
.halign(Align::Center)
|
||||||
|
.label("Open")
|
||||||
|
.margin_top(MARGIN)
|
||||||
|
.visible(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
open.connect_clicked({
|
||||||
|
let file_launcher = file_launcher.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
move |this| {
|
||||||
|
this.set_sensitive(false); // lock
|
||||||
|
file_launcher.launch(Window::NONE, Cancellable::NONE, {
|
||||||
|
let status = status.clone();
|
||||||
|
let this = this.clone();
|
||||||
|
move |result| {
|
||||||
|
if let Err(ref e) = result {
|
||||||
|
status.set_css_classes(&["error"]);
|
||||||
|
status.set_label(e.message())
|
||||||
|
}
|
||||||
|
this.set_sensitive(true); // unlock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Init `choose` feature
|
||||||
|
// * select file destination for download
|
||||||
|
let choose = Button::builder()
|
||||||
|
.css_classes(["accent"])
|
||||||
|
.halign(Align::Center)
|
||||||
|
.label("Choose..")
|
||||||
|
.margin_top(MARGIN)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
choose.connect_clicked({
|
||||||
|
// init shared references
|
||||||
|
let cancel = cancel.clone();
|
||||||
|
let dialog = dialog.clone();
|
||||||
|
let file_launcher = file_launcher.clone();
|
||||||
|
let spinner = spinner.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
let on_choose = Rc::new(on_choose);
|
||||||
|
move |this| {
|
||||||
|
// lock choose button to prevent double click
|
||||||
|
this.set_sensitive(false);
|
||||||
|
dialog.save(Window::NONE, Cancellable::NONE, {
|
||||||
|
// delegate shared references
|
||||||
|
let cancel = cancel.clone();
|
||||||
|
let file_launcher = file_launcher.clone();
|
||||||
|
let spinner = spinner.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
let this = this.clone();
|
||||||
|
let on_choose = on_choose.clone();
|
||||||
|
move |result| {
|
||||||
|
this.set_sensitive(true); // unlock
|
||||||
|
match result {
|
||||||
|
Ok(file) => {
|
||||||
|
// update destination file
|
||||||
|
file_launcher.set_file(Some(&file));
|
||||||
|
|
||||||
|
// update `status`
|
||||||
|
status.set_css_classes(&[]);
|
||||||
|
status.set_label("Loading...");
|
||||||
|
|
||||||
|
// show `cancel` button
|
||||||
|
cancel.set_visible(true);
|
||||||
|
|
||||||
|
// show `spinner`
|
||||||
|
spinner.set_visible(true);
|
||||||
|
spinner.start();
|
||||||
|
|
||||||
|
// hide self
|
||||||
|
this.set_visible(false);
|
||||||
|
|
||||||
|
// callback
|
||||||
|
on_choose(file, status)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// update destination file
|
||||||
|
file_launcher.set_file(File::NONE);
|
||||||
|
|
||||||
|
// update `spinner`
|
||||||
|
spinner.set_visible(false);
|
||||||
|
spinner.stop();
|
||||||
|
|
||||||
|
// update `status`
|
||||||
|
status.set_css_classes(&["warning"]);
|
||||||
|
status.set_label(e.message())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Init main container
|
||||||
|
let child = Box::builder().orientation(Orientation::Vertical).build();
|
||||||
|
|
||||||
|
child.append(&spinner);
|
||||||
|
child.append(&status);
|
||||||
|
child.append(&cancel);
|
||||||
|
child.append(&choose);
|
||||||
|
child.append(&open);
|
||||||
|
|
||||||
|
// Done
|
||||||
|
StatusPage::builder()
|
||||||
|
.child(&child)
|
||||||
|
.icon_name("document-save-symbolic")
|
||||||
|
.title("Download")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
34
src/app/browser/window/tab/item/page/request.rs
Normal file
34
src/app/browser/window/tab/item/page/request.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use gtk::glib::{Uri, UriFlags};
|
||||||
|
|
||||||
|
/// Request type for `Page` with optional value parsed
|
||||||
|
pub enum Request {
|
||||||
|
Default(Uri),
|
||||||
|
Download(Uri),
|
||||||
|
Source(Uri),
|
||||||
|
Search(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new `Self` from `request` string
|
||||||
|
pub fn from(request: &str) -> Self {
|
||||||
|
if let Some(postfix) = request.strip_prefix("source:") {
|
||||||
|
if let Ok(uri) = Uri::parse(postfix, UriFlags::NONE) {
|
||||||
|
return Self::Source(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(postfix) = request.strip_prefix("download:") {
|
||||||
|
if let Ok(uri) = Uri::parse(postfix, UriFlags::NONE) {
|
||||||
|
return Self::Download(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(uri) = Uri::parse(request, UriFlags::NONE) {
|
||||||
|
return Self::Default(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Search(request.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue