reorganize page loading methods

This commit is contained in:
yggverse 2025-01-19 10:17:12 +02:00
parent 8d3c4319c6
commit 0ff182f15d
4 changed files with 354 additions and 369 deletions

View File

@ -219,7 +219,7 @@ impl Tab {
pub fn save_as(&self, page_position: Option<i32>) {
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<i32>) {
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<i32>) {
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<i32>) {
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<i32>) {
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<i32>) {
if let Some(item) = self.item(page_position) {
item.page.load(true);
item::page::load(&item.page, true);
}
}

View File

@ -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);
}
});

View File

@ -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<Page>) {
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<Page>) {
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<Page>) {
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<Page>, 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
}
}
}
});
}

View File

@ -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,
}
}