From 0ff182f15d836fa08230bcbd6fa03bd57dae14ee Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Jan 2025 10:17:12 +0200 Subject: [PATCH] reorganize page loading methods --- src/app/browser/window/tab.rs | 12 +- src/app/browser/window/tab/item.rs | 6 +- src/app/browser/window/tab/item/page.rs | 699 +++++++++--------- .../browser/window/tab/item/page/status.rs | 6 +- 4 files changed, 354 insertions(+), 369 deletions(-) diff --git a/src/app/browser/window/tab.rs b/src/app/browser/window/tab.rs index 0bbfb6e4..1537dc14 100644 --- a/src/app/browser/window/tab.rs +++ b/src/app/browser/window/tab.rs @@ -219,7 +219,7 @@ impl Tab { 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(true); + item::page::load(&item.page, true); } } @@ -227,7 +227,7 @@ impl Tab { pub fn source(&self, page_position: Option) { if let Some(item) = self.item(page_position) { item.page.navigation.request.to_source(); - item.page.load(true); + item::page::load(&item.page, true); } } @@ -250,26 +250,26 @@ impl Tab { pub fn page_home(&self, page_position: Option) { if let Some(item) = self.item(page_position) { - item.page.home(); + item::page::home(&item.page); } } pub fn page_history_back(&self, page_position: Option) { if let Some(item) = self.item(page_position) { - item.page.history_back(); + item::page::history_back(&item.page); } } pub fn page_history_forward(&self, page_position: Option) { if let Some(item) = self.item(page_position) { - item.page.history_forward(); + item::page::history_forward(&item.page); } } /// 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(true); + item::page::load(&item.page, true); } } diff --git a/src/app/browser/window/tab/item.rs b/src/app/browser/window/tab/item.rs index a6049604..0c4f0d8b 100644 --- a/src/app/browser/window/tab/item.rs +++ b/src/app/browser/window/tab/item.rs @@ -1,7 +1,7 @@ mod action; mod database; mod identity; -mod page; +pub mod page; mod widget; use action::Action; @@ -75,7 +75,7 @@ impl Item { page.navigation.request.widget.entry.set_text(&text); if is_load { - page.load(true); + page::load(&page, true); } } @@ -111,7 +111,7 @@ impl Item { if let Some(text) = request { page.navigation.request.widget.entry.set_text(&text); } - page.load(is_history); + page::load(&page, is_history); } }); diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 0a0dec8c..99a27aca 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -138,361 +138,6 @@ impl Page { self.search.show() } - /// 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(&self) { - if let Some(url) = self.navigation.home.url() { - // Update with history record - self.tab_action.load.activate(Some(&url), true); - } - } - - /// Navigate back in history - /// * this method does not create new history record in memory - pub fn history_back(&self) { - if let Some(request) = self.navigation.history.back(true) { - // Update navigation entry - self.navigation.request.widget.entry.set_text(&request); - - // Load page (without history record) - self.load(false); - } - } - - /// Navigate forward in history - /// * this method does not create new history record in memory - pub fn history_forward(&self) { - if let Some(request) = self.navigation.history.forward(true) { - // Update navigation entry - self.navigation.request.widget.entry.set_text(&request); - - // Load page (without history record) - self.load(false); - } - } - - /// Main loader for current `navigation` value - pub fn load(&self, is_history: bool) { - // Move focus out from navigation entry - self.browser_action - .escape - .activate_stateful_once(Some(self.id.as_str().into())); - - // Initially disable find action - self.window_action.find.simple_action.set_enabled(false); - - // Reset widgets - self.search.unset(); - self.input.unset(); - - // Update - self.status.replace(Status::Loading { time: now() }); - self.title.replace("Loading..".into()); - self.browser_action.update.activate(Some(&self.id)); - - if is_history { - snap_history(&self.profile, &self.navigation, None); // @TODO - } - - use client::response::{Certificate, Failure, Input, Redirect}; - use client::Response; - - self.client - .request(&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(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_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(this) => match this { - Failure::Status { 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)); - } - Failure::Mime { base, mime, message } => { - // Update widget - let status_page = content.to_status_mime(&mime, Some((&tab_action, &base))); - 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(this) => match this { - 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("Titan input".into()); // @TODO - - browser_action.update.activate(Some(&id)); - } - }, - Response::Redirect(this) => match this { - Redirect::Background(request) => todo!(), // @TODO - Redirect::Foreground(request) => {navigation - .request - .widget - .entry - .set_text(&request.as_uri().to_string())} // @TODO handle - } - Response::TextGemini { base, source, is_source_request } => { - let widget = if is_source_request { - content.to_text_source(&source) - } else { - content.to_text_gemini(&base, &source) - }; - - // Connect `TextView` widget, update `search` model - search.set(Some(widget.text_view)); - - // 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 components - window_action.find.simple_action.set_enabled(true); - browser_action.update.activate(Some(&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 - status.replace(Status::Success { time: now() }); - title.replace(status_page.title()); - - // Update window - browser_action.update.activate(Some(&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_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 - } - } - } - }); - } - /// Update `Self` witch children components pub fn update(&self) { // Update components @@ -652,3 +297,347 @@ fn snap_history(profile: &Profile, navigation: &Navigation, uri: Option<&Uri>) { navigation.history.add(request, true) } } + +/// 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(url) = page.navigation.home.url() { + // Update navigation entry + page.navigation.request.widget.entry.set_text(&url); + + // Load page (with history record) + load(page, true); + } +} + +/// Navigate back in history +/// * this method does not create new history record in memory +pub fn history_back(page: &Rc) { + if let Some(request) = page.navigation.history.back(true) { + // Update navigation entry + page.navigation.request.widget.entry.set_text(&request); + + // Load page (without history record) + load(page, false); + } +} + +/// Navigate forward in history +/// * this method does not create new history record in memory +pub fn history_forward(page: &Rc) { + if let Some(request) = page.navigation.history.forward(true) { + // Update navigation entry + page.navigation.request.widget.entry.set_text(&request); + + // Load page (without history record) + load(page, false); + } +} + +/// Page load function with recursive redirection support +pub fn load(page: &Rc, 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(); + + // Update + 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 + } + + use client::response::{Certificate, Failure, Input, Redirect}; + use client::Response; + + page.client + .request(&page.navigation.request.widget.entry.text(), { + 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) => { + println!("{}",request.as_uri());load(&page, false) + }, // @TODO + Redirect::Foreground(request) => {page.navigation + .request + .widget + .entry + .set_text(&request.as_uri().to_string())} // @TODO handle + } + 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/status.rs b/src/app/browser/window/tab/item/page/status.rs index 3d675fde..d0212a62 100644 --- a/src/app/browser/window/tab/item/page/status.rs +++ b/src/app/browser/window/tab/item/page/status.rs @@ -9,7 +9,6 @@ pub enum Status { Input { time: DateTime }, Loading { time: DateTime }, New { time: DateTime }, - Redirect { time: DateTime }, SessionRestore { time: DateTime }, SessionRestored { time: DateTime }, Success { time: DateTime }, @@ -40,10 +39,7 @@ impl Status { Gemini::Complete { .. } => Some(0.9), }, }, - Self::Failure { .. } - | Self::Success { .. } - | Self::Redirect { .. } - | Self::Input { .. } => Some(1.0), + Self::Failure { .. } | Self::Success { .. } | Self::Input { .. } => Some(1.0), Self::New { .. } | Self::SessionRestored { .. } => None, } }