mirror of
https://github.com/YGGverse/Yoda.git
synced 2025-01-30 04:54:15 +00:00
begin multi-driver page client implementation
This commit is contained in:
parent
df8dea9534
commit
0c08a0fb2f
@ -16,7 +16,7 @@ use crate::app::browser::{
|
|||||||
use crate::Profile;
|
use crate::Profile;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
glib::{DateTime, GString, Propagation},
|
glib::{DateTime, GString, Propagation},
|
||||||
prelude::WidgetExt,
|
prelude::{EditableExt, WidgetExt},
|
||||||
};
|
};
|
||||||
use sqlite::Transaction;
|
use sqlite::Transaction;
|
||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
@ -218,16 +218,16 @@ impl Tab {
|
|||||||
// Save page at given `position`, `None` to save selected page (if available)
|
// Save page at given `position`, `None` to save selected page (if available)
|
||||||
pub fn save_as(&self, page_position: Option<i32>) {
|
pub fn save_as(&self, page_position: Option<i32>) {
|
||||||
if let Some(item) = self.item(page_position) {
|
if let Some(item) = self.item(page_position) {
|
||||||
item.page.navigation.request.to_download();
|
item.page.navigation.request.into_download();
|
||||||
item::page::load(&item.page, None, true);
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// View source for page at given `position`, `None` to use selected page (if available)
|
// View source for page at given `position`, `None` to use selected page (if available)
|
||||||
pub fn source(&self, page_position: Option<i32>) {
|
pub fn source(&self, page_position: Option<i32>) {
|
||||||
if let Some(item) = self.item(page_position) {
|
if let Some(item) = self.item(page_position) {
|
||||||
item.page.navigation.request.to_source();
|
item.page.navigation.request.into_source();
|
||||||
item::page::load(&item.page, None, true);
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,26 +250,36 @@ impl Tab {
|
|||||||
|
|
||||||
pub fn page_home(&self, page_position: Option<i32>) {
|
pub fn page_home(&self, page_position: Option<i32>) {
|
||||||
if let Some(item) = self.item(page_position) {
|
if let Some(item) = self.item(page_position) {
|
||||||
item::page::home(&item.page);
|
if let Some(text) = item.page.navigation.home.url() {
|
||||||
|
item.page.navigation.request.widget.entry.set_text(&text);
|
||||||
|
self.window_action.reload.activate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_history_back(&self, page_position: Option<i32>) {
|
pub fn page_history_back(&self, page_position: Option<i32>) {
|
||||||
if let Some(item) = self.item(page_position) {
|
if let Some(item) = self.item(page_position) {
|
||||||
item::page::history_back(&item.page);
|
if let Some(text) = item.page.navigation.history.back(true) {
|
||||||
|
item.page.navigation.request.widget.entry.set_text(&text);
|
||||||
|
self.window_action.reload.activate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_history_forward(&self, page_position: Option<i32>) {
|
pub fn page_history_forward(&self, page_position: Option<i32>) {
|
||||||
if let Some(item) = self.item(page_position) {
|
if let Some(item) = self.item(page_position) {
|
||||||
item::page::history_forward(&item.page);
|
if let Some(text) = item.page.navigation.history.forward(true) {
|
||||||
|
item.page.navigation.request.widget.entry.set_text(&text);
|
||||||
|
self.window_action.reload.activate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reload page at `i32` position or selected page on `None` given
|
/// Reload page at `i32` position or selected page on `None` given
|
||||||
pub fn page_reload(&self, page_position: Option<i32>) {
|
pub fn page_reload(&self, page_position: Option<i32>) {
|
||||||
if let Some(item) = self.item(page_position) {
|
if let Some(item) = self.item(page_position) {
|
||||||
item::page::load(&item.page, None, true);
|
item.client
|
||||||
|
.handle(&item.page.navigation.request.widget.entry.text(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,33 @@
|
|||||||
mod action;
|
mod action;
|
||||||
|
mod client;
|
||||||
mod database;
|
mod database;
|
||||||
mod identity;
|
mod identity;
|
||||||
pub mod page;
|
pub mod page;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
|
||||||
use action::Action;
|
|
||||||
use page::Page;
|
|
||||||
use widget::Widget;
|
|
||||||
|
|
||||||
use crate::app::browser::{
|
use crate::app::browser::{
|
||||||
window::action::{Action as WindowAction, Position},
|
window::action::{Action as WindowAction, Position},
|
||||||
Action as BrowserAction,
|
Action as BrowserAction,
|
||||||
};
|
};
|
||||||
use crate::Profile;
|
use crate::Profile;
|
||||||
|
use action::Action;
|
||||||
use adw::TabView;
|
use adw::TabView;
|
||||||
|
use client::Client;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
glib::{uuid_string_random, GString},
|
glib::{uuid_string_random, GString},
|
||||||
prelude::{Cast, EditableExt},
|
prelude::{Cast, EditableExt},
|
||||||
};
|
};
|
||||||
|
use page::Page;
|
||||||
use sqlite::Transaction;
|
use sqlite::Transaction;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use widget::Widget;
|
||||||
|
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
// Auto-generated unique item ID
|
// Auto-generated unique item ID
|
||||||
// useful as widget name in GTK actions callback
|
// useful as widget name in GTK actions callback
|
||||||
pub id: Rc<GString>,
|
pub id: Rc<GString>,
|
||||||
|
// Multi-protocol handler
|
||||||
|
pub client: Rc<Client>,
|
||||||
// Components
|
// Components
|
||||||
pub page: Rc<Page>,
|
pub page: Rc<Page>,
|
||||||
pub widget: Rc<Widget>,
|
pub widget: Rc<Widget>,
|
||||||
@ -60,6 +63,8 @@ impl Item {
|
|||||||
(browser_action, window_action, &action),
|
(browser_action, window_action, &action),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let client = Rc::new(Client::init(&page));
|
||||||
|
|
||||||
let widget = Rc::new(Widget::build(
|
let widget = Rc::new(Widget::build(
|
||||||
id.as_str(),
|
id.as_str(),
|
||||||
tab_view,
|
tab_view,
|
||||||
@ -74,7 +79,7 @@ impl Item {
|
|||||||
if let Some(text) = request {
|
if let Some(text) = request {
|
||||||
page.navigation.request.widget.entry.set_text(&text);
|
page.navigation.request.widget.entry.set_text(&text);
|
||||||
if is_load {
|
if is_load {
|
||||||
page::load(&page, None, true);
|
client.handle(&text, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +92,7 @@ impl Item {
|
|||||||
let window_action = window_action.clone();
|
let window_action = window_action.clone();
|
||||||
move || {
|
move || {
|
||||||
// Request should match valid URI for all drivers supported
|
// Request should match valid URI for all drivers supported
|
||||||
if let Some(uri) = page.navigation.request.uri() {
|
if let Some(uri) = page.navigation.request.as_uri() {
|
||||||
// Rout by scheme
|
// Rout by scheme
|
||||||
if uri.scheme().to_lowercase() == "gemini" {
|
if uri.scheme().to_lowercase() == "gemini" {
|
||||||
return identity::new_gemini(
|
return identity::new_gemini(
|
||||||
@ -106,16 +111,22 @@ impl Item {
|
|||||||
// Load new request for item
|
// Load new request for item
|
||||||
action.load.connect_activate({
|
action.load.connect_activate({
|
||||||
let page = page.clone();
|
let page = page.clone();
|
||||||
|
let client = client.clone();
|
||||||
move |request, is_history| {
|
move |request, is_history| {
|
||||||
if let Some(text) = request {
|
if let Some(text) = request {
|
||||||
page.navigation.request.widget.entry.set_text(&text);
|
page.navigation.request.widget.entry.set_text(&text);
|
||||||
|
client.handle(&text, is_history);
|
||||||
}
|
}
|
||||||
page::load(&page, None, is_history);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Done
|
// Done
|
||||||
Self { id, page, widget }
|
Self {
|
||||||
|
id,
|
||||||
|
client,
|
||||||
|
page,
|
||||||
|
widget,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
146
src/app/browser/window/tab/item/client.rs
Normal file
146
src/app/browser/window/tab/item/client.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
mod driver;
|
||||||
|
mod feature;
|
||||||
|
|
||||||
|
use super::Page;
|
||||||
|
use driver::Driver;
|
||||||
|
use feature::Feature;
|
||||||
|
use gtk::{
|
||||||
|
gio::Cancellable,
|
||||||
|
glib::{Uri, UriFlags},
|
||||||
|
prelude::CancellableExt,
|
||||||
|
};
|
||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
|
||||||
|
/// Multi-protocol client API for tab `Item`
|
||||||
|
pub struct Client {
|
||||||
|
cancellable: Cell<Cancellable>,
|
||||||
|
driver: Rc<Driver>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new `Self`
|
||||||
|
pub fn init(page: &Rc<Page>) -> Self {
|
||||||
|
Self {
|
||||||
|
cancellable: Cell::new(Cancellable::new()),
|
||||||
|
driver: Rc::new(Driver::build(page)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
/// Route tab item `request` to protocol driver
|
||||||
|
/// * or `navigation` entry if the value not provided
|
||||||
|
pub fn handle(&self, request: &str, is_snap_history: bool) {
|
||||||
|
// run async resolver to detect Uri, scheme-less host, or search query
|
||||||
|
lookup(
|
||||||
|
request,
|
||||||
|
self.driver.clone(),
|
||||||
|
self.cancellable(),
|
||||||
|
move |driver, feature, cancellable, uri| {
|
||||||
|
route(driver, feature, cancellable, uri, is_snap_history)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get new [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) by cancel previous one
|
||||||
|
fn cancellable(&self) -> Cancellable {
|
||||||
|
// Init new Cancellable
|
||||||
|
let cancellable = Cancellable::new();
|
||||||
|
|
||||||
|
// Replace by cancel previous operations
|
||||||
|
let previous = self.cancellable.replace(cancellable.clone());
|
||||||
|
if !previous.is_cancelled() {
|
||||||
|
previous.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
cancellable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create request using async DNS resolver (slow method)
|
||||||
|
/// * useful for scheme-less requests, before apply search redirect
|
||||||
|
/// * the `query` should not contain `feature` prefix
|
||||||
|
fn lookup(
|
||||||
|
query: &str,
|
||||||
|
driver: Rc<Driver>,
|
||||||
|
cancellable: Cancellable,
|
||||||
|
callback: impl FnOnce(Rc<Driver>, Feature, Cancellable, Uri) + 'static,
|
||||||
|
) {
|
||||||
|
use gtk::{
|
||||||
|
gio::{NetworkAddress, Resolver},
|
||||||
|
prelude::{NetworkAddressExt, ResolverExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SCHEME: &str = "gemini";
|
||||||
|
const DEFAULT_PORT: u16 = 1965;
|
||||||
|
const TIMEOUT: u32 = 250; // ms
|
||||||
|
|
||||||
|
let (feature, query) = Feature::parse(query.trim());
|
||||||
|
|
||||||
|
match Uri::parse(query, UriFlags::NONE) {
|
||||||
|
Ok(uri) => callback(driver, feature, cancellable, uri),
|
||||||
|
Err(_) => {
|
||||||
|
// try default scheme suggestion
|
||||||
|
let suggestion = format!("{DEFAULT_SCHEME}://{query}");
|
||||||
|
|
||||||
|
let resolver = Resolver::default();
|
||||||
|
resolver.set_timeout(TIMEOUT);
|
||||||
|
|
||||||
|
match NetworkAddress::parse_uri(&suggestion, DEFAULT_PORT) {
|
||||||
|
Ok(connectable) => resolver.lookup_by_name_async(
|
||||||
|
&connectable.hostname(),
|
||||||
|
Some(&cancellable.clone()),
|
||||||
|
move |resolve| {
|
||||||
|
callback(
|
||||||
|
driver,
|
||||||
|
feature,
|
||||||
|
cancellable,
|
||||||
|
if resolve.is_ok() {
|
||||||
|
match Uri::parse(&suggestion, UriFlags::NONE) {
|
||||||
|
Ok(uri) => uri,
|
||||||
|
Err(_) => search(&suggestion),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
search(&suggestion)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Err(_) => callback(driver, feature, cancellable, search(&suggestion)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route request (resolved by `lookup` function)
|
||||||
|
fn route(
|
||||||
|
driver: Rc<Driver>,
|
||||||
|
feature: Feature,
|
||||||
|
cancellable: Cancellable,
|
||||||
|
uri: Uri,
|
||||||
|
is_snap_history: bool,
|
||||||
|
) {
|
||||||
|
match uri.scheme().as_str() {
|
||||||
|
"gemini" => driver
|
||||||
|
.gemini
|
||||||
|
.handle(uri, feature, cancellable, is_snap_history),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert `query` to default search provider [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
||||||
|
fn search(query: &str) -> Uri {
|
||||||
|
Uri::build(
|
||||||
|
UriFlags::NONE,
|
||||||
|
"gemini",
|
||||||
|
None,
|
||||||
|
Some("tlgs.one"),
|
||||||
|
-1,
|
||||||
|
"/search",
|
||||||
|
Some(&Uri::escape_string(query, None, false)),
|
||||||
|
None,
|
||||||
|
) // @TODO optional settings
|
||||||
|
}
|
21
src/app/browser/window/tab/item/client/driver.rs
Normal file
21
src/app/browser/window/tab/item/client/driver.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
mod gemini;
|
||||||
|
|
||||||
|
use super::{Feature, Page};
|
||||||
|
use gemini::Gemini;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Different protocols implementation
|
||||||
|
pub struct Driver {
|
||||||
|
pub gemini: Gemini,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Driver {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Build new `Self`
|
||||||
|
pub fn build(page: &Rc<Page>) -> Self {
|
||||||
|
Driver {
|
||||||
|
gemini: Gemini::init(page),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
470
src/app/browser/window/tab/item/client/driver/gemini.rs
Normal file
470
src/app/browser/window/tab/item/client/driver/gemini.rs
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
use crate::tool::now;
|
||||||
|
|
||||||
|
use super::super::super::page::status::Status as PageStatus; // @TODO
|
||||||
|
|
||||||
|
use super::{Feature, Page};
|
||||||
|
use gtk::glib::GString;
|
||||||
|
use gtk::prelude::{EditableExt, FileExt};
|
||||||
|
use gtk::{
|
||||||
|
gdk::Texture,
|
||||||
|
gdk_pixbuf::Pixbuf,
|
||||||
|
gio::{Cancellable, SocketClientEvent},
|
||||||
|
glib::{Priority, Uri},
|
||||||
|
prelude::{EntryExt, SocketClientExt},
|
||||||
|
};
|
||||||
|
use std::{path::MAIN_SEPARATOR, rc::Rc, time::Duration};
|
||||||
|
|
||||||
|
/// Multi-protocol client API for `Page` object
|
||||||
|
pub struct Gemini {
|
||||||
|
client: Rc<ggemini::Client>,
|
||||||
|
page: Rc<Page>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gemini {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new `Self`
|
||||||
|
pub fn init(page: &Rc<Page>) -> Self {
|
||||||
|
// Init supported protocol libraries
|
||||||
|
let client = Rc::new(ggemini::Client::new());
|
||||||
|
|
||||||
|
// Listen for [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html) updates
|
||||||
|
client.socket.connect_event({
|
||||||
|
let page = page.clone();
|
||||||
|
move |_, event, _, _| {
|
||||||
|
page.navigation
|
||||||
|
.request
|
||||||
|
.widget
|
||||||
|
.entry
|
||||||
|
.set_progress_fraction(match event {
|
||||||
|
SocketClientEvent::Resolving => 0.1,
|
||||||
|
SocketClientEvent::Resolved => 0.2,
|
||||||
|
SocketClientEvent::Connecting => 0.3,
|
||||||
|
SocketClientEvent::Connected => 0.4,
|
||||||
|
SocketClientEvent::ProxyNegotiating => 0.5,
|
||||||
|
SocketClientEvent::ProxyNegotiated => 0.6,
|
||||||
|
// * `TlsHandshaking` | `TlsHandshaked` has effect only for guest connections!
|
||||||
|
SocketClientEvent::TlsHandshaking => 0.7,
|
||||||
|
SocketClientEvent::TlsHandshaked => 0.8,
|
||||||
|
SocketClientEvent::Complete => 0.9,
|
||||||
|
_ => todo!(), // alert on API change
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
page: page.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
pub fn handle(&self, uri: Uri, feature: Feature, cancellable: Cancellable, is_history: bool) {
|
||||||
|
use ggemini::client::connection::response::{data::Text, meta::Status};
|
||||||
|
|
||||||
|
// Move focus out from navigation entry
|
||||||
|
self.page
|
||||||
|
.browser_action
|
||||||
|
.escape
|
||||||
|
.activate_stateful_once(Some(self.page.id.as_str().into()));
|
||||||
|
|
||||||
|
// Initially disable find action
|
||||||
|
self.page
|
||||||
|
.window_action
|
||||||
|
.find
|
||||||
|
.simple_action
|
||||||
|
.set_enabled(false);
|
||||||
|
|
||||||
|
// Reset widgets
|
||||||
|
self.page.search.unset();
|
||||||
|
self.page.input.unset();
|
||||||
|
self.page
|
||||||
|
.status
|
||||||
|
.replace(PageStatus::Loading { time: now() });
|
||||||
|
self.page.title.replace("Loading..".into());
|
||||||
|
self.page
|
||||||
|
.browser_action
|
||||||
|
.update
|
||||||
|
.activate(Some(&self.page.id));
|
||||||
|
|
||||||
|
if is_history {
|
||||||
|
snap_history(&self.page, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.client.request_async(
|
||||||
|
ggemini::client::Request::gemini(uri.clone()),
|
||||||
|
Priority::DEFAULT,
|
||||||
|
cancellable.clone(),
|
||||||
|
// Search for user certificate match request
|
||||||
|
// * @TODO this feature does not support multi-protocol yet
|
||||||
|
match self
|
||||||
|
.page
|
||||||
|
.profile
|
||||||
|
.identity
|
||||||
|
.gemini
|
||||||
|
.match_scope(&uri.to_string())
|
||||||
|
{
|
||||||
|
Some(identity) => match identity.to_tls_certificate() {
|
||||||
|
Ok(certificate) => Some(certificate),
|
||||||
|
Err(_) => panic!(), // unexpected
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let uri = uri.clone();
|
||||||
|
let page = self.page.clone();
|
||||||
|
move |result| match result {
|
||||||
|
Ok(response) => {
|
||||||
|
match response.meta.status {
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected
|
||||||
|
Status::Input => {
|
||||||
|
let title = match response.meta.data {
|
||||||
|
Some(data) => data.to_string(),
|
||||||
|
None => Status::Input.to_string(),
|
||||||
|
};
|
||||||
|
page.input.set_new_response(
|
||||||
|
page.tab_action.clone(),
|
||||||
|
uri,
|
||||||
|
Some(&title),
|
||||||
|
Some(1024),
|
||||||
|
);
|
||||||
|
page.title.replace(title.into()); // @TODO
|
||||||
|
page.status.replace(PageStatus::Input { time: now() });
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
Status::SensitiveInput => {
|
||||||
|
let title = match response.meta.data {
|
||||||
|
Some(data) => data.to_string(),
|
||||||
|
None => Status::Input.to_string(),
|
||||||
|
};
|
||||||
|
page.input.set_new_sensitive(
|
||||||
|
page.tab_action.clone(),
|
||||||
|
uri,
|
||||||
|
Some(&title),
|
||||||
|
Some(1024),
|
||||||
|
);
|
||||||
|
page.title.replace(title.into()); // @TODO
|
||||||
|
page.status.replace(PageStatus::Input { time: now() });
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-20
|
||||||
|
Status::Success => match feature {
|
||||||
|
Feature::Download => {
|
||||||
|
// Init download widget
|
||||||
|
let status = page.content.to_status_download(
|
||||||
|
uri_to_title(&uri).trim_matches(MAIN_SEPARATOR), // grab default filename from base URI,
|
||||||
|
// format FS entities
|
||||||
|
&cancellable,
|
||||||
|
{
|
||||||
|
let cancellable = cancellable.clone();
|
||||||
|
let stream = response.connection.stream();
|
||||||
|
move |file, action| {
|
||||||
|
match file.replace(
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
gtk::gio::FileCreateFlags::NONE,
|
||||||
|
Some(&cancellable),
|
||||||
|
) {
|
||||||
|
Ok(file_output_stream) => {
|
||||||
|
// Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
||||||
|
// to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
|
||||||
|
// show bytes count in loading widget, validate max size for incoming data
|
||||||
|
// * no dependency of Gemini library here, feel free to use any other `IOStream` processor
|
||||||
|
ggemini::gio::file_output_stream::move_all_from_stream_async(
|
||||||
|
stream.clone(),
|
||||||
|
file_output_stream,
|
||||||
|
cancellable.clone(),
|
||||||
|
Priority::DEFAULT,
|
||||||
|
(
|
||||||
|
0x100000, // 1M bytes per chunk
|
||||||
|
None, // unlimited
|
||||||
|
0, // initial totals
|
||||||
|
),
|
||||||
|
(
|
||||||
|
// on chunk
|
||||||
|
{
|
||||||
|
let action = action.clone();
|
||||||
|
move |_, total| {
|
||||||
|
action.update.activate(&format!(
|
||||||
|
"Received {}...",
|
||||||
|
crate::tool::format_bytes(total)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// on complete
|
||||||
|
{
|
||||||
|
let action = action.clone();
|
||||||
|
move |result| match result {
|
||||||
|
Ok((_, total)) => {
|
||||||
|
action.complete.activate(&format!(
|
||||||
|
"Saved to {} ({total} bytes total)",
|
||||||
|
file.parse_name()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
action.cancel.activate(&e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => action.cancel.activate(&e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
page.status.replace(PageStatus::Success { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
},
|
||||||
|
_ => match response.meta.mime {
|
||||||
|
Some(mime) => match mime.as_str() {
|
||||||
|
"text/gemini" => Text::from_stream_async(
|
||||||
|
response.connection.stream(),
|
||||||
|
Priority::DEFAULT,
|
||||||
|
cancellable.clone(),
|
||||||
|
move |result| match result {
|
||||||
|
Ok(text) => {
|
||||||
|
/* @TODO refactor features
|
||||||
|
let widget = if is_source_request {
|
||||||
|
page.content.to_text_source(&data)
|
||||||
|
} else {
|
||||||
|
page.content.to_text_gemini(&uri, &data)
|
||||||
|
};*/
|
||||||
|
|
||||||
|
let widget = page
|
||||||
|
.content
|
||||||
|
.to_text_gemini(&uri, &text.to_string());
|
||||||
|
|
||||||
|
// Connect `TextView` widget, update `search` model
|
||||||
|
page.search.set(Some(widget.text_view));
|
||||||
|
|
||||||
|
// Update page meta
|
||||||
|
page.status
|
||||||
|
.replace(PageStatus::Success { time: now() });
|
||||||
|
page.title.replace(match widget.meta.title {
|
||||||
|
Some(title) => title.into(), // @TODO
|
||||||
|
None => uri_to_title(&uri),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deactivate progress fraction
|
||||||
|
page.navigation.request.widget.entry.set_progress_fraction(0.0);
|
||||||
|
|
||||||
|
// Update window components
|
||||||
|
page.window_action
|
||||||
|
.find
|
||||||
|
.simple_action
|
||||||
|
.set_enabled(true);
|
||||||
|
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let status = page.content.to_status_failure();
|
||||||
|
status.set_description(Some(&e.to_string()));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Failure { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"image/png" | "image/gif" | "image/jpeg" | "image/webp" => {
|
||||||
|
// Final image size unknown, show loading widget
|
||||||
|
let status = page.content.to_status_loading(
|
||||||
|
Some(Duration::from_secs(1)), // show if download time > 1 second
|
||||||
|
);
|
||||||
|
|
||||||
|
// Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
||||||
|
// to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
|
||||||
|
// show bytes count in loading widget, validate max size for incoming data
|
||||||
|
// * no dependency of Gemini library here, feel free to use any other `IOStream` processor
|
||||||
|
ggemini::gio::memory_input_stream::from_stream_async(
|
||||||
|
response.connection.stream(),
|
||||||
|
cancellable.clone(),
|
||||||
|
Priority::DEFAULT,
|
||||||
|
0x400, // 1024 bytes per chunk, optional step for images download tracking
|
||||||
|
0xA00000, // 10M bytes max to prevent memory overflow if server play with promises
|
||||||
|
move |_, total| {
|
||||||
|
// Update loading progress
|
||||||
|
status.set_description(Some(&format!("Download: {total} bytes")));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let page = page.clone();
|
||||||
|
move |result| match result {
|
||||||
|
Ok((memory_input_stream, _)) => {
|
||||||
|
Pixbuf::from_stream_async(
|
||||||
|
&memory_input_stream,
|
||||||
|
Some(&cancellable),
|
||||||
|
move |result| {
|
||||||
|
// Process buffer data
|
||||||
|
match result {
|
||||||
|
Ok(buffer) => {
|
||||||
|
page.status
|
||||||
|
.replace(PageStatus::Success { time: now() });
|
||||||
|
page.title.replace(uri_to_title(&uri));
|
||||||
|
page.content
|
||||||
|
.to_image(&Texture::for_pixbuf(&buffer));
|
||||||
|
page.browser_action
|
||||||
|
.update
|
||||||
|
.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let status = page.content.to_status_failure();
|
||||||
|
status.set_description(Some(e.message()));
|
||||||
|
|
||||||
|
page.status
|
||||||
|
.replace(PageStatus::Failure { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let status = page.content.to_status_failure();
|
||||||
|
status.set_description(Some(&e.to_string()));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Failure { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
mime => {
|
||||||
|
let status = page
|
||||||
|
.content
|
||||||
|
.to_status_mime(&mime, Some((&page.tab_action, &uri)));
|
||||||
|
status.set_description(Some(&format!("Content type `{mime}` yet not supported")));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Failure { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let status = page.content.to_status_failure();
|
||||||
|
status.set_description(Some("MIME type not found"));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Failure { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
|
||||||
|
Status::Redirect => todo!(),
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
|
||||||
|
Status::PermanentRedirect => {
|
||||||
|
page.navigation
|
||||||
|
.request
|
||||||
|
.widget
|
||||||
|
.entry
|
||||||
|
.set_text(&uri.to_string());
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
|
||||||
|
Status::CertificateRequest => {
|
||||||
|
let status = page.content.to_status_identity();
|
||||||
|
status.set_description(Some(&match response.meta.data {
|
||||||
|
Some(data) => data.to_string(),
|
||||||
|
None => Status::CertificateRequest.to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Success { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
|
||||||
|
Status::CertificateUnauthorized => {
|
||||||
|
let status = page.content.to_status_identity();
|
||||||
|
status.set_description(Some(&match response.meta.data {
|
||||||
|
Some(data) => data.to_string(),
|
||||||
|
None => Status::CertificateUnauthorized.to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Success { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
|
||||||
|
Status::CertificateInvalid => {
|
||||||
|
let status = page.content.to_status_identity();
|
||||||
|
status.set_description(Some(&match response.meta.data {
|
||||||
|
Some(data) => data.to_string(),
|
||||||
|
None => Status::CertificateInvalid.to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Success { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
status => {
|
||||||
|
let _status = page.content.to_status_failure();
|
||||||
|
_status.set_description(Some(&format!("Undefined status code `{status}`")));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Failure { time: now() });
|
||||||
|
page.title.replace(_status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let status = page.content.to_status_failure();
|
||||||
|
status.set_description(Some(&e.to_string()));
|
||||||
|
|
||||||
|
page.status.replace(PageStatus::Failure { time: now() });
|
||||||
|
page.title.replace(status.title());
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function, extract readable title from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
||||||
|
/// * useful as common placeholder when page title could not be detected
|
||||||
|
/// * this feature may be improved and moved outside @TODO
|
||||||
|
fn uri_to_title(uri: &Uri) -> GString {
|
||||||
|
let path = uri.path();
|
||||||
|
if path.split('/').last().unwrap_or_default().is_empty() {
|
||||||
|
match uri.host() {
|
||||||
|
Some(host) => host,
|
||||||
|
None => "Untitled".into(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make new history record in related components
|
||||||
|
/// * optional [Uri](https://docs.gtk.org/glib/struct.Uri.html) reference wanted only for performance reasons, to not parse it twice
|
||||||
|
fn snap_history(page: &Page, uri: Option<&Uri>) {
|
||||||
|
let request = page.navigation.request.widget.entry.text();
|
||||||
|
|
||||||
|
// Add new record into the global memory index (used in global menu)
|
||||||
|
// * if the `Uri` is `None`, try parse it from `request`
|
||||||
|
match uri {
|
||||||
|
Some(uri) => page.profile.history.memory.request.set(uri.clone()),
|
||||||
|
None => {
|
||||||
|
// this case especially useful for some routes that contain redirects
|
||||||
|
// maybe some parental optimization wanted @TODO
|
||||||
|
if let Some(uri) = page.navigation.request.as_uri() {
|
||||||
|
page.profile.history.memory.request.set(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new record into the page navigation history
|
||||||
|
if match page.navigation.history.current() {
|
||||||
|
Some(current) => current != request, // apply additional filters
|
||||||
|
None => true,
|
||||||
|
} {
|
||||||
|
page.navigation.history.add(request, true)
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,7 @@ impl Feature {
|
|||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|
||||||
|
/* @TODO not in use
|
||||||
/// Get `Self` as prefix
|
/// Get `Self` as prefix
|
||||||
pub fn as_prefix(&self) -> Option<&str> {
|
pub fn as_prefix(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
@ -37,5 +38,5 @@ impl Feature {
|
|||||||
Self::Source => Some(SOURCE),
|
Self::Source => Some(SOURCE),
|
||||||
Self::Default => None,
|
Self::Default => None,
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
@ -1,14 +1,12 @@
|
|||||||
mod client; // @TODO complete new router implementation
|
|
||||||
mod content;
|
mod content;
|
||||||
mod database;
|
mod database;
|
||||||
mod error;
|
mod error;
|
||||||
mod input;
|
mod input;
|
||||||
mod navigation;
|
mod navigation;
|
||||||
mod search;
|
mod search;
|
||||||
mod status;
|
pub mod status;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
|
||||||
use client::Client;
|
|
||||||
use content::Content;
|
use content::Content;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use input::Input;
|
use input::Input;
|
||||||
@ -21,25 +19,22 @@ use super::{Action as TabAction, BrowserAction, Profile, WindowAction};
|
|||||||
use crate::tool::now;
|
use crate::tool::now;
|
||||||
|
|
||||||
use gtk::{
|
use gtk::{
|
||||||
gdk::Texture,
|
glib::GString,
|
||||||
gdk_pixbuf::Pixbuf,
|
prelude::{EditableExt, EntryExt},
|
||||||
glib::{GString, Priority, Uri},
|
|
||||||
prelude::{EditableExt, FileExt},
|
|
||||||
};
|
};
|
||||||
use sqlite::Transaction;
|
use sqlite::Transaction;
|
||||||
use std::{cell::RefCell, path::MAIN_SEPARATOR, rc::Rc, time::Duration};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
id: Rc<GString>,
|
pub id: Rc<GString>,
|
||||||
profile: Rc<Profile>,
|
pub profile: Rc<Profile>,
|
||||||
status: Rc<RefCell<Status>>,
|
pub status: Rc<RefCell<Status>>,
|
||||||
title: Rc<RefCell<GString>>,
|
pub title: Rc<RefCell<GString>>,
|
||||||
// Actions
|
// Actions
|
||||||
browser_action: Rc<BrowserAction>,
|
pub browser_action: Rc<BrowserAction>,
|
||||||
tab_action: Rc<TabAction>,
|
pub tab_action: Rc<TabAction>,
|
||||||
window_action: Rc<WindowAction>,
|
pub window_action: Rc<WindowAction>,
|
||||||
// Components
|
// Components
|
||||||
pub client: Rc<Client>,
|
|
||||||
pub content: Rc<Content>,
|
pub content: Rc<Content>,
|
||||||
pub search: Rc<Search>,
|
pub search: Rc<Search>,
|
||||||
pub input: Rc<Input>,
|
pub input: Rc<Input>,
|
||||||
@ -81,16 +76,6 @@ impl Page {
|
|||||||
|
|
||||||
let status = Rc::new(RefCell::new(Status::New { time: now() }));
|
let status = Rc::new(RefCell::new(Status::New { time: now() }));
|
||||||
|
|
||||||
let client = Rc::new(Client::init(profile, {
|
|
||||||
let id = id.clone();
|
|
||||||
let status = status.clone();
|
|
||||||
let update = browser_action.update.clone();
|
|
||||||
move |this| {
|
|
||||||
status.replace(Status::Client(this));
|
|
||||||
update.activate(Some(&id));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Done
|
// Done
|
||||||
Self {
|
Self {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
@ -101,7 +86,6 @@ impl Page {
|
|||||||
tab_action: tab_action.clone(),
|
tab_action: tab_action.clone(),
|
||||||
window_action: window_action.clone(),
|
window_action: window_action.clone(),
|
||||||
// Components
|
// Components
|
||||||
client,
|
|
||||||
status,
|
status,
|
||||||
content,
|
content,
|
||||||
search,
|
search,
|
||||||
@ -140,10 +124,8 @@ impl Page {
|
|||||||
|
|
||||||
/// Update `Self` witch children components
|
/// Update `Self` witch children components
|
||||||
pub fn update(&self) {
|
pub fn update(&self) {
|
||||||
// Update components
|
// Update children components
|
||||||
self.navigation
|
self.navigation.update();
|
||||||
.update(self.status.borrow().to_progress_fraction());
|
|
||||||
// @TODO self.content.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cleanup session for `Self`
|
/// Cleanup session for `Self`
|
||||||
@ -188,7 +170,9 @@ impl Page {
|
|||||||
self.navigation.restore(transaction, &record.id)?;
|
self.navigation.restore(transaction, &record.id)?;
|
||||||
// Make initial page history snap using `navigation` values restored
|
// Make initial page history snap using `navigation` values restored
|
||||||
// * just to have back/forward navigation ability
|
// * just to have back/forward navigation ability
|
||||||
snap_history(&self.profile, &self.navigation, None);
|
if let Some(uri) = self.navigation.request.as_uri() {
|
||||||
|
self.profile.history.memory.request.set(uri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e.to_string()),
|
Err(e) => return Err(e.to_string()),
|
||||||
@ -232,72 +216,11 @@ impl Page {
|
|||||||
|
|
||||||
/// Get `Self` loading status
|
/// Get `Self` loading status
|
||||||
pub fn is_loading(&self) -> bool {
|
pub fn is_loading(&self) -> bool {
|
||||||
match self.status.borrow().to_progress_fraction() {
|
let progress_fraction = self.navigation.request.widget.entry.progress_fraction();
|
||||||
Some(progress_fraction) => progress_fraction < 1.0,
|
progress_fraction > 0.0 && progress_fraction < 1.0
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigate home URL (parsed from current navigation entry)
|
|
||||||
/// * this method create new history record in memory as defined in `action_page_open` action
|
|
||||||
pub fn home(page: &Rc<Page>) {
|
|
||||||
if let Some(text) = page.navigation.home.url() {
|
|
||||||
page.navigation.request.widget.entry.set_text(&text);
|
|
||||||
load(page, None, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigate back in history
|
|
||||||
/// * this method does not create new history record in memory
|
|
||||||
pub fn history_back(page: &Rc<Page>) {
|
|
||||||
if let Some(text) = page.navigation.history.back(true) {
|
|
||||||
page.navigation.request.widget.entry.set_text(&text);
|
|
||||||
load(page, None, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigate forward in history
|
|
||||||
/// * this method does not create new history record in memory
|
|
||||||
pub fn history_forward(page: &Rc<Page>) {
|
|
||||||
if let Some(text) = page.navigation.history.forward(true) {
|
|
||||||
page.navigation.request.widget.entry.set_text(&text);
|
|
||||||
load(page, None, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Page load function with recursive redirection support
|
|
||||||
pub fn load(page: &Rc<Page>, request: Option<&str>, is_history: bool) {
|
|
||||||
// Move focus out from navigation entry
|
|
||||||
page.browser_action
|
|
||||||
.escape
|
|
||||||
.activate_stateful_once(Some(page.id.as_str().into()));
|
|
||||||
|
|
||||||
// Initially disable find action
|
|
||||||
page.window_action.find.simple_action.set_enabled(false);
|
|
||||||
|
|
||||||
// Reset widgets
|
|
||||||
page.search.unset();
|
|
||||||
page.input.unset();
|
|
||||||
page.status.replace(Status::Loading { time: now() });
|
|
||||||
page.title.replace("Loading..".into());
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
|
|
||||||
if is_history {
|
|
||||||
snap_history(&page.profile, &page.navigation, None); // @TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = match request {
|
|
||||||
Some(query) => query,
|
|
||||||
None => &page.navigation.request.widget.entry.text(),
|
|
||||||
};
|
|
||||||
|
|
||||||
page.client.request(query, {
|
|
||||||
let page = page.clone();
|
|
||||||
move |response| handle(&page, response)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools
|
// Tools
|
||||||
|
|
||||||
pub fn migrate(tx: &Transaction) -> Result<(), String> {
|
pub fn migrate(tx: &Transaction) -> Result<(), String> {
|
||||||
@ -312,350 +235,3 @@ pub fn migrate(tx: &Transaction) -> Result<(), String> {
|
|||||||
// Success
|
// Success
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function, extract readable title from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
|
||||||
/// * useful as common placeholder when page title could not be detected
|
|
||||||
/// * this feature may be improved and moved outside @TODO
|
|
||||||
fn uri_to_title(uri: &Uri) -> GString {
|
|
||||||
let path = uri.path();
|
|
||||||
if path.split('/').last().unwrap_or_default().is_empty() {
|
|
||||||
match uri.host() {
|
|
||||||
Some(host) => host,
|
|
||||||
None => "Untitled".into(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make new history record in related components
|
|
||||||
/// * optional [Uri](https://docs.gtk.org/glib/struct.Uri.html) reference wanted only for performance reasons, to not parse it twice
|
|
||||||
fn snap_history(profile: &Profile, navigation: &Navigation, uri: Option<&Uri>) {
|
|
||||||
let request = navigation.request.widget.entry.text();
|
|
||||||
|
|
||||||
// Add new record into the global memory index (used in global menu)
|
|
||||||
// * if the `Uri` is `None`, try parse it from `request`
|
|
||||||
match uri {
|
|
||||||
Some(uri) => profile.history.memory.request.set(uri.clone()),
|
|
||||||
None => {
|
|
||||||
// this case especially useful for some routes that contain redirects
|
|
||||||
// maybe some parental optimization wanted @TODO
|
|
||||||
if let Some(uri) = navigation.request.uri() {
|
|
||||||
profile.history.memory.request.set(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new record into the page navigation history
|
|
||||||
if match navigation.history.current() {
|
|
||||||
Some(current) => current != request, // apply additional filters
|
|
||||||
None => true,
|
|
||||||
} {
|
|
||||||
navigation.history.add(request, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Response handler for `Page`
|
|
||||||
/// * may call itself on Titan response
|
|
||||||
fn handle(page: &Rc<Page>, response: client::Response) {
|
|
||||||
use client::{
|
|
||||||
response::{text::Text, Certificate, Failure, Input, Redirect},
|
|
||||||
Response,
|
|
||||||
};
|
|
||||||
match response {
|
|
||||||
Response::Certificate(this) => match this {
|
|
||||||
Certificate::Invalid {
|
|
||||||
title: certificate_title,
|
|
||||||
}
|
|
||||||
| Certificate::Request {
|
|
||||||
title: certificate_title,
|
|
||||||
}
|
|
||||||
| Certificate::Unauthorized {
|
|
||||||
title: certificate_title,
|
|
||||||
} => {
|
|
||||||
// Update widget
|
|
||||||
let status = page.content.to_status_identity();
|
|
||||||
status.set_description(Some(&certificate_title));
|
|
||||||
|
|
||||||
// Update meta
|
|
||||||
page.status.replace(Status::Success { time: now() });
|
|
||||||
page.title.replace(status.title());
|
|
||||||
|
|
||||||
// Update window
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Response::Failure(this) => match this {
|
|
||||||
Failure::Status { message } | Failure::Error { message } => {
|
|
||||||
// Update widget
|
|
||||||
let status = page.content.to_status_failure();
|
|
||||||
status.set_description(Some(&message));
|
|
||||||
|
|
||||||
// Update meta
|
|
||||||
page.status.replace(Status::Failure { time: now() });
|
|
||||||
page.title.replace(status.title());
|
|
||||||
|
|
||||||
// Update window
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
}
|
|
||||||
Failure::Mime {
|
|
||||||
base,
|
|
||||||
mime,
|
|
||||||
message,
|
|
||||||
} => {
|
|
||||||
// Update widget
|
|
||||||
let status = page
|
|
||||||
.content
|
|
||||||
.to_status_mime(&mime, Some((&page.tab_action, &base)));
|
|
||||||
status.set_description(Some(&message));
|
|
||||||
|
|
||||||
// Update meta
|
|
||||||
page.status.replace(Status::Failure { time: now() });
|
|
||||||
page.title.replace(status.title());
|
|
||||||
|
|
||||||
// Update window
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Response::Input(this) => {
|
|
||||||
match this {
|
|
||||||
Input::Response {
|
|
||||||
base,
|
|
||||||
title: response_title,
|
|
||||||
} => {
|
|
||||||
page.input.set_new_response(
|
|
||||||
page.tab_action.clone(),
|
|
||||||
base,
|
|
||||||
Some(&response_title),
|
|
||||||
Some(1024),
|
|
||||||
);
|
|
||||||
page.title.replace(response_title);
|
|
||||||
}
|
|
||||||
Input::Sensitive {
|
|
||||||
base,
|
|
||||||
title: response_title,
|
|
||||||
} => {
|
|
||||||
page.input.set_new_sensitive(
|
|
||||||
page.tab_action.clone(),
|
|
||||||
base,
|
|
||||||
Some(&response_title),
|
|
||||||
Some(1024),
|
|
||||||
);
|
|
||||||
page.title.replace(response_title);
|
|
||||||
}
|
|
||||||
Input::Titan(this) => {
|
|
||||||
page.input.set_new_titan(this, {
|
|
||||||
let page = page.clone();
|
|
||||||
move |result| match result {
|
|
||||||
Ok(response) => handle(&page, response),
|
|
||||||
Err(e) => {
|
|
||||||
let status = page.content.to_status_failure();
|
|
||||||
//status.set_description(Some(&e.to_string()));
|
|
||||||
// @TODO
|
|
||||||
|
|
||||||
page.status.replace(Status::Failure { time: now() });
|
|
||||||
page.title.replace(status.title());
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
page.title.replace("Titan input".into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
page.status.replace(Status::Input { time: now() });
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
}
|
|
||||||
Response::Redirect(this) => match this {
|
|
||||||
Redirect::Background(uri) => load(&page, Some(&uri.to_string()), false),
|
|
||||||
Redirect::Foreground(uri) => {
|
|
||||||
page.navigation
|
|
||||||
.request
|
|
||||||
.widget
|
|
||||||
.entry
|
|
||||||
.set_text(&uri.to_string());
|
|
||||||
load(&page, Some(&uri.to_string()), false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Response::Text(this) => match this {
|
|
||||||
Text::Gemini { base, data } => {
|
|
||||||
/* @TODO refactor features
|
|
||||||
let widget = if is_source_request {
|
|
||||||
page.content.to_text_source(&data)
|
|
||||||
} else {
|
|
||||||
page.content.to_text_gemini(&base, &data)
|
|
||||||
};*/
|
|
||||||
|
|
||||||
let widget = page.content.to_text_gemini(&base, &data);
|
|
||||||
|
|
||||||
// Connect `TextView` widget, update `search` model
|
|
||||||
page.search.set(Some(widget.text_view));
|
|
||||||
|
|
||||||
// Update page meta
|
|
||||||
page.status.replace(Status::Success { time: now() });
|
|
||||||
page.title.replace(match widget.meta.title {
|
|
||||||
Some(title) => title.into(), // @TODO
|
|
||||||
None => uri_to_title(&base),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update window components
|
|
||||||
page.window_action.find.simple_action.set_enabled(true);
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
}
|
|
||||||
Text::Plain { data } => todo!(),
|
|
||||||
},
|
|
||||||
Response::Download {
|
|
||||||
base,
|
|
||||||
cancellable,
|
|
||||||
stream,
|
|
||||||
} => {
|
|
||||||
// Init download widget
|
|
||||||
let status = page.content.to_status_download(
|
|
||||||
uri_to_title(&base).trim_matches(MAIN_SEPARATOR), // grab default filename from base URI,
|
|
||||||
// format FS entities
|
|
||||||
&cancellable,
|
|
||||||
{
|
|
||||||
let cancellable = cancellable.clone();
|
|
||||||
let stream = stream.clone();
|
|
||||||
move |file, action| {
|
|
||||||
match file.replace(
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
gtk::gio::FileCreateFlags::NONE,
|
|
||||||
Some(&cancellable),
|
|
||||||
) {
|
|
||||||
Ok(file_output_stream) => {
|
|
||||||
// Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
// to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
|
|
||||||
// show bytes count in loading widget, validate max size for incoming data
|
|
||||||
// * no dependency of Gemini library here, feel free to use any other `IOStream` processor
|
|
||||||
ggemini::gio::file_output_stream::move_all_from_stream_async(
|
|
||||||
stream.clone(),
|
|
||||||
file_output_stream,
|
|
||||||
cancellable.clone(),
|
|
||||||
Priority::DEFAULT,
|
|
||||||
(
|
|
||||||
0x100000, // 1M bytes per chunk
|
|
||||||
None, // unlimited
|
|
||||||
0, // initial totals
|
|
||||||
),
|
|
||||||
(
|
|
||||||
// on chunk
|
|
||||||
{
|
|
||||||
let action = action.clone();
|
|
||||||
move |_, total| {
|
|
||||||
action.update.activate(&format!(
|
|
||||||
"Received {}...",
|
|
||||||
crate::tool::format_bytes(total)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// on complete
|
|
||||||
{
|
|
||||||
let action = action.clone();
|
|
||||||
move |result| match result {
|
|
||||||
Ok((_, total)) => {
|
|
||||||
action.complete.activate(&format!(
|
|
||||||
"Saved to {} ({total} bytes total)",
|
|
||||||
file.parse_name()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Err(e) => action.cancel.activate(&e.to_string()),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(e) => action.cancel.activate(&e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update meta
|
|
||||||
page.status.replace(Status::Success { time: now() });
|
|
||||||
page.title.replace(status.title());
|
|
||||||
|
|
||||||
// Update window
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
}
|
|
||||||
Response::Stream {
|
|
||||||
base,
|
|
||||||
mime,
|
|
||||||
stream,
|
|
||||||
cancellable,
|
|
||||||
} => match mime.as_str() {
|
|
||||||
// @TODO use client-side const or enum?
|
|
||||||
"image/png" | "image/gif" | "image/jpeg" | "image/webp" => {
|
|
||||||
// Final image size unknown, show loading widget
|
|
||||||
let status = page.content.to_status_loading(
|
|
||||||
Some(Duration::from_secs(1)), // show if download time > 1 second
|
|
||||||
);
|
|
||||||
|
|
||||||
// Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
// to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
|
|
||||||
// show bytes count in loading widget, validate max size for incoming data
|
|
||||||
// * no dependency of Gemini library here, feel free to use any other `IOStream` processor
|
|
||||||
ggemini::gio::memory_input_stream::from_stream_async(
|
|
||||||
stream,
|
|
||||||
cancellable.clone(),
|
|
||||||
Priority::DEFAULT,
|
|
||||||
0x400, // 1024 bytes per chunk, optional step for images download tracking
|
|
||||||
0xA00000, // 10M bytes max to prevent memory overflow if server play with promises
|
|
||||||
move |_, total| {
|
|
||||||
// Update loading progress
|
|
||||||
status.set_description(Some(&format!("Download: {total} bytes")));
|
|
||||||
},
|
|
||||||
{
|
|
||||||
let page = page.clone();
|
|
||||||
move |result| match result {
|
|
||||||
Ok((memory_input_stream, _)) => {
|
|
||||||
Pixbuf::from_stream_async(
|
|
||||||
&memory_input_stream,
|
|
||||||
Some(&cancellable),
|
|
||||||
move |result| {
|
|
||||||
// Process buffer data
|
|
||||||
match result {
|
|
||||||
Ok(buffer) => {
|
|
||||||
// Update page meta
|
|
||||||
page.status
|
|
||||||
.replace(Status::Success { time: now() });
|
|
||||||
page.title.replace(uri_to_title(&base));
|
|
||||||
|
|
||||||
// Update page content
|
|
||||||
page.content
|
|
||||||
.to_image(&Texture::for_pixbuf(&buffer));
|
|
||||||
|
|
||||||
// Update window components
|
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Update widget
|
|
||||||
let status = page.content.to_status_failure();
|
|
||||||
status.set_description(Some(e.message()));
|
|
||||||
|
|
||||||
// Update meta
|
|
||||||
page.status
|
|
||||||
.replace(Status::Failure { time: now() });
|
|
||||||
page.title.replace(status.title());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Update widget
|
|
||||||
let status = page.content.to_status_failure();
|
|
||||||
status.set_description(Some(&e.to_string()));
|
|
||||||
|
|
||||||
// Update meta
|
|
||||||
page.status.replace(Status::Failure { time: now() });
|
|
||||||
page.title.replace(status.title());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => todo!(), // unexpected
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
pub mod request;
|
|
||||||
pub mod response;
|
|
||||||
pub mod status;
|
|
||||||
|
|
||||||
// Children dependencies
|
|
||||||
pub use request::Request;
|
|
||||||
pub use response::Response;
|
|
||||||
pub use status::Status;
|
|
||||||
|
|
||||||
// Global dependencies
|
|
||||||
use crate::{tool::now, Profile};
|
|
||||||
use gtk::{
|
|
||||||
gio::{Cancellable, SocketClientEvent},
|
|
||||||
prelude::{CancellableExt, SocketClientExt},
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
cell::{Cell, RefCell},
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Multi-protocol client API for `Page` object
|
|
||||||
pub struct Client {
|
|
||||||
cancellable: Cell<Cancellable>,
|
|
||||||
status: Rc<RefCell<Status>>,
|
|
||||||
/// Profile reference required for Gemini protocol auth (match scope)
|
|
||||||
profile: Rc<Profile>,
|
|
||||||
/// Supported clients
|
|
||||||
/// * gemini driver should be initiated once (on page object init)
|
|
||||||
/// to process all it connection features properly
|
|
||||||
gemini: Rc<ggemini::Client>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self`
|
|
||||||
pub fn init(profile: &Rc<Profile>, callback: impl Fn(Status) + 'static) -> Self {
|
|
||||||
use status::Gemini;
|
|
||||||
// Init supported protocol libraries
|
|
||||||
let gemini = Rc::new(ggemini::Client::new());
|
|
||||||
|
|
||||||
// Retransmit gemini [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html) updates
|
|
||||||
gemini.socket.connect_event(move |_, event, _, _| {
|
|
||||||
callback(Status::Gemini(match event {
|
|
||||||
SocketClientEvent::Resolving => Gemini::Resolving { time: now() },
|
|
||||||
SocketClientEvent::Resolved => Gemini::Resolved { time: now() },
|
|
||||||
SocketClientEvent::Connecting => Gemini::Connecting { time: now() },
|
|
||||||
SocketClientEvent::Connected => Gemini::Connected { time: now() },
|
|
||||||
SocketClientEvent::ProxyNegotiating => Gemini::ProxyNegotiating { time: now() },
|
|
||||||
SocketClientEvent::ProxyNegotiated => Gemini::ProxyNegotiated { time: now() },
|
|
||||||
// * `TlsHandshaking` | `TlsHandshaked` has effect only for guest connections!
|
|
||||||
SocketClientEvent::TlsHandshaking => Gemini::TlsHandshaking { time: now() },
|
|
||||||
SocketClientEvent::TlsHandshaked => Gemini::TlsHandshaked { time: now() },
|
|
||||||
SocketClientEvent::Complete => Gemini::Complete { time: now() },
|
|
||||||
_ => todo!(), // alert on API change
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
cancellable: Cell::new(Cancellable::new()),
|
|
||||||
status: Rc::new(RefCell::new(Status::Cancellable { time: now() })), // e.g. "ready to use"
|
|
||||||
profile: profile.clone(),
|
|
||||||
gemini,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
/// Begin new request
|
|
||||||
/// * the `query` as string, to support system routes (e.g. `source:` prefix)
|
|
||||||
pub fn request(&self, query: &str, callback: impl FnOnce(Response) + 'static) {
|
|
||||||
self.status.replace(Status::Request {
|
|
||||||
time: now(),
|
|
||||||
value: query.to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
use request::Error;
|
|
||||||
use response::{Failure, Redirect};
|
|
||||||
|
|
||||||
let cancellable = self.new_cancellable();
|
|
||||||
|
|
||||||
match Request::parse(query) {
|
|
||||||
Ok(request) => request.handle(self, cancellable, callback),
|
|
||||||
Err(e) => match e {
|
|
||||||
// return failure response on unsupported scheme detected
|
|
||||||
Error::Unsupported => callback(Response::Failure(Failure::Error {
|
|
||||||
message: "Request scheme yet not supported".to_string(),
|
|
||||||
})),
|
|
||||||
// try async resolver (slow method)
|
|
||||||
_ => Request::lookup(query, Some(&cancellable), |result| {
|
|
||||||
callback(match result {
|
|
||||||
// redirection with scheme auto-complete or default search provider
|
|
||||||
Ok(request) => match request {
|
|
||||||
Request::Gemini(this, _) => {
|
|
||||||
Response::Redirect(Redirect::Foreground(this.uri))
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
// unresolvable request.
|
|
||||||
Err(e) => Response::Failure(Failure::Error {
|
|
||||||
message: e.to_string(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get new [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) by cancel previous one
|
|
||||||
fn new_cancellable(&self) -> Cancellable {
|
|
||||||
// Init new Cancellable
|
|
||||||
let cancellable = Cancellable::new();
|
|
||||||
|
|
||||||
// Replace by cancel previous operations
|
|
||||||
let previous = self.cancellable.replace(cancellable.clone());
|
|
||||||
if !previous.is_cancelled() {
|
|
||||||
previous.cancel();
|
|
||||||
self.status.replace(Status::Cancelled { time: now() });
|
|
||||||
} else {
|
|
||||||
self.status.replace(Status::Cancellable { time: now() });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Done
|
|
||||||
cancellable
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
mod error;
|
|
||||||
mod feature;
|
|
||||||
mod gemini;
|
|
||||||
mod search;
|
|
||||||
|
|
||||||
use gemini::Gemini;
|
|
||||||
|
|
||||||
use super::{Client, Response};
|
|
||||||
pub use error::Error;
|
|
||||||
use feature::Feature;
|
|
||||||
use gtk::{
|
|
||||||
gio::Cancellable,
|
|
||||||
glib::{Uri, UriFlags},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Single `Request` API for multiple `Client` drivers
|
|
||||||
pub enum Request {
|
|
||||||
Gemini(Gemini, Feature),
|
|
||||||
Titan {
|
|
||||||
referrer: Option<Box<Self>>,
|
|
||||||
uri: Uri,
|
|
||||||
}, // @TODO deprecated
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from featured string
|
|
||||||
pub fn parse(query: &str) -> Result<Self, Error> {
|
|
||||||
let (feature, request) = Feature::parse(query);
|
|
||||||
|
|
||||||
match Uri::parse(request, UriFlags::NONE) {
|
|
||||||
Ok(uri) => Self::from_uri(uri, feature),
|
|
||||||
Err(e) => Err(Error::Glib(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
|
||||||
pub fn from_uri(uri: Uri, feature: Feature) -> Result<Self, Error> {
|
|
||||||
match uri.scheme().as_str() {
|
|
||||||
"gemini" => Ok(Self::Gemini(
|
|
||||||
Gemini {
|
|
||||||
uri,
|
|
||||||
referrer: None,
|
|
||||||
},
|
|
||||||
feature,
|
|
||||||
)),
|
|
||||||
"titan" => todo!(),
|
|
||||||
_ => Err(Error::Unsupported),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new `Self` as the redirection query to default search provider
|
|
||||||
/// @TODO
|
|
||||||
// * implement DNS lookup before apply this option
|
|
||||||
// * make search provider optional
|
|
||||||
// * validate request len by gemini specifications
|
|
||||||
pub fn search(query: &str) -> Self {
|
|
||||||
Self::from_uri(search::tgls(query), Feature::Default).unwrap() // no handler as unexpected
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new `Self` using DNS async resolver (slow method)
|
|
||||||
/// * useful for scheme-less requests, before apply search redirect
|
|
||||||
pub fn lookup(
|
|
||||||
query: &str,
|
|
||||||
cancellable: Option<&Cancellable>,
|
|
||||||
callback: impl FnOnce(Result<Self, Error>) + 'static,
|
|
||||||
) {
|
|
||||||
use gtk::{
|
|
||||||
gio::{NetworkAddress, Resolver},
|
|
||||||
prelude::{NetworkAddressExt, ResolverExt},
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_SCHEME: &str = "gemini";
|
|
||||||
const DEFAULT_PORT: u16 = 1965;
|
|
||||||
const TIMEOUT: u32 = 250; // ms
|
|
||||||
|
|
||||||
let query = query.trim();
|
|
||||||
|
|
||||||
match Uri::parse(query, UriFlags::NONE) {
|
|
||||||
Ok(uri) => callback(Self::from_uri(uri, Feature::Default)),
|
|
||||||
Err(_) => {
|
|
||||||
// try default scheme suggestion
|
|
||||||
let suggestion = format!("{DEFAULT_SCHEME}://{query}");
|
|
||||||
|
|
||||||
let resolver = Resolver::default();
|
|
||||||
resolver.set_timeout(TIMEOUT);
|
|
||||||
|
|
||||||
match NetworkAddress::parse_uri(&suggestion, DEFAULT_PORT) {
|
|
||||||
Ok(connectable) => resolver.lookup_by_name_async(
|
|
||||||
&connectable.hostname(),
|
|
||||||
cancellable,
|
|
||||||
move |resolve| {
|
|
||||||
callback(if resolve.is_ok() {
|
|
||||||
Self::parse(&suggestion)
|
|
||||||
} else {
|
|
||||||
Ok(Self::search(&suggestion))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Err(_) => callback(Ok(Self::search(&suggestion))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
/// Handle `Self` request
|
|
||||||
pub fn handle(
|
|
||||||
self,
|
|
||||||
client: &Client,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
callback: impl FnOnce(Response) + 'static,
|
|
||||||
) {
|
|
||||||
match self {
|
|
||||||
Self::Gemini(this, feature) => this.handle(client, cancellable, callback),
|
|
||||||
Self::Titan { .. } => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Glib(gtk::glib::Error),
|
|
||||||
Unsupported,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Glib(e) => write!(f, "{e}"),
|
|
||||||
Self::Unsupported => write!(f, "Request not supported"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,212 +0,0 @@
|
|||||||
use super::{super::response::*, Client, Feature, Response};
|
|
||||||
|
|
||||||
use gtk::{
|
|
||||||
gio::Cancellable,
|
|
||||||
glib::{Priority, Uri, UriFlags},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Gemini {
|
|
||||||
pub referrer: Option<Box<Self>>,
|
|
||||||
pub uri: Uri,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Gemini {
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
pub fn handle(
|
|
||||||
self,
|
|
||||||
client: &Client,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
callback: impl FnOnce(Response) + 'static,
|
|
||||||
) {
|
|
||||||
use ggemini::client::connection::response::{data::Text, meta::Status};
|
|
||||||
|
|
||||||
client.gemini.request_async(
|
|
||||||
ggemini::client::Request::gemini(self.uri.clone()),
|
|
||||||
Priority::DEFAULT,
|
|
||||||
cancellable.clone(),
|
|
||||||
// Search for user certificate match request
|
|
||||||
// * @TODO this feature does not support multi-protocol yet
|
|
||||||
match client
|
|
||||||
.profile
|
|
||||||
.identity
|
|
||||||
.gemini
|
|
||||||
.match_scope(&self.uri.to_string())
|
|
||||||
{
|
|
||||||
Some(identity) => match identity.to_tls_certificate() {
|
|
||||||
Ok(certificate) => Some(certificate),
|
|
||||||
Err(_) => panic!(), // unexpected
|
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
},
|
|
||||||
|result| match result {
|
|
||||||
Ok(response) => {
|
|
||||||
match response.meta.status {
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected
|
|
||||||
Status::Input => callback(Response::Input(Input::Response {
|
|
||||||
base: self.uri.clone(),
|
|
||||||
title: match response.meta.data {
|
|
||||||
Some(data) => data.to_gstring(),
|
|
||||||
None => "Input expected".into(),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
Status::SensitiveInput => callback(Response::Input(Input::Sensitive {
|
|
||||||
base: self.uri.clone(),
|
|
||||||
title: match response.meta.data {
|
|
||||||
Some(data) => data.to_gstring(),
|
|
||||||
None => "Input expected".into(),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-20
|
|
||||||
Status::Success => match response.meta.mime {
|
|
||||||
Some(mime) => match mime.as_str() {
|
|
||||||
"text/gemini" => Text::from_stream_async(
|
|
||||||
response.connection.stream(),
|
|
||||||
Priority::DEFAULT,
|
|
||||||
cancellable.clone(),
|
|
||||||
move |result| match result {
|
|
||||||
Ok(text) => callback(Response::Text(
|
|
||||||
super::super::response::Text::Gemini {
|
|
||||||
base: self.uri.clone(),
|
|
||||||
data: text.to_string(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
Err(e) => callback(Response::Failure(Failure::Mime {
|
|
||||||
base: self.uri.clone(),
|
|
||||||
mime: mime.to_string(),
|
|
||||||
message: e.to_string(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"image/png" | "image/gif" | "image/jpeg" | "image/webp" => {
|
|
||||||
callback(Response::Stream {
|
|
||||||
base: self.uri.clone(),
|
|
||||||
mime: mime.to_string(),
|
|
||||||
stream: response.connection.stream(),
|
|
||||||
cancellable,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
mime => callback(Response::Failure(Failure::Mime {
|
|
||||||
base: self.uri.clone(),
|
|
||||||
mime: mime.to_string(),
|
|
||||||
message: format!("Content type `{mime}` yet not supported"),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
None => callback(Response::Failure(Failure::Error {
|
|
||||||
message: "MIME type not found".to_string(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
|
|
||||||
Status::Redirect => callback(self.redirect(response, false)),
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
|
|
||||||
Status::PermanentRedirect => callback(self.redirect(response, true)),
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
|
|
||||||
Status::CertificateRequest => {
|
|
||||||
callback(Response::Certificate(Certificate::Request {
|
|
||||||
title: match response.meta.data {
|
|
||||||
Some(data) => data.to_gstring(),
|
|
||||||
None => "Client certificate required".into(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
|
|
||||||
Status::CertificateUnauthorized => {
|
|
||||||
callback(Response::Certificate(Certificate::Request {
|
|
||||||
title: match response.meta.data {
|
|
||||||
Some(data) => data.to_gstring(),
|
|
||||||
None => "Certificate not authorized".into(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
|
|
||||||
Status::CertificateInvalid => {
|
|
||||||
callback(Response::Certificate(Certificate::Request {
|
|
||||||
title: match response.meta.data {
|
|
||||||
Some(data) => data.to_gstring(),
|
|
||||||
None => "Certificate not valid".into(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
status => callback(Response::Failure(Failure::Status {
|
|
||||||
message: format!("Undefined status code `{status}`"),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => callback(Response::Failure(Failure::Error {
|
|
||||||
message: e.to_string(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Redirection builder for `Self`
|
|
||||||
/// * [Redirect specification](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection)
|
|
||||||
fn redirect(
|
|
||||||
self,
|
|
||||||
response: ggemini::client::connection::Response,
|
|
||||||
is_permanent: bool,
|
|
||||||
) -> Response {
|
|
||||||
// Validate redirection count
|
|
||||||
if self.referrers() > 5 {
|
|
||||||
return Response::Failure(Failure::Error {
|
|
||||||
message: "Max redirection count reached".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Target URL expected from response meta data
|
|
||||||
match response.meta.data {
|
|
||||||
Some(target) => {
|
|
||||||
match Uri::parse_relative(&self.uri, target.as_str(), UriFlags::NONE) {
|
|
||||||
Ok(target) => {
|
|
||||||
// Disallow external redirection
|
|
||||||
if self.uri.scheme() != target.scheme()
|
|
||||||
|| self.uri.port() != target.port()
|
|
||||||
|| self.uri.host() != target.host()
|
|
||||||
{
|
|
||||||
return Response::Failure(Failure::Error {
|
|
||||||
message: "External redirects not allowed by protocol specification"
|
|
||||||
.to_string(),
|
|
||||||
}); // @TODO placeholder page with optional link open button
|
|
||||||
}
|
|
||||||
// Build new request
|
|
||||||
Response::Redirect(if is_permanent {
|
|
||||||
Redirect::Foreground(target)
|
|
||||||
} else {
|
|
||||||
Redirect::Background(target)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Err(e) => Response::Failure(Failure::Error {
|
|
||||||
message: e.to_string(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => Response::Failure(Failure::Error {
|
|
||||||
message: "Target address not found".to_string(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively count referrers of `Self`
|
|
||||||
/// * useful to apply redirection rules by protocol driver selected
|
|
||||||
pub fn referrers(&self) -> usize {
|
|
||||||
self.referrer
|
|
||||||
.as_ref()
|
|
||||||
.map_or(0, |request| request.referrers())
|
|
||||||
+ 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @TODO
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_referrers() {
|
|
||||||
const QUERY: &str = "gemini://geminiprotocol.net";
|
|
||||||
|
|
||||||
let r1 = Request::parse(QUERY, None).unwrap();
|
|
||||||
let r2 = Request::parse(QUERY, Some(r1)).unwrap();
|
|
||||||
let r3 = Request::parse(QUERY, Some(r2)).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(r3.referrers(), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
@ -1,17 +0,0 @@
|
|||||||
//! Search providers asset
|
|
||||||
|
|
||||||
use gtk::glib::{Uri, UriFlags};
|
|
||||||
|
|
||||||
/// Default search provider
|
|
||||||
pub fn tgls(query: &str) -> Uri {
|
|
||||||
Uri::build(
|
|
||||||
UriFlags::NONE,
|
|
||||||
"gemini",
|
|
||||||
None,
|
|
||||||
Some("tlgs.one"),
|
|
||||||
-1,
|
|
||||||
"/search",
|
|
||||||
Some(&Uri::escape_string(query, None, false)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
pub mod certificate;
|
|
||||||
pub mod failure;
|
|
||||||
pub mod input;
|
|
||||||
pub mod redirect;
|
|
||||||
pub mod text;
|
|
||||||
|
|
||||||
// Local dependencies
|
|
||||||
pub use certificate::Certificate;
|
|
||||||
pub use failure::Failure;
|
|
||||||
pub use input::Input;
|
|
||||||
pub use redirect::Redirect;
|
|
||||||
pub use text::Text;
|
|
||||||
|
|
||||||
// Global dependencies
|
|
||||||
use gtk::{
|
|
||||||
gio::{Cancellable, IOStream},
|
|
||||||
glib::Uri,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Single `Client` response API for all protocol drivers
|
|
||||||
pub enum Response {
|
|
||||||
Certificate(Certificate),
|
|
||||||
Download {
|
|
||||||
base: Uri,
|
|
||||||
stream: IOStream,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
},
|
|
||||||
Failure(Failure),
|
|
||||||
Input(Input),
|
|
||||||
Redirect(Redirect),
|
|
||||||
Stream {
|
|
||||||
base: Uri,
|
|
||||||
mime: String,
|
|
||||||
stream: IOStream,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
},
|
|
||||||
Text(Text),
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
use gtk::glib::GString;
|
|
||||||
|
|
||||||
pub enum Certificate {
|
|
||||||
Invalid { title: GString },
|
|
||||||
Request { title: GString },
|
|
||||||
Unauthorized { title: GString },
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
use gtk::glib::Uri;
|
|
||||||
|
|
||||||
/// Failure type for client `Response`
|
|
||||||
pub enum Failure {
|
|
||||||
Status {
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
/// This failure type provides `base` member to build Download page
|
|
||||||
/// for the constructed request [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
|
||||||
Mime {
|
|
||||||
base: Uri,
|
|
||||||
mime: String,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
/// Common error type
|
|
||||||
Error {
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
pub mod titan;
|
|
||||||
pub use titan::Titan;
|
|
||||||
|
|
||||||
use gtk::glib::{GString, Uri};
|
|
||||||
|
|
||||||
pub enum Input {
|
|
||||||
Response { base: Uri, title: GString },
|
|
||||||
Sensitive { base: Uri, title: GString },
|
|
||||||
Titan(Titan),
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
use gtk::{
|
|
||||||
gio::{Cancellable, IOStream},
|
|
||||||
glib::{Bytes, Error, Priority},
|
|
||||||
prelude::{IOStreamExt, OutputStreamExt},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Titan {
|
|
||||||
cancellable: Cancellable,
|
|
||||||
stream: IOStream,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Titan {
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
pub fn send(&self, data: Bytes, callback: impl FnOnce(Result<isize, Error>) + 'static) {
|
|
||||||
self.stream.output_stream().write_bytes_async(
|
|
||||||
&data,
|
|
||||||
Priority::DEFAULT,
|
|
||||||
Some(&self.cancellable),
|
|
||||||
callback,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
use gtk::glib::Uri;
|
|
||||||
|
|
||||||
pub enum Redirect {
|
|
||||||
Foreground(Uri),
|
|
||||||
Background(Uri),
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
use gtk::glib::Uri;
|
|
||||||
|
|
||||||
pub enum Text {
|
|
||||||
Gemini { base: Uri, data: String },
|
|
||||||
Plain { data: String },
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
pub mod failure;
|
|
||||||
pub mod gemini;
|
|
||||||
|
|
||||||
// Children dependencies
|
|
||||||
pub use failure::Failure;
|
|
||||||
pub use gemini::Gemini;
|
|
||||||
|
|
||||||
// Global dependencies
|
|
||||||
use crate::tool::format_time;
|
|
||||||
use gtk::glib::DateTime;
|
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
/// Local `Client` status
|
|
||||||
/// * not same as the Gemini status!
|
|
||||||
pub enum Status {
|
|
||||||
/// Ready to use (or cancel from outside)
|
|
||||||
Cancellable { time: DateTime },
|
|
||||||
/// Operation cancelled, new `Cancellable` required to continue
|
|
||||||
Cancelled { time: DateTime },
|
|
||||||
/// Protocol driver updates
|
|
||||||
Gemini(Gemini),
|
|
||||||
/// Something went wrong
|
|
||||||
Failure { time: DateTime, failure: Failure },
|
|
||||||
/// New `request` begin
|
|
||||||
Request { time: DateTime, value: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Status {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Cancellable { time } => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"[{}] Ready to use (or cancel from outside)",
|
|
||||||
format_time(time)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Self::Cancelled { time } => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"[{}] Operation cancelled, new `Cancellable` required to continue",
|
|
||||||
format_time(time)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Self::Gemini(status) => {
|
|
||||||
write!(f, "{status}")
|
|
||||||
}
|
|
||||||
Self::Failure { time, failure } => {
|
|
||||||
write!(f, "[{}] Failure: {failure}", format_time(time))
|
|
||||||
}
|
|
||||||
Self::Request { time, value } => {
|
|
||||||
write!(f, "[{}] Request `{value}`...", format_time(time))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
// Global dependencies
|
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
/// Local `Failure` status for `Client`
|
|
||||||
pub enum Failure {
|
|
||||||
/// Redirection count limit reached by protocol driver or global settings
|
|
||||||
RedirectCount { count: usize, is_global: bool },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Failure {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self::RedirectCount`
|
|
||||||
pub fn redirect_count(count: usize, is_global: bool) -> Self {
|
|
||||||
Self::RedirectCount { count, is_global }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Failure {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::RedirectCount { count, is_global } => {
|
|
||||||
if *is_global {
|
|
||||||
write!(f, "Redirection limit ({count}) reached by global settings")
|
|
||||||
} else {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Redirection limit ({count}) reached by protocol restrictions"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
// Global dependencies
|
|
||||||
use crate::tool::format_time;
|
|
||||||
use gtk::glib::DateTime;
|
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
/// Shared asset for `Gemini` statuses
|
|
||||||
pub enum Gemini {
|
|
||||||
Resolving { time: DateTime },
|
|
||||||
Resolved { time: DateTime },
|
|
||||||
Connecting { time: DateTime },
|
|
||||||
Connected { time: DateTime },
|
|
||||||
ProxyNegotiating { time: DateTime },
|
|
||||||
ProxyNegotiated { time: DateTime },
|
|
||||||
TlsHandshaking { time: DateTime },
|
|
||||||
TlsHandshaked { time: DateTime },
|
|
||||||
Complete { time: DateTime },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Gemini {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Resolving { time } => {
|
|
||||||
write!(f, "[{}] Resolving", format_time(time))
|
|
||||||
}
|
|
||||||
Self::Resolved { time } => {
|
|
||||||
write!(f, "[{}] Resolved", format_time(time))
|
|
||||||
}
|
|
||||||
Self::Connecting { time } => {
|
|
||||||
write!(f, "[{}] Connecting", format_time(time))
|
|
||||||
}
|
|
||||||
Self::Connected { time } => {
|
|
||||||
write!(f, "[{}] Connected", format_time(time))
|
|
||||||
}
|
|
||||||
Self::ProxyNegotiating { time } => {
|
|
||||||
write!(f, "[{}] Proxy negotiating", format_time(time))
|
|
||||||
}
|
|
||||||
Self::ProxyNegotiated { time } => {
|
|
||||||
write!(f, "[{}] Proxy negotiated", format_time(time))
|
|
||||||
}
|
|
||||||
Self::TlsHandshaking { time } => {
|
|
||||||
write!(f, "[{}] TLS handshaking", format_time(time))
|
|
||||||
}
|
|
||||||
Self::TlsHandshaked { time } => {
|
|
||||||
write!(f, "[{}] TLS handshaked", format_time(time))
|
|
||||||
}
|
|
||||||
Self::Complete { time } => {
|
|
||||||
write!(f, "[{}] Completed", format_time(time))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
mod response;
|
mod response;
|
||||||
mod sensitive;
|
mod sensitive;
|
||||||
mod titan;
|
|
||||||
mod widget;
|
mod widget;
|
||||||
|
|
||||||
use super::TabAction;
|
use super::TabAction;
|
||||||
@ -8,7 +7,6 @@ use gtk::glib::Uri;
|
|||||||
use response::Response;
|
use response::Response;
|
||||||
use sensitive::Sensitive;
|
use sensitive::Sensitive;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use titan::Titan;
|
|
||||||
use widget::Widget;
|
use widget::Widget;
|
||||||
|
|
||||||
pub struct Input {
|
pub struct Input {
|
||||||
@ -64,13 +62,4 @@ impl Input {
|
|||||||
.g_box,
|
.g_box,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_new_titan(
|
|
||||||
&self,
|
|
||||||
titan: super::client::response::input::Titan,
|
|
||||||
callback: impl Fn(Result<super::client::Response, ()>) + 'static,
|
|
||||||
) {
|
|
||||||
self.widget
|
|
||||||
.update(Some(&Titan::build(titan, callback).widget.g_box));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
mod control;
|
|
||||||
mod form;
|
|
||||||
mod title;
|
|
||||||
mod widget;
|
|
||||||
|
|
||||||
use control::Control;
|
|
||||||
use form::Form;
|
|
||||||
use gtk::{gio::SimpleAction, glib::uuid_string_random};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use title::Title;
|
|
||||||
use widget::Widget;
|
|
||||||
|
|
||||||
pub struct Titan {
|
|
||||||
// Components
|
|
||||||
pub widget: Rc<Widget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Titan {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(
|
|
||||||
titan: super::super::client::response::input::Titan,
|
|
||||||
callback: impl Fn(Result<super::super::client::Response, ()>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
// Init local actions
|
|
||||||
let action_update = SimpleAction::new(&uuid_string_random(), None);
|
|
||||||
let action_send = SimpleAction::new(&uuid_string_random(), None);
|
|
||||||
|
|
||||||
// Init components
|
|
||||||
let control = Rc::new(Control::build(action_send.clone()));
|
|
||||||
let form = Rc::new(Form::build(action_update.clone()));
|
|
||||||
let title = Rc::new(Title::build(None)); // @TODO
|
|
||||||
|
|
||||||
// Init widget
|
|
||||||
let widget = Rc::new(Widget::build(
|
|
||||||
&title.widget.label,
|
|
||||||
&form.widget.text_view,
|
|
||||||
&control.widget.g_box,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Init events
|
|
||||||
action_update.connect_activate({
|
|
||||||
let control = control.clone();
|
|
||||||
let form = form.clone();
|
|
||||||
move |_, _| control.update(Some(form.widget.size()))
|
|
||||||
});
|
|
||||||
|
|
||||||
action_send.connect_activate({
|
|
||||||
// @TODO let form = form.clone();
|
|
||||||
move |_, _| callback(todo!()) // @TODO input data
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return activated struct
|
|
||||||
Self { widget }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
mod counter;
|
|
||||||
mod send;
|
|
||||||
mod widget;
|
|
||||||
|
|
||||||
use counter::Counter;
|
|
||||||
use send::Send;
|
|
||||||
use widget::Widget;
|
|
||||||
|
|
||||||
use gtk::gio::SimpleAction;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
pub struct Control {
|
|
||||||
pub counter: Rc<Counter>,
|
|
||||||
pub send: Rc<Send>,
|
|
||||||
pub widget: Rc<Widget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Control {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(action_send: SimpleAction) -> Self {
|
|
||||||
// Init components
|
|
||||||
let counter = Rc::new(Counter::new());
|
|
||||||
let send = Rc::new(Send::build(action_send));
|
|
||||||
|
|
||||||
// Init widget
|
|
||||||
let widget = Rc::new(Widget::build(&counter.widget.label, &send.widget.button));
|
|
||||||
|
|
||||||
// Return activated struct
|
|
||||||
Self {
|
|
||||||
counter,
|
|
||||||
send,
|
|
||||||
widget,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
pub fn update(&self, bytes: Option<usize>) {
|
|
||||||
// Update children components
|
|
||||||
self.counter.update(bytes);
|
|
||||||
self.send.update(match bytes {
|
|
||||||
Some(left) => left > 0,
|
|
||||||
None => false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
mod widget;
|
|
||||||
|
|
||||||
use widget::Widget;
|
|
||||||
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
pub struct Counter {
|
|
||||||
pub widget: Rc<Widget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Counter {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Counter {
|
|
||||||
// Construct
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
widget: Rc::new(Widget::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
pub fn update(&self, bytes: Option<usize>) {
|
|
||||||
self.widget.update(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
use gtk::{prelude::WidgetExt, Label};
|
|
||||||
|
|
||||||
pub struct Widget {
|
|
||||||
pub label: Label,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Widget {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
// Construct
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
label: Label::builder().css_classes(["dim-label"]).build(), // @TODO `.dimmed` Since: Adw 1.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
pub fn update(&self, bytes: Option<usize>) {
|
|
||||||
match bytes {
|
|
||||||
Some(value) => {
|
|
||||||
self.label.set_label(&crate::tool::format_bytes(value));
|
|
||||||
self.label.set_visible(value > 0);
|
|
||||||
}
|
|
||||||
None => self.label.set_visible(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
mod widget;
|
|
||||||
use widget::Widget;
|
|
||||||
|
|
||||||
use gtk::gio::SimpleAction;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
pub struct Send {
|
|
||||||
pub widget: Rc<Widget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Send {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(action_send: SimpleAction) -> Self {
|
|
||||||
// Init widget
|
|
||||||
let widget = Rc::new(Widget::build(action_send));
|
|
||||||
|
|
||||||
// Result
|
|
||||||
Self { widget }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
pub fn update(&self, is_sensitive: bool) {
|
|
||||||
self.widget.update(is_sensitive);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
use gtk::{
|
|
||||||
gio::SimpleAction,
|
|
||||||
prelude::{ActionExt, ButtonExt, WidgetExt},
|
|
||||||
Button,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Widget {
|
|
||||||
pub button: Button,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(action_send: SimpleAction) -> Self {
|
|
||||||
// Init main widget
|
|
||||||
let button = Button::builder()
|
|
||||||
.css_classes(["accent"]) // | `suggested-action`
|
|
||||||
.label("Send")
|
|
||||||
.sensitive(false)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Init events
|
|
||||||
button.connect_clicked({
|
|
||||||
move |_| {
|
|
||||||
action_send.activate(None);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return activated `Self`
|
|
||||||
Self { button }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
pub fn update(&self, is_sensitive: bool) {
|
|
||||||
self.button.set_sensitive(is_sensitive);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
use gtk::{prelude::BoxExt, Align, Box, Button, Label, Orientation};
|
|
||||||
|
|
||||||
const SPACING: i32 = 8;
|
|
||||||
|
|
||||||
pub struct Widget {
|
|
||||||
pub g_box: Box,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(limit: &Label, send: &Button) -> Self {
|
|
||||||
// Init main widget
|
|
||||||
let g_box = Box::builder()
|
|
||||||
.halign(Align::End)
|
|
||||||
.orientation(Orientation::Horizontal)
|
|
||||||
.spacing(SPACING)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
g_box.append(limit);
|
|
||||||
g_box.append(send);
|
|
||||||
|
|
||||||
// Return new `Self`
|
|
||||||
Self { g_box }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
mod widget;
|
|
||||||
|
|
||||||
use widget::Widget;
|
|
||||||
|
|
||||||
use gtk::gio::SimpleAction;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
pub struct Form {
|
|
||||||
pub widget: Rc<Widget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Form {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(action_update: SimpleAction) -> Self {
|
|
||||||
Self {
|
|
||||||
widget: Rc::new(Widget::new(action_update)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
use gtk::{
|
|
||||||
gio::SimpleAction,
|
|
||||||
prelude::{ActionExt, TextBufferExt, TextViewExt, WidgetExt},
|
|
||||||
TextView, WrapMode,
|
|
||||||
};
|
|
||||||
use libspelling::{Checker, TextBufferAdapter};
|
|
||||||
use sourceview::Buffer;
|
|
||||||
|
|
||||||
const MARGIN: i32 = 8;
|
|
||||||
|
|
||||||
pub struct Widget {
|
|
||||||
pub text_view: TextView,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
// Construct
|
|
||||||
pub fn new(action_update: SimpleAction) -> Self {
|
|
||||||
// Init [SourceView](https://gitlab.gnome.org/GNOME/gtksourceview) type buffer
|
|
||||||
let buffer = Buffer::builder().build();
|
|
||||||
|
|
||||||
// Init [libspelling](https://gitlab.gnome.org/GNOME/libspelling)
|
|
||||||
let checker = Checker::default();
|
|
||||||
let adapter = TextBufferAdapter::new(&buffer, &checker);
|
|
||||||
adapter.set_enabled(true);
|
|
||||||
|
|
||||||
// Init main widget
|
|
||||||
let text_view = TextView::builder()
|
|
||||||
.bottom_margin(MARGIN)
|
|
||||||
.buffer(&buffer)
|
|
||||||
.css_classes(["frame", "view"])
|
|
||||||
.extra_menu(&adapter.menu_model())
|
|
||||||
.left_margin(MARGIN)
|
|
||||||
.margin_bottom(MARGIN / 4)
|
|
||||||
.right_margin(MARGIN)
|
|
||||||
.top_margin(MARGIN)
|
|
||||||
.wrap_mode(WrapMode::Word)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
text_view.insert_action_group("spelling", Some(&adapter));
|
|
||||||
|
|
||||||
// Init events
|
|
||||||
text_view.buffer().connect_changed(move |_| {
|
|
||||||
action_update.activate(None);
|
|
||||||
});
|
|
||||||
|
|
||||||
text_view.connect_realize(move |this| {
|
|
||||||
this.grab_focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return activated `Self`
|
|
||||||
Self { text_view }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
pub fn size(&self) -> usize {
|
|
||||||
let buffer = self.text_view.buffer();
|
|
||||||
buffer
|
|
||||||
.text(&buffer.start_iter(), &buffer.end_iter(), true)
|
|
||||||
.len()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
mod widget;
|
|
||||||
|
|
||||||
use widget::Widget;
|
|
||||||
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
pub struct Title {
|
|
||||||
pub widget: Rc<Widget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Title {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(title: Option<&str>) -> Self {
|
|
||||||
Self {
|
|
||||||
widget: Rc::new(Widget::build(title)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
use gtk::{prelude::WidgetExt, Align, Label};
|
|
||||||
|
|
||||||
pub struct Widget {
|
|
||||||
pub label: Label,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(title: Option<&str>) -> Self {
|
|
||||||
let label = Label::builder()
|
|
||||||
.css_classes(["heading"])
|
|
||||||
.halign(Align::Start)
|
|
||||||
.visible(false)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
if let Some(value) = title {
|
|
||||||
if !value.is_empty() {
|
|
||||||
label.set_label(value);
|
|
||||||
label.set_visible(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self { label }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
use gtk::{prelude::BoxExt, Box, Label, Orientation, TextView};
|
|
||||||
|
|
||||||
const MARGIN: i32 = 6;
|
|
||||||
const SPACING: i32 = 8;
|
|
||||||
|
|
||||||
pub struct Widget {
|
|
||||||
pub g_box: Box,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Build new `Self`
|
|
||||||
pub fn build(title: &Label, response: &TextView, control: &Box) -> Self {
|
|
||||||
let g_box = Box::builder()
|
|
||||||
.margin_bottom(MARGIN)
|
|
||||||
.margin_end(MARGIN)
|
|
||||||
.margin_start(MARGIN)
|
|
||||||
.margin_top(MARGIN)
|
|
||||||
.spacing(SPACING)
|
|
||||||
.orientation(Orientation::Vertical)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
g_box.append(title);
|
|
||||||
g_box.append(response);
|
|
||||||
g_box.append(control);
|
|
||||||
|
|
||||||
Self { g_box }
|
|
||||||
}
|
|
||||||
}
|
|
@ -66,7 +66,7 @@ impl Navigation {
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
pub fn update(&self, progress_fraction: Option<f64>) {
|
pub fn update(&self) {
|
||||||
// init shared request value
|
// init shared request value
|
||||||
let request = self.request.strip_prefix();
|
let request = self.request.strip_prefix();
|
||||||
|
|
||||||
@ -74,10 +74,9 @@ impl Navigation {
|
|||||||
self.bookmark
|
self.bookmark
|
||||||
.update(self.profile.bookmark.get(&request).is_ok());
|
.update(self.profile.bookmark.get(&request).is_ok());
|
||||||
self.history.update();
|
self.history.update();
|
||||||
self.home.update(self.request.uri().as_ref());
|
self.home.update(self.request.as_uri().as_ref());
|
||||||
self.reload.update(!request.is_empty());
|
self.reload.update(!request.is_empty());
|
||||||
self.request.update(
|
self.request.update(
|
||||||
progress_fraction,
|
|
||||||
self.profile
|
self.profile
|
||||||
.identity
|
.identity
|
||||||
.gemini
|
.gemini
|
||||||
|
@ -29,8 +29,8 @@ impl Request {
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
pub fn update(&self, progress_fraction: Option<f64>, is_identity_active: bool) {
|
pub fn update(&self, is_identity_active: bool) {
|
||||||
self.widget.update(progress_fraction, is_identity_active);
|
self.widget.update(is_identity_active);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clean(
|
pub fn clean(
|
||||||
@ -94,19 +94,19 @@ impl Request {
|
|||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
|
|
||||||
pub fn to_download(&self) {
|
pub fn into_download(&self) {
|
||||||
self.widget.entry.set_text(&self.download());
|
self.widget.entry.set_text(&self.download());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_source(&self) {
|
pub fn into_source(&self) {
|
||||||
self.widget.entry.set_text(&self.source());
|
self.widget.entry.set_text(&self.source());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|
||||||
/// Get current request value in [Uri](https://docs.gtk.org/glib/struct.Uri.html) format
|
/// Try get current request value as [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
||||||
/// * `strip_prefix` on parse
|
/// * `strip_prefix` on parse
|
||||||
pub fn uri(&self) -> Option<Uri> {
|
pub fn as_uri(&self) -> Option<Uri> {
|
||||||
match Uri::parse(&strip_prefix(self.widget.entry.text()), UriFlags::NONE) {
|
match Uri::parse(&strip_prefix(self.widget.entry.text()), UriFlags::NONE) {
|
||||||
Ok(uri) => Some(uri),
|
Ok(uri) => Some(uri),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
@ -188,7 +188,7 @@ impl Widget {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&self, progress_fraction: Option<f64>, is_identity_active: bool) {
|
pub fn update(&self, is_identity_active: bool) {
|
||||||
// Update primary icon
|
// Update primary icon
|
||||||
self.entry
|
self.entry
|
||||||
.first_child()
|
.first_child()
|
||||||
@ -229,46 +229,46 @@ impl Widget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update progress
|
// Update progress
|
||||||
// * skip update animation for None value
|
// * @TODO skip update animation for None value
|
||||||
if let Some(value) = progress_fraction {
|
let value = self.entry.progress_fraction();
|
||||||
// Update shared fraction on new value was changed
|
|
||||||
if value != self.progress.fraction.replace(value) {
|
|
||||||
// Start new frame on previous process function completed (`source_id` changed to None)
|
|
||||||
// If previous process still active, we have just updated shared fraction value before, to use it inside the active process
|
|
||||||
if self.progress.source_id.borrow().is_none() {
|
|
||||||
// Start new animation frame iterator, update `source_id`
|
|
||||||
self.progress.source_id.replace(Some(timeout_add_local(
|
|
||||||
Duration::from_millis(PROGRESS_ANIMATION_TIME),
|
|
||||||
{
|
|
||||||
// Clone async pointers dependency
|
|
||||||
let entry = self.entry.clone();
|
|
||||||
let progress = self.progress.clone();
|
|
||||||
|
|
||||||
// Frame
|
// Update shared fraction on new value was changed
|
||||||
move || {
|
if value != self.progress.fraction.replace(value) {
|
||||||
// Animate
|
// Start new frame on previous process function completed (`source_id` changed to None)
|
||||||
if *progress.fraction.borrow() > entry.progress_fraction() {
|
// If previous process still active, we have just updated shared fraction value before, to use it inside the active process
|
||||||
entry.set_progress_fraction(
|
if self.progress.source_id.borrow().is_none() {
|
||||||
// Currently, here is no outrange validation, seems that wrapper make this work @TODO
|
// Start new animation frame iterator, update `source_id`
|
||||||
entry.progress_fraction() + PROGRESS_ANIMATION_STEP,
|
self.progress.source_id.replace(Some(timeout_add_local(
|
||||||
);
|
Duration::from_millis(PROGRESS_ANIMATION_TIME),
|
||||||
return ControlFlow::Continue;
|
{
|
||||||
}
|
// Clone async pointers dependency
|
||||||
// Deactivate
|
let entry = self.entry.clone();
|
||||||
progress.source_id.replace(None);
|
let progress = self.progress.clone();
|
||||||
|
|
||||||
// Reset on 100% (to hide progress bar)
|
// Frame
|
||||||
// or, just await for new value request
|
move || {
|
||||||
if entry.progress_fraction() == 1.0 {
|
// Animate
|
||||||
entry.set_progress_fraction(0.0);
|
if *progress.fraction.borrow() > entry.progress_fraction() {
|
||||||
}
|
entry.set_progress_fraction(
|
||||||
|
// Currently, here is no outrange validation, seems that wrapper make this work @TODO
|
||||||
// Stop iteration
|
entry.progress_fraction() + PROGRESS_ANIMATION_STEP,
|
||||||
ControlFlow::Break
|
);
|
||||||
|
return ControlFlow::Continue;
|
||||||
}
|
}
|
||||||
},
|
// Deactivate
|
||||||
)));
|
progress.source_id.replace(None);
|
||||||
}
|
|
||||||
|
// Reset on 100% (to hide progress bar)
|
||||||
|
// or, just await for new value request
|
||||||
|
if entry.progress_fraction() == 1.0 {
|
||||||
|
entry.set_progress_fraction(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop iteration
|
||||||
|
ControlFlow::Break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
// Global dependencies
|
// Global dependencies
|
||||||
use super::client::{status::Gemini, Status as Client};
|
|
||||||
use crate::tool::format_time;
|
use crate::tool::format_time;
|
||||||
use gtk::glib::DateTime;
|
use gtk::glib::DateTime;
|
||||||
use std::fmt::{Display, Formatter, Result};
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
/// `Page` status
|
/// `Page` status
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Client(Client),
|
|
||||||
Failure { time: DateTime },
|
Failure { time: DateTime },
|
||||||
Input { time: DateTime },
|
Input { time: DateTime },
|
||||||
Loading { time: DateTime },
|
Loading { time: DateTime },
|
||||||
@ -16,43 +14,9 @@ pub enum Status {
|
|||||||
Success { time: DateTime },
|
Success { time: DateTime },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Status {
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Translate `Self` to `progress-fraction` presentation
|
|
||||||
/// * see also: [Entry](https://docs.gtk.org/gtk4/property.Entry.progress-fraction.html)
|
|
||||||
pub fn to_progress_fraction(&self) -> Option<f64> {
|
|
||||||
match self {
|
|
||||||
Self::Loading { .. } | Self::SessionRestore { .. } => Some(0.0),
|
|
||||||
Self::Client(status) => match status {
|
|
||||||
Client::Cancellable { .. }
|
|
||||||
| Client::Cancelled { .. }
|
|
||||||
| Client::Failure { .. }
|
|
||||||
| Client::Request { .. } => Some(0.0),
|
|
||||||
Client::Gemini(status) => match status {
|
|
||||||
Gemini::Resolving { .. } => Some(0.1),
|
|
||||||
Gemini::Resolved { .. } => Some(0.2),
|
|
||||||
Gemini::Connecting { .. } => Some(0.3),
|
|
||||||
Gemini::Connected { .. } => Some(0.4),
|
|
||||||
Gemini::ProxyNegotiating { .. } => Some(0.5),
|
|
||||||
Gemini::ProxyNegotiated { .. } => Some(0.6),
|
|
||||||
Gemini::TlsHandshaking { .. } => Some(0.7),
|
|
||||||
Gemini::TlsHandshaked { .. } => Some(0.8),
|
|
||||||
Gemini::Complete { .. } => Some(0.9),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Self::Failure { .. } | Self::Success { .. } | Self::Input { .. } => Some(1.0),
|
|
||||||
Self::New { .. } | Self::SessionRestored { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Status {
|
impl Display for Status {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Client(client) => {
|
|
||||||
write!(f, "{client}")
|
|
||||||
}
|
|
||||||
Self::Failure { time } => {
|
Self::Failure { time } => {
|
||||||
write!(f, "[{}] Failure", format_time(time))
|
write!(f, "[{}] Failure", format_time(time))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user