diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index a4885df2..e091343a 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -3,7 +3,6 @@ mod content; mod database; mod error; mod input; -mod mode; // @TODO deprecated mod navigation; mod search; mod status; @@ -13,20 +12,20 @@ use client::Client; use content::Content; use error::Error; use input::Input; -use mode::Mode; use navigation::Navigation; use search::Search; use status::Status; use widget::Widget; use super::{Action as TabAction, BrowserAction, Profile, WindowAction}; +use crate::tool::now; use gtk::{ gdk::Texture, gdk_pixbuf::Pixbuf, - gio::SocketClientEvent, - glib::{gformat, GString, Priority, Uri, UriFlags, UriHideFlags}, - prelude::{EditableExt, FileExt, SocketClientExt}, + gio::Cancellable, + glib::{gformat, GString, Priority, Uri}, + prelude::{EditableExt, FileExt}, }; use sqlite::Transaction; use std::{cell::RefCell, rc::Rc, time::Duration}; @@ -81,20 +80,30 @@ impl Page { &input.widget.clamp, )); - //let meta = Rc::new(Meta::new(Status::New, gformat!("New 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(), profile: profile.clone(), - status: Rc::new(RefCell::new(Status::New)), title: Rc::new(RefCell::new(gformat!("New page"))), // Actions browser_action: browser_action.clone(), tab_action: tab_action.clone(), window_action: window_action.clone(), // Components - client: Rc::new(Client::new()), + client, + status, content, search, input, @@ -120,7 +129,7 @@ impl Page { result } - /// Request `Escape` action for child components + /// Request `Escape` action for all page components pub fn escape(&self) { self.search.hide() } @@ -163,8 +172,7 @@ impl Page { } } - /// Main loader for different protocols, that routed by scheme - /// * every protocol has it own (children) method implementation + /// Main loader for current `navigation` value pub fn load(&self, is_history: bool) { // Move focus out from navigation entry self.browser_action @@ -178,109 +186,322 @@ impl Page { self.search.unset(); self.input.unset(); - // Try redirect request - let request = if let Some(redirect) = self.client.redirect.last() { - // Gemini protocol may provide background (temporarily) redirects - if redirect.is_foreground { - self.navigation - .request - .widget - .entry - .set_text(&redirect.request.to_string()); - } - - // Return value from redirection holder - Mode::from(&redirect.request.to_str(), None /*redirect.referrer*/) // @TODO - } else { - // Reset redirect counter as request value taken from user input - self.client.redirect.clear(); - - // Return value from navigation entry - Mode::from(&self.navigation.request.widget.entry.text(), None) - }; - // Update - self.status.replace(Status::Reload); + self.status.replace(Status::Loading { time: now() }); self.title.replace(gformat!("Loading..")); self.browser_action.update.activate(Some(&self.id)); - // Route by `Mode` - match request { - Mode::Default(ref uri) | Mode::Download(ref uri) | Mode::Source(ref uri) => { - // Route by scheme - match uri.scheme().as_str() { - "file" => todo!(), - "gemini" => { - let (uri, is_download, is_source) = match request { - Mode::Default(uri) => (uri, false, false), - Mode::Download(uri) => (uri, true, false), - Mode::Source(uri) => (uri, false, true), - _ => panic!(), - }; - self.load_gemini(uri, is_download, is_source, is_history) - } - "titan" => { - // Toggle input form - // @TODO self.input.set_new_titan(|data|{}); + if is_history { + snap_history(&self.profile, &self.navigation, None); // @TODO + } - // Update meta - self.status.replace(Status::Input); - self.title.replace(gformat!("Titan input")); + use client::response::{Certificate, Failure, Input}; + use client::Response; - // Update page - self.browser_action.update.activate(Some(&self.id)); - } - scheme => { - // Add history record - if is_history { - snap_history(&self.profile, &self.navigation, Some(uri)); + self.client + .request_async(&self.navigation.request.widget.entry.text(), { + let browser_action = self.browser_action.clone(); + let content = self.content.clone(); + let id = self.id.clone(); + let input = self.input.clone(); + let navigation = self.navigation.clone(); + let search = self.search.clone(); + let status = self.status.clone(); + let tab_action = self.tab_action.clone(); + let title = self.title.clone(); + let window_action = self.window_action.clone(); + move |response| { + match response { + Response::Certificate(certificate) => match certificate { + 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_page.set_description(Some(&certificate_title)); + + // Update meta + status.replace(Status::Success { time: now() }); + title.replace(status_page.title()); + + // Update window + browser_action.update.activate(Some(&id)); + } + }, + Response::Failure(failure) => match failure { + Failure::Status { message } + | Failure::Mime { message } + | Failure::Error { message } => { + // Update widget + let status_page = content.to_status_failure(); + status_page.set_description(Some(&message)); + + // Update meta + status.replace(Status::Failure { time: now() }); + title.replace(status_page.title()); + + // Update window + browser_action.update.activate(Some(&id)); + } + }, + Response::Input(response_input) => match response_input { + Input::Response { + base, + title: response_title, + } => { + input.set_new_response( + tab_action.clone(), + base, + Some(&response_title), + Some(1024), + ); + + status.replace(Status::Input { time: now() }); + title.replace(response_title); + + browser_action.update.activate(Some(&id)); + } + Input::Sensitive { + base, + title: response_title, + } => { + input.set_new_sensitive( + tab_action.clone(), + base, + Some(&response_title), + Some(1024), + ); + + status.replace(Status::Input { time: now() }); + title.replace(response_title); + + browser_action.update.activate(Some(&id)); + } + Input::Titan { base } => { + input.set_new_titan(move |data| {}); // @TODO + + status.replace(Status::Input { time: now() }); + title.replace(gformat!("Titan input")); // @TODO + + browser_action.update.activate(Some(&id)); + } + }, + Response::Redirect { + request, + is_foreground, + } => { + // Some protocols may support foreground redirects + // for example status code `31` in Gemini + if is_foreground { + navigation + .request + .widget + .entry + .set_text(&request.to_string()); + } + + // @TODO request_async } + Response::Gemtext { base, source, is_source_request } => { + let widget = if is_source_request { + content.to_text_source(&source) + } else { + content.to_text_gemini(&base, &source) + }; - // Update widget - let status = self.content.to_status_failure(); - status.set_description(Some(&format!("Scheme `{scheme}` not supported"))); + // Connect `TextView` widget, update `search` model + search.set(Some(widget.text_view)); - // Update meta - self.status.replace(Status::Failure); - self.title.replace(status.title()); + // Update page meta + status.replace(Status::Success { time: now() }); + title.replace(match widget.meta.title { + Some(title) => title.into(), // @TODO + None => uri_to_title(&base), + }); - // Update window - self.browser_action.update.activate(Some(&self.id)); + // Update window components + window_action.find.simple_action.set_enabled(true); + browser_action.update.activate(Some(&id)); + } + Response::Download { base, stream } => { + // @TODO use `client` wrapper + + let cancellable = Cancellable::new(); // @TODO share with `client` + + // Init download widget + let status_page = content.to_status_download( + &uri_to_title(&base), // grab default filename from base URI + &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) => { + gemini::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 + status.replace(Status::Success { time: now() }); + title.replace(status_page.title()); + + // Update window + browser_action.update.activate(Some(&id)); + } + Response::Stream { base, mime, stream } => 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 + ); + + // @TODO use `client` wrapper + + let cancellable = Cancellable::new(); // @TODO share with `client` + + // Asynchronously move `InputStream` data from `SocketConnection` into the local `MemoryInputStream` + // this action allows to count the bytes for loading widget and validate max size for incoming data + gemini::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_page.set_description(Some(&format!( + "Download: {total} bytes" + ))); + }, + { + let browser_action = browser_action.clone(); + let cancellable = cancellable.clone(); + let content = content.clone(); + let id = id.clone(); + let status = status.clone(); + let title = title.clone(); + let base = base.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 + status.replace(Status::Success { + time: now(), + }); + title.replace(uri_to_title(&base)); + + // Update page content + content.to_image( + &Texture::for_pixbuf(&buffer), + ); + + // Update window components + browser_action + .update + .activate(Some(&id)); + } + Err(e) => { + // Update widget + let status_page = + content.to_status_failure(); + status_page.set_description(Some( + e.message(), + )); + + // Update meta + status.replace(Status::Failure { + time: now(), + }); + title.replace(status_page.title()); + } + } + }, + ); + } + Err(e) => { + // Update widget + let status_page = content.to_status_failure(); + status_page.set_description(Some(&e.to_string())); + + // Update meta + status.replace(Status::Failure { time: now() }); + title.replace(status_page.title()); + } + } + }, + ); + } + _ => todo!(), // unexpected + } } } - } - Mode::Search(query) => { - // try autocomplete scheme and request it on successful resolve - // otherwise make search request @TODO optional search provider - self.navigation - .request - .to_gemini_async(500, Some(&self.client.cancellable()), { - let tab_action = self.tab_action.clone(); - move |result| { - tab_action.load.activate( - Some(&match result { - Some(url) => url, - None => gformat!( - "gemini://tlgs.one/search?{}", - Uri::escape_string(&query, None, false) - ), - }), - true, - ) - } - }); - } - }; + }); } /// Update `Self` witch children components pub fn update(&self) { // Update components - self.navigation.update(self.to_progress_fraction()); + self.navigation + .update(self.status.borrow().to_progress_fraction()); // @TODO self.content.update(); } - /// Cleanup `Self` session + /// Cleanup session for `Self` pub fn clean( &self, transaction: &Transaction, @@ -310,7 +531,7 @@ impl Page { app_browser_window_tab_item_id: i64, ) -> Result<(), String> { // Update status - self.status.replace(Status::SessionRestore); + self.status.replace(Status::SessionRestore { time: now() }); // Begin page restore match database::select(transaction, app_browser_window_tab_item_id) { @@ -329,7 +550,7 @@ impl Page { } // Update status - self.status.replace(Status::SessionRestored); + self.status.replace(Status::SessionRestored { time: now() }); Ok(()) } @@ -364,531 +585,17 @@ impl Page { self.title.borrow().clone() } - /// Convert `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 { - // Interpret status to progress fraction - match *self.status.borrow() { - Status::Reload | Status::SessionRestore => Some(0.0), - Status::Resolving => Some(0.1), - Status::Resolved => Some(0.2), - Status::Connecting => Some(0.3), - Status::Connected => Some(0.4), - Status::ProxyNegotiating => Some(0.5), - Status::ProxyNegotiated => Some(0.6), - Status::TlsHandshaking => Some(0.7), - Status::TlsHandshaked => Some(0.8), - Status::Complete => Some(0.9), - Status::Failure | Status::Redirect | Status::Success | Status::Input => Some(1.0), - Status::New | Status::SessionRestored => None, - } - } - /// Get `Self` loading status pub fn is_loading(&self) -> bool { - match self.to_progress_fraction() { + match self.status.borrow().to_progress_fraction() { Some(progress_fraction) => progress_fraction < 1.0, None => false, } } - - // Private helpers - - // @TODO move outside - fn load_gemini(&self, uri: Uri, is_download: bool, is_source: bool, is_history: bool) { - // Init local namespace - use gemini::client::connection::{response, Request}; - - // Init shared clones - let browser_action = self.browser_action.clone(); - let cancellable = self.client.cancellable(); - let content = self.content.clone(); - let id = self.id.clone(); - let input = self.input.clone(); - let title = self.title.clone(); - let status = self.status.clone(); - let navigation = self.navigation.clone(); - let profile = self.profile.clone(); - let search = self.search.clone(); - let tab_action = self.tab_action.clone(); - let window_action = self.window_action.clone(); - let redirect = self.client.redirect.clone(); - - // Listen for connection status updates - self.client.gemini.socket.connect_event({ - let id = id.clone(); - let status = status.clone(); - let update = browser_action.update.clone(); - move |_, event, _, _| { - status.replace(match event { - SocketClientEvent::Resolving => Status::Resolving, - SocketClientEvent::Resolved => Status::Resolved, - SocketClientEvent::Connecting => Status::Connecting, - SocketClientEvent::Connected => Status::Connected, - SocketClientEvent::ProxyNegotiating => Status::ProxyNegotiating, - SocketClientEvent::ProxyNegotiated => Status::ProxyNegotiated, - // TlsHandshaking have effect only for guest connections! - SocketClientEvent::TlsHandshaking => Status::TlsHandshaking, - SocketClientEvent::TlsHandshaked => Status::TlsHandshaked, - SocketClientEvent::Complete => Status::Complete, - _ => todo!(), // notice on API change - }); - update.activate(Some(&id)); - } - }); - - // Begin new socket request - self.client.gemini.request_async( - Request::gemini(uri.clone()), - Priority::DEFAULT, - cancellable.clone(), - // Search for user certificate match request - match self.profile.identity.gemini.match_scope(&uri.to_string()) { - Some(identity) => match identity.to_tls_certificate() { - Ok(certificate) => Some(certificate), - Err(e) => todo!("{e}"), - }, - None => None, - }, - move |result| match result { - Ok(response) => { - // Route by status - match response.meta.status { - // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected - response::meta::Status::Input | - response::meta::Status::SensitiveInput => { - // Format response - let header = match response.meta.data { - Some(data) => data.value, - None => gformat!("Input expected"), - }; - - // Toggle input form variant - match response.meta.status { - response::meta::Status::SensitiveInput => - input.set_new_sensitive( - tab_action.clone(), - uri.clone(), - Some(&header), - Some(1024), - ), - _ => - input.set_new_response( - tab_action.clone(), - uri.clone(), - Some(&header), - Some(1024), - ), - } - - // Update meta - status.replace(Status::Input); - title.replace(header); - - // Update page - browser_action.update.activate(Some(&id)); - }, - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 - response::meta::Status::Success => { - if is_history { - snap_history(&profile, &navigation, Some(&uri)); - } - if is_download { - // Init download widget - let status_page = content.to_status_download( - &uri_to_title(&uri), // grab default filename - &cancellable, - { - let cancellable = cancellable.clone(); - move |file, action| { - match file.replace( - None, - false, - gtk::gio::FileCreateFlags::NONE, - Some(&cancellable) - ) { - Ok(file_output_stream) => { - gemini::gio::file_output_stream::move_all_from_stream_async( - response.connection.stream(), - file_output_stream, - cancellable.clone(), - Priority::DEFAULT, - ( - 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 - status.replace(Status::Success); - title.replace(status_page.title()); - - // Update window - browser_action.update.activate(Some(&id)); - } else { // browse - match response.meta.mime.unwrap().value.to_lowercase().as_str() { - "text/gemini" => { - // Read entire input stream to buffer - response::data::Text::from_stream_async( - response.connection.stream(), - Priority::DEFAULT, - cancellable.clone(), - { - let browser_action = browser_action.clone(); - let content = content.clone(); - let id = id.clone(); - let search = search.clone(); - let status = status.clone(); - let title = title.clone(); - let uri = uri.clone(); - let window_action = window_action.clone(); - move |result| { - match result { - Ok(buffer) => { - // Set children component, - // extract title from meta parsed - let text_widget = if is_source { - content.to_text_source( - &buffer.data - ) - } else { - content.to_text_gemini( - &uri, - &buffer.data - ) - }; - - // Connect `TextView` widget, update `search` model - search.set(Some(text_widget.text_view)); - - // Update page meta - status.replace(Status::Success); - title.replace(match text_widget.meta.title { - Some(meta_title) => meta_title.into(), // @TODO - None => uri_to_title(&uri) - }); - - // Update window components - window_action.find.simple_action.set_enabled(true); - - browser_action.update.activate(Some(&id)); - } - Err(e) => { - // Update widget - let status_page = content.to_status_failure(); - status_page.set_description(Some(&e.to_string())); - - // Update meta - status.replace(Status::Failure); - title.replace(status_page.title()); - - // Update window - browser_action.update.activate(Some(&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 move `InputStream` data from `SocketConnection` into the local `MemoryInputStream` - // this action allows to count the bytes for loading widget and validate max size for incoming data - gemini::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_page.set_description( - Some(&gformat!("Download: {total} bytes")) - ); - }, - { - let browser_action = browser_action.clone(); - let cancellable = cancellable.clone(); - let content = content.clone(); - let id = id.clone(); - let status = status.clone(); - let title = title.clone(); - let uri = uri.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 - status.replace(Status::Success); - title.replace(uri_to_title(&uri)); - - // Update page content - content.to_image(&Texture::for_pixbuf(&buffer)); - - // Update window components - browser_action.update.activate(Some(&id)); - } - Err(e) => { - // Update widget - let status_page = content.to_status_failure(); - status_page.set_description(Some(e.message())); - - // Update meta - status.replace(Status::Failure); - title.replace(status_page.title()); - } - } - } - ); - }, - Err(e) => { - // Update widget - let status_page = content.to_status_failure(); - status_page.set_description(Some(&e.to_string())); - - // Update meta - status.replace(Status::Failure); - title.replace(status_page.title()); - } - } - } - ); - }, - mime => { - // Init children widget - let status_page = content.to_status_mime( - mime, - Some((tab_action.clone(), navigation.request.download())) - ); - - // Update page meta - status.replace(Status::Failure); - title.replace(status_page.title()); - - // Update window - browser_action.update.activate(Some(&id)); - }, - } - } - }, - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - response::meta::Status::Redirect | - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - response::meta::Status::PermanentRedirect => { - // Extract redirection URL from response data - match response.meta.data { - Some(unresolved_url) => { - // New URL from server MAY to be relative (according to the protocol specification), - // resolve to absolute URI gobject using current request as the base for parser: - // https://docs.gtk.org/glib/type_func.Uri.resolve_relative.html - match Uri::resolve_relative( - Some(&uri.to_string()), - unresolved_url.value.as_str(), - UriFlags::NONE, - ) { - Ok(resolved_url) => { - // Build valid URI from resolved URL string - // this conversion wanted to simply exclude `query` and `fragment` later (as restricted by protocol specification) - match Uri::parse(resolved_url.as_str(), UriFlags::NONE) { - Ok(resolved_uri) => { - // Client MUST prevent external redirects (by protocol specification) - if is_external_uri(&resolved_uri, &uri) { - // Update meta - status.replace(Status::Failure); - title.replace(gformat!("Oops")); - - // Show placeholder with manual confirmation to continue @TODO status page? - content.to_text_gemini( - &uri, - &gformat!( - "# Redirect issue\n\nExternal redirects not allowed by protocol\n\nContinue:\n\n=> {}", - resolved_uri.to_string() - ) - ); - // Client MUST limit the number of redirects they follow to 5 (by protocol specification) - } else if redirect.count() > 5 { - // Update meta - status.replace(Status::Failure); - title.replace(gformat!("Oops")); - - // Show placeholder with manual confirmation to continue @TODO status page? - content.to_text_gemini( - &uri, - &gformat!( - "# Redirect issue\n\nLimit the number of redirects reached\n\nContinue:\n\n=> {}", - resolved_uri.to_string() - ) - ); - // Redirection value looks valid, create new redirect (stored in meta `Redirect` holder) - // then call page reload action to apply it by the parental controller - } else { - redirect.add( - // skip query and fragment by protocol requirements - // @TODO review fragment specification - Uri::parse(&resolved_uri.to_string_partial( - UriHideFlags::FRAGMENT | UriHideFlags::QUERY - ), UriFlags::NONE).unwrap(), // @TODO handle - // referrer - Some(uri.clone()), - // set follow policy based on status code - matches!(response.meta.status, response::meta::Status::PermanentRedirect), - ); - - status.replace(Status::Redirect); // @TODO is this status really wanted here? - title.replace(gformat!("Redirect")); - - // Reload page to apply redirection (without history record request) - tab_action.load.activate(None, false); - } - }, - Err(e) => { - // Update widget - let status_page = content.to_status_failure(); - status_page.set_description(Some(&e.to_string())); - - // Update meta - status.replace(Status::Failure); - title.replace(status_page.title()); - } - } - } - Err(e) => { - // Update widget - let status_page = content.to_status_failure(); - status_page.set_description(Some(&e.to_string())); - - // Update meta - status.replace(Status::Failure); - title.replace(status_page.title()); - }, - } - }, - None => { - // Update widget - let status_page = content.to_status_failure(); - status_page.set_description(Some("Redirection target not defined")); - - // Update meta - status.replace(Status::Failure); - title.replace(status_page.title()); - }, - } - - browser_action.update.activate(Some(&id)); - }, - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - response::meta::Status::CertificateRequest | - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - response::meta::Status::CertificateUnauthorized | - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - response::meta::Status::CertificateInvalid => { - // Add history record - if is_history { - snap_history(&profile, &navigation, Some(&uri)); - } - - // Update widget - let status_page = content.to_status_identity(); - - status_page.set_description(Some(&match response.meta.data { - Some(data) => data.value, - None => match response.meta.status { - response::meta::Status::CertificateUnauthorized => gformat!("Certificate not authorized"), - response::meta::Status::CertificateInvalid => gformat!("Certificate not valid"), - _ => gformat!("Client certificate required") - }, - })); - - // Update meta - status.replace(Status::Success); - title.replace(status_page.title()); - - // Update window - browser_action.update.activate(Some(&id)); - } - _ => { - // Add history record - if is_history { - snap_history(&profile, &navigation, Some(&uri)); - } - - // Update widget - let status_page = content.to_status_failure(); - - status_page.set_description(Some(&match response.meta.data { - Some(data) => data.value, - None => gformat!("Status code not supported"), - })); - - // Update meta - status.replace(Status::Failure); - title.replace(status_page.title()); - - // Update window - browser_action.update.activate(Some(&id)); - } - } - }, - Err(e) => { - // Add history record - if is_history { - snap_history(&profile, &navigation, Some(&uri)); - } - - // Update widget - let status_page = content.to_status_failure(); - status_page.set_description(Some(&e.to_string())); - - // Update meta - status.replace(Status::Failure); - title.replace(status_page.title()); - - // Update window - browser_action.update.activate(Some(&id)); - } - } - ); - } } +// Private helpers + // Tools pub fn migrate(tx: &Transaction) -> Result<(), String> { @@ -921,19 +628,6 @@ fn uri_to_title(uri: &Uri) -> GString { } } -/// Compare `subject` with `base` -/// -/// Return `false` on scheme, port or host mismatch -fn is_external_uri(subject: &Uri, base: &Uri) -> bool { - if subject.scheme() != base.scheme() { - return true; - } - if subject.port() != base.port() { - return true; - } - subject.host() != base.host() -} - /// 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>) { diff --git a/src/app/browser/window/tab/item/page/client.rs b/src/app/browser/window/tab/item/page/client.rs index 7ef088e0..beed515d 100644 --- a/src/app/browser/window/tab/item/page/client.rs +++ b/src/app/browser/window/tab/item/page/client.rs @@ -1,63 +1,61 @@ +pub mod driver; mod feature; -mod redirect; -mod status; +pub mod response; +pub mod status; // Children dependencies +pub use driver::Driver; use feature::Feature; -use redirect::Redirect; -use status::Status; +pub use response::Response; +pub use status::Status; // Global dependencies +use crate::{tool::now, Profile}; use gtk::{gio::Cancellable, prelude::CancellableExt}; use std::{ cell::{Cell, RefCell}, rc::Rc, }; -/// Multi-client holder for single `Page` object -/// -/// Unlike init new client instance on every page load, -/// this struct creates single holder for different protocol drivers; -/// it also provides additional client-side features -/// e.g. session resumption or multi-thread connection management (depending of client type selected) +/// Multi-protocol client API for `Page` object pub struct Client { - // Shared reference to cancel async operations - // * keep it private to make sure that `status` member tracked properly cancellable: Cell, - // Redirect resolver for different protocols - pub redirect: Rc, - // Track update status status: Rc>, - // Drivers - pub gemini: gemini::Client, - // other clients.. -} - -impl Default for Client { - fn default() -> Self { - Self::new() - } + driver: Driver, } impl Client { // Constructors /// Create new `Self` - pub fn new() -> Self { + pub fn init(profile: &Rc, callback: impl Fn(Status) + 'static) -> Self { Self { cancellable: Cell::new(Cancellable::new()), - redirect: Rc::new(Redirect::new()), - status: Rc::new(RefCell::new(Status::cancellable())), // e.g. "ready to use" - gemini: gemini::Client::new(), + driver: Driver::init(profile, move |status| callback(Status::Driver(status))), + status: Rc::new(RefCell::new(Status::Cancellable { time: now() })), // e.g. "ready to use" } } // Actions + /// Begin new request + /// * the `query` as string, to support system routes (e.g. `source:` prefix) + pub fn request_async(&self, request: &str, callback: impl Fn(Response) + 'static) { + // Update client status + self.status.replace(Status::Request { + time: now(), + value: request.to_string(), + }); + + self.driver.feature_async( + Feature::from_string(request), + self.new_cancellable(), + Rc::new(callback), + ); + } + /// Get new [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) by cancel previous one - /// * this action wanted just because of `Cancelable` member constructed privately, - /// where some external components may depend to sync their related processes - pub fn cancellable(&self) -> Cancellable { + fn new_cancellable(&self) -> Cancellable { // Init new Cancellable let cancellable = Cancellable::new(); @@ -65,33 +63,12 @@ impl Client { let previous = self.cancellable.replace(cancellable.clone()); if !previous.is_cancelled() { previous.cancel(); - self.status.replace(Status::cancelled()); + self.status.replace(Status::Cancelled { time: now() }); } else { - self.status.replace(Status::cancellable()); + self.status.replace(Status::Cancellable { time: now() }); } // Done cancellable } - - /// Begin new request - /// * the `query` as string, to support system routing requests (e.g. `source:`) - pub fn request(&self, query: &str) { - self.status.replace(Status::request(query.to_string())); - - // Forcefully prevent infinitive redirection - // * this condition just to make sure that client will never stuck by driver implementation issue - if self.redirect.count() > redirect::LIMIT { - self.status - .replace(Status::failure_redirect_limit(redirect::LIMIT, true)); - // @TODO return; - } - - // Route request by protocol - match Feature::from_string(query) { - Feature::Default { request } - | Feature::Download { request } - | Feature::Source { request } => request.send(), // @TODO - } - } } diff --git a/src/app/browser/window/tab/item/page/client/driver.rs b/src/app/browser/window/tab/item/page/client/driver.rs new file mode 100644 index 00000000..294a7c58 --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/driver.rs @@ -0,0 +1,240 @@ +//! At this moment, the `Driver` contain only one protocol library, +//! by extending it features with new protocol, please make sub-module implementation + +mod redirect; +pub mod status; + +// Local dependencies +use redirect::Redirect; +pub use status::Status; + +// Global dependencies +use super::{feature, response, Feature, Response}; +use crate::{tool::now, Profile}; +use gtk::{ + gio::{Cancellable, SocketClientEvent}, + glib::{gformat, Priority, Uri}, + prelude::SocketClientExt, +}; +use std::rc::Rc; + +pub struct Driver { + /// Profile reference required for Gemini protocol auth (match request) + profile: Rc, + /// Redirect resolver for different protocols + redirect: Rc, + /// Supported clients + gemini: gemini::Client, + // other clients here.. +} + +impl Driver { + pub fn init(profile: &Rc, callback: impl Fn(Status) + 'static) -> Self { + // Init protocol driver libraries + let gemini = gemini::Client::new(); + + // Translate driver status to `Status` + + // Gemini + gemini.socket.connect_event(move |_, event, _, _| { + callback(match event { + SocketClientEvent::Resolving => Status::Resolving { time: now() }, + SocketClientEvent::Resolved => Status::Resolved { time: now() }, + SocketClientEvent::Connecting => Status::Connecting { time: now() }, + SocketClientEvent::Connected => Status::Connected { time: now() }, + SocketClientEvent::ProxyNegotiating => Status::ProxyNegotiating { time: now() }, + SocketClientEvent::ProxyNegotiated => Status::ProxyNegotiated { time: now() }, + // * `TlsHandshaking` | `TlsHandshaked` has effect only for guest connections! + SocketClientEvent::TlsHandshaking => Status::TlsHandshaking { time: now() }, + SocketClientEvent::TlsHandshaked => Status::TlsHandshaked { time: now() }, + SocketClientEvent::Complete => Status::Complete { time: now() }, + _ => todo!(), // alert on API change + }) + }); + + // other client listeners here.. + + // Done + Self { + profile: profile.clone(), + redirect: Rc::new(Redirect::new()), + gemini, + } + } + + pub fn feature_async( + &self, + feature: Feature, + cancellable: Cancellable, + callback: Rc, + ) { + match feature { + Feature::Download { request } => match request { + feature::Request::Gemini { uri } => { + request_gemini_async(self, uri.clone(), cancellable.clone(), move |result| { + match result { + Ok(response) => callback(Response::Download { + base: uri.clone(), + stream: response.connection.stream(), + }), + Err(e) => callback(Response::Failure(response::Failure::Error { + message: e.to_string(), + })), + } + }) + } + _ => todo!(), + }, + Feature::Default { request } => match request { + feature::Request::Gemini { uri } => { + request_gemini_async(self, uri.clone(), cancellable.clone(), move |result| { + handle_gemini( + result, + uri.clone(), + cancellable.clone(), + false, + callback.clone(), + ) + }) + } + feature::Request::Titan { .. } => todo!(), + feature::Request::Undefined => todo!(), + }, + Feature::Source { request } => match request { + feature::Request::Gemini { uri } => { + request_gemini_async(self, uri.clone(), cancellable.clone(), move |result| { + handle_gemini( + result, + uri.clone(), + cancellable.clone(), + true, + callback.clone(), + ) + }) + } + feature::Request::Titan { .. } => todo!(), + feature::Request::Undefined => todo!(), + }, + } + } +} + +/// Shared request interface for Gemini protocol +fn request_gemini_async( + driver: &Driver, + uri: Uri, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, +) { + driver.gemini.request_async( + gemini::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 driver.profile.identity.gemini.match_scope(&uri.to_string()) { + Some(identity) => match identity.to_tls_certificate() { + Ok(certificate) => Some(certificate), + Err(_) => todo!(), + }, + None => None, + }, + move |result| callback(result), + ) +} + +/// Shared handler for Gemini `Result` +/// * same implementation for Gemini and Titan protocols response +fn handle_gemini( + result: Result, + base: Uri, + cancellable: Cancellable, + is_source_request: bool, // @TODO yet partial implementation + callback: Rc, +) { + use gemini::client::connection::response::{data::Text, meta::Status}; + match result { + Ok(response) => match response.meta.status { + // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected + Status::Input => callback(Response::Input(response::Input::Response { + base, + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Input expected"), + }, + })), + Status::SensitiveInput => callback(Response::Input(response::Input::Sensitive { + base, + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Input expected"), + }, + })), + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 + Status::Success => { + let mime = response.meta.mime.unwrap().value.to_lowercase(); + match mime.as_str() { + "text/gemini" => Text::from_stream_async( + response.connection.stream(), + Priority::DEFAULT, + cancellable, + move |result| match result { + Ok(text) => callback(Response::Gemtext { + base, + source: text.data, + is_source_request, + }), + Err(_) => todo!(), + }, + ), + "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { + callback(Response::Stream { + base, + mime, + stream: response.connection.stream(), + }) + } + mime => callback(Response::Failure(response::Failure::Mime { + message: format!("Undefined content type `{mime}`"), + })), + } // @TODO handle `None` + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection + Status::Redirect | Status::PermanentRedirect => todo!(), + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 + Status::CertificateRequest => { + callback(Response::Certificate(response::Certificate::Request { + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Client certificate required"), + }, + })) + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized + Status::CertificateUnauthorized => { + callback(Response::Certificate(response::Certificate::Request { + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Certificate not authorized"), + }, + })) + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid + Status::CertificateInvalid => { + callback(Response::Certificate(response::Certificate::Request { + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Certificate not valid"), + }, + })) + } + status => callback(Response::Failure(response::Failure::Status { + message: format!("Undefined status code `{:?}`", status), // @TODO implement display trait for `ggemini` lib + })), + }, + Err(e) => callback(Response::Failure(response::Failure::Error { + message: e.to_string(), + })), + } +} diff --git a/src/app/browser/window/tab/item/page/client/redirect.rs b/src/app/browser/window/tab/item/page/client/driver/redirect.rs similarity index 100% rename from src/app/browser/window/tab/item/page/client/redirect.rs rename to src/app/browser/window/tab/item/page/client/driver/redirect.rs diff --git a/src/app/browser/window/tab/item/page/client/redirect/item.rs b/src/app/browser/window/tab/item/page/client/driver/redirect/item.rs similarity index 100% rename from src/app/browser/window/tab/item/page/client/redirect/item.rs rename to src/app/browser/window/tab/item/page/client/driver/redirect/item.rs diff --git a/src/app/browser/window/tab/item/page/client/driver/status.rs b/src/app/browser/window/tab/item/page/client/driver/status.rs new file mode 100644 index 00000000..8a9eb692 --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/driver/status.rs @@ -0,0 +1,51 @@ +// Global dependencies +use crate::tool::format_time; +use gtk::glib::DateTime; +use std::fmt::{Display, Formatter, Result}; + +/// Shared asset for `Driver` statuses +pub enum Status { + 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 Status { + 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/client/feature.rs b/src/app/browser/window/tab/item/page/client/feature.rs index e5d6e1d9..b618e60a 100644 --- a/src/app/browser/window/tab/item/page/client/feature.rs +++ b/src/app/browser/window/tab/item/page/client/feature.rs @@ -1,14 +1,7 @@ -//! Feature components in development, -//! this asset initiated as the attempt to reduce current `Page` code size -//! and delegate different protocol features to specified drivers under this location with itself implementation -// @TODO cleanup this message on complete +pub mod request; +pub use request::Request; -mod request; - -// Local dependencies -use request::Request; - -/// Features route for `Client` +/// Feature wrapper for client `Request` pub enum Feature { /// Common feature for protocol selected (e.g. browser view) Default { request: Request }, @@ -22,21 +15,21 @@ impl Feature { // Constructors /// Parse new `Self` from string - pub fn from_string(request: &str) -> Self { - if let Some(postfix) = request.strip_prefix("download:") { + pub fn from_string(query: &str) -> Self { + if let Some(postfix) = query.strip_prefix("download:") { return Self::Download { request: Request::from_string(postfix), }; } - if let Some(postfix) = request.strip_prefix("source:") { + if let Some(postfix) = query.strip_prefix("source:") { return Self::Source { request: Request::from_string(postfix), }; } Self::Default { - request: Request::from_string(request), + request: Request::from_string(query), } } } diff --git a/src/app/browser/window/tab/item/page/client/feature/request.rs b/src/app/browser/window/tab/item/page/client/feature/request.rs index 2c009168..b4f101c1 100644 --- a/src/app/browser/window/tab/item/page/client/feature/request.rs +++ b/src/app/browser/window/tab/item/page/client/feature/request.rs @@ -27,15 +27,4 @@ impl Request { }, } } - - // Actions - - /// Send request using protocol driver constructed - pub fn send(&self) { - match self { - Request::Gemini { uri } => todo!("{uri}"), - Request::Titan { uri } => todo!("{uri}"), - Request::Undefined => todo!(), - } - } } diff --git a/src/app/browser/window/tab/item/page/client/response.rs b/src/app/browser/window/tab/item/page/client/response.rs new file mode 100644 index 00000000..d0a327e8 --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/response.rs @@ -0,0 +1,36 @@ +pub mod certificate; +pub mod failure; +pub mod input; + +pub use certificate::Certificate; +pub use failure::Failure; +pub use input::Input; + +use gtk::{ + gio::IOStream, + glib::{GString, Uri}, +}; + +pub enum Response { + Certificate(Certificate), + Download { + base: Uri, + stream: IOStream, + }, + Failure(Failure), + Gemtext { + base: Uri, + source: GString, + is_source_request: bool, + }, + Input(Input), + Redirect { + request: Uri, + is_foreground: bool, + }, + Stream { + base: Uri, + mime: String, + stream: IOStream, + }, +} 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 new file mode 100644 index 00000000..2920a6ce --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/response/certificate.rs @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..1549237a --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/response/failure.rs @@ -0,0 +1,5 @@ +pub enum Failure { + Status { message: String }, + Mime { message: String }, + 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 new file mode 100644 index 00000000..0494994f --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/response/input.rs @@ -0,0 +1,7 @@ +use gtk::glib::{GString, Uri}; + +pub enum Input { + Response { base: Uri, title: GString }, + Sensitive { base: Uri, title: GString }, + Titan { base: Uri }, +} diff --git a/src/app/browser/window/tab/item/page/client/status.rs b/src/app/browser/window/tab/item/page/client/status.rs index 664e1b8c..e153a753 100644 --- a/src/app/browser/window/tab/item/page/client/status.rs +++ b/src/app/browser/window/tab/item/page/client/status.rs @@ -1,88 +1,54 @@ -mod failure; +pub mod failure; // Children dependencies -use failure::Failure; +pub use failure::Failure; // Global dependencies -use gtk::glib::{DateTime, GString}; +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 { event: DateTime }, + Cancellable { time: DateTime }, /// Operation cancelled, new `Cancellable` required to continue - Cancelled { event: DateTime }, + Cancelled { time: DateTime }, + /// Protocol driver updates + Driver(super::driver::Status), /// Something went wrong - Failure { event: DateTime, failure: Failure }, + Failure { time: DateTime, failure: Failure }, /// New `request` begin - Request { event: DateTime, value: String }, -} - -impl Status { - // Constructors - - /// Create new `Self::Cancellable` - pub fn cancellable() -> Self { - Self::Cancellable { event: now() } - } - - /// Create new `Self::Cancelled` - pub fn cancelled() -> Self { - Self::Cancelled { event: now() } - } - - /// Create new `Self::Failure` as `Failure::RedirectCount` - pub fn failure_redirect_limit(count: usize, is_global: bool) -> Self { - Self::Failure { - event: now(), - failure: Failure::redirect_count(count, is_global), - } - } - - /// Create new `Self::Request` - pub fn request(value: String) -> Self { - Self::Request { - event: now(), - value, - } - } + Request { time: DateTime, value: String }, } impl Display for Status { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Cancellable { event } => { + Self::Cancellable { time } => { write!( f, "[{}] Ready to use (or cancel from outside)", - format_time(event) + format_time(time) ) } - Self::Cancelled { event } => { + Self::Cancelled { time } => { write!( f, "[{}] Operation cancelled, new `Cancellable` required to continue", - format_time(event) + format_time(time) ) } - Self::Failure { event, failure } => { - write!(f, "[{}] Failure: {failure}", format_time(event)) + Self::Driver(status) => { + write!(f, "{status}") } - Self::Request { event, value } => { - write!(f, "[{}] Request `{value}`...", format_time(event)) + Self::Failure { time, failure } => { + write!(f, "[{}] Failure: {failure}", format_time(time)) + } + Self::Request { time, value } => { + write!(f, "[{}] Request `{value}`...", format_time(time)) } } } } - -/// Format given [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) -fn format_time(t: &DateTime) -> GString { - t.format_iso8601().unwrap() // @TODO handle? -} - -/// Get current [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) -fn now() -> DateTime { - DateTime::now_local().unwrap() // @TODO handle? -} diff --git a/src/app/browser/window/tab/item/page/mode.rs b/src/app/browser/window/tab/item/page/mode.rs deleted file mode 100644 index 18447c89..00000000 --- a/src/app/browser/window/tab/item/page/mode.rs +++ /dev/null @@ -1,51 +0,0 @@ -use gtk::glib::{GString, Uri, UriFlags}; - -/// Page type for `Page` with optional value parsed -pub enum Mode { - Default(Uri), - Download(Uri), - Source(Uri), - Search(String), -} - -impl Mode { - // Constructors - - /// Create new `Self` from `request` string - /// * if some `referrer` given, make additional check in previous request - pub fn from(request: &str, referrer: Option<&GString>) -> Self { - // check in request - if let Some(postfix) = request.strip_prefix("source:") { - if let Ok(uri) = Uri::parse(postfix, UriFlags::NONE) { - return Self::Source(uri); - } - } - - if let Some(postfix) = request.strip_prefix("download:") { - if let Ok(uri) = Uri::parse(postfix, UriFlags::NONE) { - return Self::Download(uri); - } - } - - // check in referrer @TODO tmp - if referrer.is_some_and(|this| this.starts_with("source:")) { - if let Ok(uri) = Uri::parse(request, UriFlags::NONE) { - return Self::Source(uri); - } - } - - if referrer.is_some_and(|this| this.starts_with("download:")) { - if let Ok(uri) = Uri::parse(request, UriFlags::NONE) { - return Self::Download(uri); - } - } - - // is default - if let Ok(uri) = Uri::parse(request, UriFlags::NONE) { - return Self::Default(uri); - } - - // is search - Self::Search(request.to_string()) - } -} diff --git a/src/app/browser/window/tab/item/page/status.rs b/src/app/browser/window/tab/item/page/status.rs index b5b3face..4bb7fbe8 100644 --- a/src/app/browser/window/tab/item/page/status.rs +++ b/src/app/browser/window/tab/item/page/status.rs @@ -1,22 +1,50 @@ +/// Global dependencies +use super::client::{driver::Status as Driver, Status as Client}; +use gtk::glib::DateTime; + /// `Page` status -/// * not same as the Gemini status! -#[derive(Debug, Clone)] pub enum Status { - Complete, - Connected, - Connecting, - Failure, - Input, - New, - ProxyNegotiated, - ProxyNegotiating, - Redirect, - Reload, - Resolved, - Resolving, - SessionRestore, - SessionRestored, - Success, - TlsHandshaked, - TlsHandshaking, + Client(Client), + Failure { time: DateTime }, + Input { time: DateTime }, + Loading { time: DateTime }, + New { time: DateTime }, + Redirect { time: DateTime }, + SessionRestore { time: DateTime }, + SessionRestored { 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 { + 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::Driver(status) => match status { + Driver::Resolving { .. } => Some(0.1), + Driver::Resolved { .. } => Some(0.2), + Driver::Connecting { .. } => Some(0.3), + Driver::Connected { .. } => Some(0.4), + Driver::ProxyNegotiating { .. } => Some(0.5), + Driver::ProxyNegotiated { .. } => Some(0.6), + Driver::TlsHandshaking { .. } => Some(0.7), + Driver::TlsHandshaked { .. } => Some(0.8), + Driver::Complete { .. } => Some(0.9), + }, + }, + Self::Failure { .. } + | Self::Success { .. } + | Self::Redirect { .. } + | Self::Input { .. } => Some(1.0), + Self::New { .. } | Self::SessionRestored { .. } => None, + } + } } diff --git a/src/tool.rs b/src/tool.rs index b1ec934b..13c50508 100644 --- a/src/tool.rs +++ b/src/tool.rs @@ -1,5 +1,8 @@ //! Some shared helpers collection +// Global dependencies +use gtk::glib::{DateTime, GString, Uri}; + /// Format bytes to KB/MB/GB presentation pub fn format_bytes(value: usize) -> String { const KB: f32 = 1024.0; @@ -21,3 +24,24 @@ pub fn format_bytes(value: usize) -> String { format!("{:.2} GB", f / GB) } } + +/// Format given [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) +pub fn format_time(t: &DateTime) -> GString { + t.format_iso8601().unwrap() // @TODO handle? +} + +/// Get current [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) +pub fn now() -> DateTime { + DateTime::now_local().unwrap() // @TODO handle? +} + +/// Compare `subject` with `base` +pub fn _is_external(subject: &Uri, base: &Uri) -> bool { + if subject.scheme() != base.scheme() { + return true; + } + if subject.port() != base.port() { + return true; + } + subject.host() != base.host() +} // @TODO not in use