From f73b5c7956ef7c94a06fae301a602b8a742f9512 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Jan 2025 12:52:53 +0200 Subject: [PATCH] separate page handler from loader, draft some Titan features --- src/app/browser/window/tab/item/page.rs | 596 +++++++++--------- .../tab/item/page/client/response/input.rs | 24 +- .../item/page/client/response/input/titan.rs | 6 + src/app/browser/window/tab/item/page/input.rs | 15 +- .../window/tab/item/page/input/titan.rs | 12 +- 5 files changed, 343 insertions(+), 310 deletions(-) create mode 100644 src/app/browser/window/tab/item/page/client/response/input/titan.rs diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 427d13a7..f4689832 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -325,9 +325,6 @@ pub fn history_forward(page: &Rc) { /// Page load function with recursive redirection support pub fn load(page: &Rc, request: Option<&str>, is_history: bool) { - use client::response::{Certificate, Failure, Input, Redirect}; - use client::Response; - // Move focus out from navigation entry page.browser_action .escape @@ -354,284 +351,319 @@ pub fn load(page: &Rc, request: Option<&str>, is_history: bool) { page.client.request(query, { let page = page.clone(); - move |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.status.replace(Status::Input { time: now() }); - page.title.replace(response_title); - - page.browser_action.update.activate(Some(&page.id)); - } - Input::Sensitive { - base, - title: response_title, - } => { - page.input.set_new_sensitive( - page.tab_action.clone(), - base, - Some(&response_title), - Some(1024), - ); - - page.status.replace(Status::Input { time: now() }); - page.title.replace(response_title); - - page.browser_action.update.activate(Some(&page.id)); - } - Input::Titan { .. } => { - page.input.set_new_titan(move |_| todo!()); - - page.status.replace(Status::Input { time: now() }); - page.title.replace("Titan input".into()); - - page.browser_action.update.activate(Some(&page.id)); - } - }, - Response::Redirect(this) => match this { - Redirect::Background(request) => { - load(&page, Some(&request.as_uri().to_string()), false) - }, - Redirect::Foreground(request) => { - page.navigation - .request - .widget - .entry - .set_text(&request.as_uri().to_string()); - load(&page, Some(&request.as_uri().to_string()), false); - } - } - Response::TextGemini { base, source, is_source_request } => { - let widget = if is_source_request { - page.content.to_text_source(&source) - } else { - page.content.to_text_gemini(&base, &source) - }; - - // 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)); - } - 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 - } - } - } + move |response| handle(&page, response) }); } + +/// Response handler for `Page` +/// * may call itself on Titan response +fn handle(page: &Rc, response: client::Response) { + use client::{ + response::{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.status.replace(Status::Input { time: now() }); + page.title.replace(response_title); + + page.browser_action.update.activate(Some(&page.id)); + } + Input::Sensitive { + base, + title: response_title, + } => { + page.input.set_new_sensitive( + page.tab_action.clone(), + base, + Some(&response_title), + Some(1024), + ); + + page.status.replace(Status::Input { time: now() }); + page.title.replace(response_title); + + page.browser_action.update.activate(Some(&page.id)); + } + 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.status.replace(Status::Input { time: now() }); + page.title.replace("Titan input".into()); + + page.browser_action.update.activate(Some(&page.id)); + } + }, + Response::Redirect(this) => match this { + Redirect::Background(request) => { + load(&page, Some(&request.as_uri().to_string()), false) + } + Redirect::Foreground(request) => { + page.navigation + .request + .widget + .entry + .set_text(&request.as_uri().to_string()); + load(&page, Some(&request.as_uri().to_string()), false); + } + }, + Response::TextGemini { + base, + source, + is_source_request, + } => { + let widget = if is_source_request { + page.content.to_text_source(&source) + } else { + page.content.to_text_gemini(&base, &source) + }; + + // 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)); + } + 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/response/input.rs b/src/app/browser/window/tab/item/page/client/response/input.rs index b46e84bd..3993d908 100644 --- a/src/app/browser/window/tab/item/page/client/response/input.rs +++ b/src/app/browser/window/tab/item/page/client/response/input.rs @@ -1,20 +1,10 @@ -use gtk::{ - gio::{Cancellable, IOStream}, - glib::{GString, Uri}, -}; +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 { - base: Uri, - cancellable: Cancellable, - stream: IOStream, - }, + 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 new file mode 100644 index 00000000..809f0975 --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/response/input/titan.rs @@ -0,0 +1,6 @@ +use gtk::gio::{Cancellable, IOStream}; + +pub struct Titan { + cancellable: Cancellable, + stream: IOStream, +} diff --git a/src/app/browser/window/tab/item/page/input.rs b/src/app/browser/window/tab/item/page/input.rs index cce564f1..d959d9c0 100644 --- a/src/app/browser/window/tab/item/page/input.rs +++ b/src/app/browser/window/tab/item/page/input.rs @@ -3,15 +3,14 @@ mod sensitive; mod titan; mod widget; +use super::TabAction; +use gtk::glib::Uri; use response::Response; use sensitive::Sensitive; +use std::rc::Rc; use titan::Titan; use widget::Widget; -use crate::app::browser::window::tab::item::Action as TabAction; -use gtk::glib::Uri; -use std::rc::Rc; - pub struct Input { pub widget: Rc, } @@ -66,8 +65,12 @@ impl Input { )); } - pub fn set_new_titan(&self, callback: impl Fn(&[u8]) + 'static) { + pub fn set_new_titan( + &self, + titan: super::client::response::input::Titan, + callback: impl Fn(Result) + 'static, + ) { self.widget - .update(Some(&Titan::build(callback).widget.g_box)); + .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 index 8d0071ff..27598850 100644 --- a/src/app/browser/window/tab/item/page/input/titan.rs +++ b/src/app/browser/window/tab/item/page/input/titan.rs @@ -5,11 +5,10 @@ mod widget; use control::Control; use form::Form; -use title::Title; -use widget::Widget; - use gtk::{gio::SimpleAction, glib::uuid_string_random}; use std::rc::Rc; +use title::Title; +use widget::Widget; pub struct Titan { // Components @@ -20,7 +19,10 @@ impl Titan { // Constructors /// Build new `Self` - pub fn build(on_sent: impl Fn(&[u8]) + 'static) -> 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); @@ -46,7 +48,7 @@ impl Titan { action_send.connect_activate({ // @TODO let form = form.clone(); - move |_, _| on_sent(&[]) // @TODO input data + move |_, _| callback(todo!()) // @TODO input data }); // Return activated struct