From 0c08a0fb2f5fc36bc84fa6492791338ef19d464b Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 21 Jan 2025 15:04:31 +0200 Subject: [PATCH] begin multi-driver page client implementation --- src/app/browser/window/tab.rs | 28 +- src/app/browser/window/tab/item.rs | 27 +- src/app/browser/window/tab/item/client.rs | 146 ++++++ .../browser/window/tab/item/client/driver.rs | 21 + .../window/tab/item/client/driver/gemini.rs | 470 ++++++++++++++++++ .../client/request => client}/feature.rs | 3 +- src/app/browser/window/tab/item/page.rs | 460 +---------------- .../browser/window/tab/item/page/client.rs | 126 ----- .../window/tab/item/page/client/request.rs | 121 ----- .../tab/item/page/client/request/error.rs | 16 - .../tab/item/page/client/request/gemini.rs | 212 -------- .../tab/item/page/client/request/search.rs | 17 - .../window/tab/item/page/client/response.rs | 38 -- .../item/page/client/response/certificate.rs | 7 - .../tab/item/page/client/response/failure.rs | 19 - .../tab/item/page/client/response/input.rs | 10 - .../item/page/client/response/input/titan.rs | 23 - .../tab/item/page/client/response/redirect.rs | 6 - .../tab/item/page/client/response/text.rs | 6 - .../window/tab/item/page/client/status.rs | 56 --- .../tab/item/page/client/status/failure.rs | 34 -- .../tab/item/page/client/status/gemini.rs | 51 -- src/app/browser/window/tab/item/page/input.rs | 11 - .../window/tab/item/page/input/titan.rs | 57 --- .../tab/item/page/input/titan/control.rs | 47 -- .../item/page/input/titan/control/counter.rs | 29 -- .../input/titan/control/counter/widget.rs | 31 -- .../tab/item/page/input/titan/control/send.rs | 27 - .../page/input/titan/control/send/widget.rs | 38 -- .../item/page/input/titan/control/widget.rs | 27 - .../window/tab/item/page/input/titan/form.rs | 21 - .../tab/item/page/input/titan/form/widget.rs | 62 --- .../window/tab/item/page/input/titan/title.rs | 20 - .../tab/item/page/input/titan/title/widget.rs | 27 - .../tab/item/page/input/titan/widget.rs | 30 -- .../window/tab/item/page/navigation.rs | 5 +- .../tab/item/page/navigation/request.rs | 12 +- .../item/page/navigation/request/widget.rs | 76 +-- .../browser/window/tab/item/page/status.rs | 36 -- 39 files changed, 741 insertions(+), 1712 deletions(-) create mode 100644 src/app/browser/window/tab/item/client.rs create mode 100644 src/app/browser/window/tab/item/client/driver.rs create mode 100644 src/app/browser/window/tab/item/client/driver/gemini.rs rename src/app/browser/window/tab/item/{page/client/request => client}/feature.rs (96%) delete mode 100644 src/app/browser/window/tab/item/page/client.rs delete mode 100644 src/app/browser/window/tab/item/page/client/request.rs delete mode 100644 src/app/browser/window/tab/item/page/client/request/error.rs delete mode 100644 src/app/browser/window/tab/item/page/client/request/gemini.rs delete mode 100644 src/app/browser/window/tab/item/page/client/request/search.rs delete mode 100644 src/app/browser/window/tab/item/page/client/response.rs delete mode 100644 src/app/browser/window/tab/item/page/client/response/certificate.rs delete mode 100644 src/app/browser/window/tab/item/page/client/response/failure.rs delete mode 100644 src/app/browser/window/tab/item/page/client/response/input.rs delete mode 100644 src/app/browser/window/tab/item/page/client/response/input/titan.rs delete mode 100644 src/app/browser/window/tab/item/page/client/response/redirect.rs delete mode 100644 src/app/browser/window/tab/item/page/client/response/text.rs delete mode 100644 src/app/browser/window/tab/item/page/client/status.rs delete mode 100644 src/app/browser/window/tab/item/page/client/status/failure.rs delete mode 100644 src/app/browser/window/tab/item/page/client/status/gemini.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/control.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/control/counter.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/control/counter/widget.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/control/send.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/control/send/widget.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/control/widget.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/form.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/form/widget.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/title.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/title/widget.rs delete mode 100644 src/app/browser/window/tab/item/page/input/titan/widget.rs diff --git a/src/app/browser/window/tab.rs b/src/app/browser/window/tab.rs index 8caf1f5f..70a017b9 100644 --- a/src/app/browser/window/tab.rs +++ b/src/app/browser/window/tab.rs @@ -16,7 +16,7 @@ use crate::app::browser::{ use crate::Profile; use gtk::{ glib::{DateTime, GString, Propagation}, - prelude::WidgetExt, + prelude::{EditableExt, WidgetExt}, }; use sqlite::Transaction; 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) pub fn save_as(&self, page_position: Option) { if let Some(item) = self.item(page_position) { - item.page.navigation.request.to_download(); - item::page::load(&item.page, None, true); + item.page.navigation.request.into_download(); + todo!() } } // View source for page at given `position`, `None` to use selected page (if available) pub fn source(&self, page_position: Option) { if let Some(item) = self.item(page_position) { - item.page.navigation.request.to_source(); - item::page::load(&item.page, None, true); + item.page.navigation.request.into_source(); + todo!() } } @@ -250,26 +250,36 @@ impl Tab { pub fn page_home(&self, page_position: Option) { 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) { 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) { 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 pub fn page_reload(&self, page_position: Option) { 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); } } diff --git a/src/app/browser/window/tab/item.rs b/src/app/browser/window/tab/item.rs index feca2a1f..2b864537 100644 --- a/src/app/browser/window/tab/item.rs +++ b/src/app/browser/window/tab/item.rs @@ -1,30 +1,33 @@ mod action; +mod client; mod database; mod identity; pub mod page; mod widget; -use action::Action; -use page::Page; -use widget::Widget; - use crate::app::browser::{ window::action::{Action as WindowAction, Position}, Action as BrowserAction, }; use crate::Profile; +use action::Action; use adw::TabView; +use client::Client; use gtk::{ glib::{uuid_string_random, GString}, prelude::{Cast, EditableExt}, }; +use page::Page; use sqlite::Transaction; use std::rc::Rc; +use widget::Widget; pub struct Item { // Auto-generated unique item ID // useful as widget name in GTK actions callback pub id: Rc, + // Multi-protocol handler + pub client: Rc, // Components pub page: Rc, pub widget: Rc, @@ -60,6 +63,8 @@ impl Item { (browser_action, window_action, &action), )); + let client = Rc::new(Client::init(&page)); + let widget = Rc::new(Widget::build( id.as_str(), tab_view, @@ -74,7 +79,7 @@ impl Item { if let Some(text) = request { page.navigation.request.widget.entry.set_text(&text); if is_load { - page::load(&page, None, true); + client.handle(&text, true); } } @@ -87,7 +92,7 @@ impl Item { let window_action = window_action.clone(); move || { // 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 if uri.scheme().to_lowercase() == "gemini" { return identity::new_gemini( @@ -106,16 +111,22 @@ impl Item { // Load new request for item action.load.connect_activate({ let page = page.clone(); + let client = client.clone(); move |request, is_history| { if let Some(text) = request { page.navigation.request.widget.entry.set_text(&text); + client.handle(&text, is_history); } - page::load(&page, None, is_history); } }); // Done - Self { id, page, widget } + Self { + id, + client, + page, + widget, + } } // Actions diff --git a/src/app/browser/window/tab/item/client.rs b/src/app/browser/window/tab/item/client.rs new file mode 100644 index 00000000..316e0d32 --- /dev/null +++ b/src/app/browser/window/tab/item/client.rs @@ -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, + driver: Rc, +} + +impl Client { + // Constructors + + /// Create new `Self` + pub fn init(page: &Rc) -> 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, + cancellable: Cancellable, + callback: impl FnOnce(Rc, 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, + 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 +} diff --git a/src/app/browser/window/tab/item/client/driver.rs b/src/app/browser/window/tab/item/client/driver.rs new file mode 100644 index 00000000..9df84ff7 --- /dev/null +++ b/src/app/browser/window/tab/item/client/driver.rs @@ -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) -> Self { + Driver { + gemini: Gemini::init(page), + } + } +} diff --git a/src/app/browser/window/tab/item/client/driver/gemini.rs b/src/app/browser/window/tab/item/client/driver/gemini.rs new file mode 100644 index 00000000..fd700e7a --- /dev/null +++ b/src/app/browser/window/tab/item/client/driver/gemini.rs @@ -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, + page: Rc, +} + +impl Gemini { + // Constructors + + /// Create new `Self` + pub fn init(page: &Rc) -> 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) + } +} diff --git a/src/app/browser/window/tab/item/page/client/request/feature.rs b/src/app/browser/window/tab/item/client/feature.rs similarity index 96% rename from src/app/browser/window/tab/item/page/client/request/feature.rs rename to src/app/browser/window/tab/item/client/feature.rs index 8a01c454..1e399423 100644 --- a/src/app/browser/window/tab/item/page/client/request/feature.rs +++ b/src/app/browser/window/tab/item/client/feature.rs @@ -30,6 +30,7 @@ impl Feature { // Getters + /* @TODO not in use /// Get `Self` as prefix pub fn as_prefix(&self) -> Option<&str> { match self { @@ -37,5 +38,5 @@ impl Feature { Self::Source => Some(SOURCE), Self::Default => None, } - } + }*/ } diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 064d3399..b2bc701b 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -1,14 +1,12 @@ -mod client; // @TODO complete new router implementation mod content; mod database; mod error; mod input; mod navigation; mod search; -mod status; +pub mod status; mod widget; -use client::Client; use content::Content; use error::Error; use input::Input; @@ -21,25 +19,22 @@ use super::{Action as TabAction, BrowserAction, Profile, WindowAction}; use crate::tool::now; use gtk::{ - gdk::Texture, - gdk_pixbuf::Pixbuf, - glib::{GString, Priority, Uri}, - prelude::{EditableExt, FileExt}, + glib::GString, + prelude::{EditableExt, EntryExt}, }; use sqlite::Transaction; -use std::{cell::RefCell, path::MAIN_SEPARATOR, rc::Rc, time::Duration}; +use std::{cell::RefCell, rc::Rc}; pub struct Page { - id: Rc, - profile: Rc, - status: Rc>, - title: Rc>, + pub id: Rc, + pub profile: Rc, + pub status: Rc>, + pub title: Rc>, // Actions - browser_action: Rc, - tab_action: Rc, - window_action: Rc, + pub browser_action: Rc, + pub tab_action: Rc, + pub window_action: Rc, // Components - pub client: Rc, pub content: Rc, pub search: Rc, pub input: Rc, @@ -81,16 +76,6 @@ impl Page { 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 Self { id: id.clone(), @@ -101,7 +86,6 @@ impl Page { tab_action: tab_action.clone(), window_action: window_action.clone(), // Components - client, status, content, search, @@ -140,10 +124,8 @@ impl Page { /// Update `Self` witch children components pub fn update(&self) { - // Update components - self.navigation - .update(self.status.borrow().to_progress_fraction()); - // @TODO self.content.update(); + // Update children components + self.navigation.update(); } /// Cleanup session for `Self` @@ -188,7 +170,9 @@ impl Page { self.navigation.restore(transaction, &record.id)?; // Make initial page history snap using `navigation` values restored // * 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()), @@ -232,72 +216,11 @@ impl Page { /// Get `Self` loading status pub fn is_loading(&self) -> bool { - match self.status.borrow().to_progress_fraction() { - Some(progress_fraction) => progress_fraction < 1.0, - None => false, - } + let progress_fraction = self.navigation.request.widget.entry.progress_fraction(); + progress_fraction > 0.0 && progress_fraction < 1.0 } } -/// 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) { - 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) { - 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) { - 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, 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 pub fn migrate(tx: &Transaction) -> Result<(), String> { @@ -312,350 +235,3 @@ pub fn migrate(tx: &Transaction) -> Result<(), String> { // Success 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, 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 - }, - } -} diff --git a/src/app/browser/window/tab/item/page/client.rs b/src/app/browser/window/tab/item/page/client.rs deleted file mode 100644 index 670872a2..00000000 --- a/src/app/browser/window/tab/item/page/client.rs +++ /dev/null @@ -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, - status: Rc>, - /// Profile reference required for Gemini protocol auth (match scope) - profile: Rc, - /// Supported clients - /// * gemini driver should be initiated once (on page object init) - /// to process all it connection features properly - gemini: Rc, -} - -impl Client { - // Constructors - - /// Create new `Self` - pub fn init(profile: &Rc, 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 - } -} diff --git a/src/app/browser/window/tab/item/page/client/request.rs b/src/app/browser/window/tab/item/page/client/request.rs deleted file mode 100644 index 334fad2b..00000000 --- a/src/app/browser/window/tab/item/page/client/request.rs +++ /dev/null @@ -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>, - uri: Uri, - }, // @TODO deprecated -} - -impl Request { - // Constructors - - /// Create new `Self` from featured string - pub fn parse(query: &str) -> Result { - 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 { - 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) + '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!(), - } - } -} diff --git a/src/app/browser/window/tab/item/page/client/request/error.rs b/src/app/browser/window/tab/item/page/client/request/error.rs deleted file mode 100644 index 903e8724..00000000 --- a/src/app/browser/window/tab/item/page/client/request/error.rs +++ /dev/null @@ -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"), - } - } -} diff --git a/src/app/browser/window/tab/item/page/client/request/gemini.rs b/src/app/browser/window/tab/item/page/client/request/gemini.rs deleted file mode 100644 index c489ec06..00000000 --- a/src/app/browser/window/tab/item/page/client/request/gemini.rs +++ /dev/null @@ -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>, - 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); -} - -*/ diff --git a/src/app/browser/window/tab/item/page/client/request/search.rs b/src/app/browser/window/tab/item/page/client/request/search.rs deleted file mode 100644 index 56f97012..00000000 --- a/src/app/browser/window/tab/item/page/client/request/search.rs +++ /dev/null @@ -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, - ) -} diff --git a/src/app/browser/window/tab/item/page/client/response.rs b/src/app/browser/window/tab/item/page/client/response.rs deleted file mode 100644 index 87677fb2..00000000 --- a/src/app/browser/window/tab/item/page/client/response.rs +++ /dev/null @@ -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), -} diff --git a/src/app/browser/window/tab/item/page/client/response/certificate.rs b/src/app/browser/window/tab/item/page/client/response/certificate.rs deleted file mode 100644 index 2920a6ce..00000000 --- a/src/app/browser/window/tab/item/page/client/response/certificate.rs +++ /dev/null @@ -1,7 +0,0 @@ -use gtk::glib::GString; - -pub enum Certificate { - Invalid { title: GString }, - Request { title: GString }, - Unauthorized { title: GString }, -} diff --git a/src/app/browser/window/tab/item/page/client/response/failure.rs b/src/app/browser/window/tab/item/page/client/response/failure.rs deleted file mode 100644 index 3417086e..00000000 --- a/src/app/browser/window/tab/item/page/client/response/failure.rs +++ /dev/null @@ -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, - }, -} diff --git a/src/app/browser/window/tab/item/page/client/response/input.rs b/src/app/browser/window/tab/item/page/client/response/input.rs deleted file mode 100644 index 3993d908..00000000 --- a/src/app/browser/window/tab/item/page/client/response/input.rs +++ /dev/null @@ -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), -} diff --git a/src/app/browser/window/tab/item/page/client/response/input/titan.rs b/src/app/browser/window/tab/item/page/client/response/input/titan.rs deleted file mode 100644 index 2d6f583c..00000000 --- a/src/app/browser/window/tab/item/page/client/response/input/titan.rs +++ /dev/null @@ -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) + 'static) { - self.stream.output_stream().write_bytes_async( - &data, - Priority::DEFAULT, - Some(&self.cancellable), - callback, - ) - } -} diff --git a/src/app/browser/window/tab/item/page/client/response/redirect.rs b/src/app/browser/window/tab/item/page/client/response/redirect.rs deleted file mode 100644 index 01dac73c..00000000 --- a/src/app/browser/window/tab/item/page/client/response/redirect.rs +++ /dev/null @@ -1,6 +0,0 @@ -use gtk::glib::Uri; - -pub enum Redirect { - Foreground(Uri), - Background(Uri), -} diff --git a/src/app/browser/window/tab/item/page/client/response/text.rs b/src/app/browser/window/tab/item/page/client/response/text.rs deleted file mode 100644 index 08ae39f5..00000000 --- a/src/app/browser/window/tab/item/page/client/response/text.rs +++ /dev/null @@ -1,6 +0,0 @@ -use gtk::glib::Uri; - -pub enum Text { - Gemini { base: Uri, data: String }, - Plain { data: String }, -} diff --git a/src/app/browser/window/tab/item/page/client/status.rs b/src/app/browser/window/tab/item/page/client/status.rs deleted file mode 100644 index 60c040e9..00000000 --- a/src/app/browser/window/tab/item/page/client/status.rs +++ /dev/null @@ -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)) - } - } - } -} diff --git a/src/app/browser/window/tab/item/page/client/status/failure.rs b/src/app/browser/window/tab/item/page/client/status/failure.rs deleted file mode 100644 index b8e76bb3..00000000 --- a/src/app/browser/window/tab/item/page/client/status/failure.rs +++ /dev/null @@ -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" - ) - } - } - } - } -} diff --git a/src/app/browser/window/tab/item/page/client/status/gemini.rs b/src/app/browser/window/tab/item/page/client/status/gemini.rs deleted file mode 100644 index 84854b63..00000000 --- a/src/app/browser/window/tab/item/page/client/status/gemini.rs +++ /dev/null @@ -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)) - } - } - } -} diff --git a/src/app/browser/window/tab/item/page/input.rs b/src/app/browser/window/tab/item/page/input.rs index d959d9c0..75672da5 100644 --- a/src/app/browser/window/tab/item/page/input.rs +++ b/src/app/browser/window/tab/item/page/input.rs @@ -1,6 +1,5 @@ mod response; mod sensitive; -mod titan; mod widget; use super::TabAction; @@ -8,7 +7,6 @@ use gtk::glib::Uri; use response::Response; use sensitive::Sensitive; use std::rc::Rc; -use titan::Titan; use widget::Widget; pub struct Input { @@ -64,13 +62,4 @@ impl Input { .g_box, )); } - - pub fn set_new_titan( - &self, - titan: super::client::response::input::Titan, - callback: impl Fn(Result) + 'static, - ) { - self.widget - .update(Some(&Titan::build(titan, callback).widget.g_box)); - } } diff --git a/src/app/browser/window/tab/item/page/input/titan.rs b/src/app/browser/window/tab/item/page/input/titan.rs deleted file mode 100644 index 27598850..00000000 --- a/src/app/browser/window/tab/item/page/input/titan.rs +++ /dev/null @@ -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, -} - -impl Titan { - // Constructors - - /// Build new `Self` - pub fn build( - titan: super::super::client::response::input::Titan, - callback: impl Fn(Result) + '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 } - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/control.rs b/src/app/browser/window/tab/item/page/input/titan/control.rs deleted file mode 100644 index 87151085..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/control.rs +++ /dev/null @@ -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, - pub send: Rc, - pub widget: Rc, -} - -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) { - // Update children components - self.counter.update(bytes); - self.send.update(match bytes { - Some(left) => left > 0, - None => false, - }); - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/counter.rs b/src/app/browser/window/tab/item/page/input/titan/control/counter.rs deleted file mode 100644 index 6347eadd..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/control/counter.rs +++ /dev/null @@ -1,29 +0,0 @@ -mod widget; - -use widget::Widget; - -use std::rc::Rc; - -pub struct Counter { - pub widget: Rc, -} - -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) { - self.widget.update(bytes); - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/counter/widget.rs b/src/app/browser/window/tab/item/page/input/titan/control/counter/widget.rs deleted file mode 100644 index 2c754acf..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/control/counter/widget.rs +++ /dev/null @@ -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) { - 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), - } - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/send.rs b/src/app/browser/window/tab/item/page/input/titan/control/send.rs deleted file mode 100644 index 299b298c..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/control/send.rs +++ /dev/null @@ -1,27 +0,0 @@ -mod widget; -use widget::Widget; - -use gtk::gio::SimpleAction; -use std::rc::Rc; - -pub struct Send { - pub widget: Rc, -} - -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); - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/send/widget.rs b/src/app/browser/window/tab/item/page/input/titan/control/send/widget.rs deleted file mode 100644 index bd0bf6e7..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/control/send/widget.rs +++ /dev/null @@ -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); - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/control/widget.rs b/src/app/browser/window/tab/item/page/input/titan/control/widget.rs deleted file mode 100644 index 7242866e..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/control/widget.rs +++ /dev/null @@ -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 } - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/form.rs b/src/app/browser/window/tab/item/page/input/titan/form.rs deleted file mode 100644 index e0bcdb3a..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/form.rs +++ /dev/null @@ -1,21 +0,0 @@ -mod widget; - -use widget::Widget; - -use gtk::gio::SimpleAction; -use std::rc::Rc; - -pub struct Form { - pub widget: Rc, -} - -impl Form { - // Constructors - - /// Build new `Self` - pub fn build(action_update: SimpleAction) -> Self { - Self { - widget: Rc::new(Widget::new(action_update)), - } - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/form/widget.rs b/src/app/browser/window/tab/item/page/input/titan/form/widget.rs deleted file mode 100644 index 840f93b9..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/form/widget.rs +++ /dev/null @@ -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() - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/title.rs b/src/app/browser/window/tab/item/page/input/titan/title.rs deleted file mode 100644 index 383e6875..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/title.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod widget; - -use widget::Widget; - -use std::rc::Rc; - -pub struct Title { - pub widget: Rc, -} - -impl Title { - // Constructors - - /// Build new `Self` - pub fn build(title: Option<&str>) -> Self { - Self { - widget: Rc::new(Widget::build(title)), - } - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/title/widget.rs b/src/app/browser/window/tab/item/page/input/titan/title/widget.rs deleted file mode 100644 index 107ecce9..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/title/widget.rs +++ /dev/null @@ -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 } - } -} diff --git a/src/app/browser/window/tab/item/page/input/titan/widget.rs b/src/app/browser/window/tab/item/page/input/titan/widget.rs deleted file mode 100644 index 857b6cb4..00000000 --- a/src/app/browser/window/tab/item/page/input/titan/widget.rs +++ /dev/null @@ -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 } - } -} diff --git a/src/app/browser/window/tab/item/page/navigation.rs b/src/app/browser/window/tab/item/page/navigation.rs index 2210136d..6de3bfdb 100644 --- a/src/app/browser/window/tab/item/page/navigation.rs +++ b/src/app/browser/window/tab/item/page/navigation.rs @@ -66,7 +66,7 @@ impl Navigation { // Actions - pub fn update(&self, progress_fraction: Option) { + pub fn update(&self) { // init shared request value let request = self.request.strip_prefix(); @@ -74,10 +74,9 @@ impl Navigation { self.bookmark .update(self.profile.bookmark.get(&request).is_ok()); 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.request.update( - progress_fraction, self.profile .identity .gemini diff --git a/src/app/browser/window/tab/item/page/navigation/request.rs b/src/app/browser/window/tab/item/page/navigation/request.rs index 7f044b03..eded84f8 100644 --- a/src/app/browser/window/tab/item/page/navigation/request.rs +++ b/src/app/browser/window/tab/item/page/navigation/request.rs @@ -29,8 +29,8 @@ impl Request { // Actions - pub fn update(&self, progress_fraction: Option, is_identity_active: bool) { - self.widget.update(progress_fraction, is_identity_active); + pub fn update(&self, is_identity_active: bool) { + self.widget.update(is_identity_active); } pub fn clean( @@ -94,19 +94,19 @@ impl Request { // Setters - pub fn to_download(&self) { + pub fn into_download(&self) { self.widget.entry.set_text(&self.download()); } - pub fn to_source(&self) { + pub fn into_source(&self) { self.widget.entry.set_text(&self.source()); } // 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 - pub fn uri(&self) -> Option { + pub fn as_uri(&self) -> Option { match Uri::parse(&strip_prefix(self.widget.entry.text()), UriFlags::NONE) { Ok(uri) => Some(uri), _ => None, diff --git a/src/app/browser/window/tab/item/page/navigation/request/widget.rs b/src/app/browser/window/tab/item/page/navigation/request/widget.rs index 3746a9b8..f1e4e707 100644 --- a/src/app/browser/window/tab/item/page/navigation/request/widget.rs +++ b/src/app/browser/window/tab/item/page/navigation/request/widget.rs @@ -188,7 +188,7 @@ impl Widget { Ok(()) } - pub fn update(&self, progress_fraction: Option, is_identity_active: bool) { + pub fn update(&self, is_identity_active: bool) { // Update primary icon self.entry .first_child() @@ -229,46 +229,46 @@ impl Widget { } // Update progress - // * skip update animation for None value - if let Some(value) = 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(); + // * @TODO skip update animation for None value + let value = self.entry.progress_fraction(); - // Frame - move || { - // Animate - if *progress.fraction.borrow() > entry.progress_fraction() { - entry.set_progress_fraction( - // Currently, here is no outrange validation, seems that wrapper make this work @TODO - entry.progress_fraction() + PROGRESS_ANIMATION_STEP, - ); - return ControlFlow::Continue; - } - // Deactivate - progress.source_id.replace(None); + // 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(); - // 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 + // Frame + move || { + // Animate + if *progress.fraction.borrow() > entry.progress_fraction() { + entry.set_progress_fraction( + // Currently, here is no outrange validation, seems that wrapper make this work @TODO + entry.progress_fraction() + PROGRESS_ANIMATION_STEP, + ); + 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 + } + }, + ))); } } } diff --git a/src/app/browser/window/tab/item/page/status.rs b/src/app/browser/window/tab/item/page/status.rs index bef4d7b7..72a071a9 100644 --- a/src/app/browser/window/tab/item/page/status.rs +++ b/src/app/browser/window/tab/item/page/status.rs @@ -1,12 +1,10 @@ // Global dependencies -use super::client::{status::Gemini, Status as Client}; use crate::tool::format_time; use gtk::glib::DateTime; use std::fmt::{Display, Formatter, Result}; /// `Page` status pub enum Status { - Client(Client), Failure { time: DateTime }, Input { time: DateTime }, Loading { time: DateTime }, @@ -16,43 +14,9 @@ pub enum Status { 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 { - 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 { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Client(client) => { - write!(f, "{client}") - } Self::Failure { time } => { write!(f, "[{}] Failure", format_time(time)) }