From f6fb73c241f6e3021e75f72594cb2cfc8cd7336e Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 6 Feb 2025 17:24:54 +0200 Subject: [PATCH] implement titan header options --- README.md | 4 +- .../window/tab/item/client/driver/gemini.rs | 10 +-- src/app/browser/window/tab/item/page/input.rs | 4 +- .../window/tab/item/page/input/titan.rs | 22 ++++-- .../tab/item/page/input/titan/header.rs | 75 +++++++++++++++++++ .../tab/item/page/input/titan/header/form.rs | 56 ++++++++++++++ .../item/page/input/titan/header/form/mime.rs | 44 +++++++++++ .../page/input/titan/header/form/token.rs | 14 ++++ .../window/tab/item/page/input/titan/text.rs | 41 +++++++--- .../tab/item/page/input/titan/text/control.rs | 8 +- .../page/input/titan/text/control/options.rs | 36 +++++++++ 11 files changed, 289 insertions(+), 25 deletions(-) create mode 100644 src/app/browser/window/tab/item/page/input/titan/header.rs create mode 100644 src/app/browser/window/tab/item/page/input/titan/header/form.rs create mode 100644 src/app/browser/window/tab/item/page/input/titan/header/form/mime.rs create mode 100644 src/app/browser/window/tab/item/page/input/titan/header/form/token.rs create mode 100644 src/app/browser/window/tab/item/page/input/titan/text/control/options.rs diff --git a/README.md b/README.md index 3e12e7fb..d8ad1b1d 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,9 @@ GTK 4 / Libadwaita client written in Rust * [ ] [Titan](https://transjovian.org/titan/page/The%20Titan%20Specification) * [ ] Binary data (file uploads) * [x] Text input - * [ ] Custom headers + * [x] Header options + * [x] MIME + * [x] Token * [ ] [NEX](https://nightfall.city/nex/info/specification.txt) - useful for networks with build-in encryption (e.g. [Yggdrasil](https://yggdrasil-network.github.io)) * [ ] [NPS](https://nightfall.city/nps/info/specification.txt) * [ ] System diff --git a/src/app/browser/window/tab/item/client/driver/gemini.rs b/src/app/browser/window/tab/item/client/driver/gemini.rs index 8137a097..fa45044c 100644 --- a/src/app/browser/window/tab/item/client/driver/gemini.rs +++ b/src/app/browser/window/tab/item/client/driver/gemini.rs @@ -7,7 +7,6 @@ use ggemini::{ client::{Client, Request, Response}, gio::{file_output_stream, memory_input_stream}, }; -use gtk::glib::Bytes; use gtk::glib::GString; use gtk::{ gdk::Texture, @@ -85,14 +84,13 @@ impl Gemini { let client = self.client.clone(); let page = self.page.clone(); let redirects = self.redirects.clone(); - move |data, on_failure| { + move |header, bytes, on_failure| { handle( Request::Titan { uri: uri.clone(), - data: Bytes::from(data), - // * some servers may reject the request without content type - mime: Some("text/plain".to_string()), - token: None, // @TODO + data: bytes, + mime: header.mime.map(|mime| mime.into()), + token: header.token.map(|token| token.into()), }, client.clone(), page.clone(), diff --git a/src/app/browser/window/tab/item/page/input.rs b/src/app/browser/window/tab/item/page/input.rs index 1afe40cd..59690bb4 100644 --- a/src/app/browser/window/tab/item/page/input.rs +++ b/src/app/browser/window/tab/item/page/input.rs @@ -5,7 +5,7 @@ mod titan; use super::ItemAction; use adw::Clamp; use gtk::{ - glib::Uri, + glib::{Bytes, Uri}, prelude::{IsA, WidgetExt}, Widget, }; @@ -74,7 +74,7 @@ impl Input { self.update(Some(>k::Box::sensitive(action, base, title, max_length))); } - pub fn set_new_titan(&self, on_send: impl Fn(&[u8], Box) + 'static) { + pub fn set_new_titan(&self, on_send: impl Fn(titan::Header, Bytes, Box) + 'static) { self.update(Some(>k::Notebook::titan(on_send))); } } 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 ef799679..5225416d 100644 --- a/src/app/browser/window/tab/item/page/input/titan.rs +++ b/src/app/browser/window/tab/item/page/input/titan.rs @@ -1,25 +1,35 @@ mod file; +mod header; mod text; mod title; use file::File; -use gtk::{glib::uuid_string_random, prelude::WidgetExt, Label, Notebook}; +use gtk::{ + glib::{uuid_string_random, Bytes}, + Label, Notebook, +}; +pub use header::Header; use text::Text; use title::Title; pub trait Titan { - fn titan(callback: impl Fn(&[u8], Box) + 'static) -> Self; + fn titan(callback: impl Fn(Header, Bytes, Box) + 'static) -> Self; } impl Titan for Notebook { - fn titan(callback: impl Fn(&[u8], Box) + 'static) -> Self { + fn titan(callback: impl Fn(Header, Bytes, Box) + 'static) -> Self { + use gtk::Box; + use std::{cell::Cell, rc::Rc}; + let notebook = Notebook::builder() .name(format!("s{}", uuid_string_random())) .show_border(false) .build(); - notebook.append_page(>k::Box::text(callback), Some(&Label::title("Text"))); - notebook.append_page(>k::Box::file(), Some(&Label::title("File"))); + let header = Rc::new(Cell::new(Header::new())); + + notebook.append_page(&Box::text(&header, callback), Some(&Label::title("Text"))); + notebook.append_page(&Box::file(), Some(&Label::title("File"))); notebook_css_patch(¬ebook); notebook @@ -29,6 +39,8 @@ impl Titan for Notebook { // Tools fn notebook_css_patch(notebook: &Notebook) { + use gtk::prelude::WidgetExt; + let name = notebook.widget_name(); let provider = gtk::CssProvider::new(); diff --git a/src/app/browser/window/tab/item/page/input/titan/header.rs b/src/app/browser/window/tab/item/page/input/titan/header.rs new file mode 100644 index 00000000..256ec73a --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/header.rs @@ -0,0 +1,75 @@ +mod form; + +use gtk::{glib::GString, prelude::IsA, Widget}; + +#[derive(Default)] +pub struct Header { + pub mime: Option, + pub token: Option, +} + +impl Header { + pub fn new() -> Self { + Self { + mime: None, + token: None, + } + } + + /// Show header options dialog for the referrer `widget` + /// * takes ownership of `Self`, return new updated copy in `callback` function + pub fn dialog(self, widget: Option<&impl IsA>, callback: impl Fn(Self) + 'static) { + use adw::{ + prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, + AlertDialog, ResponseAppearance, + }; + use form::Form; + use std::rc::Rc; + + // Response variants + const RESPONSE_APPLY: (&str, &str) = ("apply", "Apply"); + const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel"); + + // Init form components + let form = Rc::new(Form::build( + &self.mime.unwrap_or_default(), + &self.token.unwrap_or_default(), + )); + + // Init main widget + let alert_dialog = AlertDialog::builder() + .heading("Header") + .body("Custom header options") + .close_response(RESPONSE_CANCEL.0) + .default_response(RESPONSE_APPLY.0) + .extra_child(&form.g_box) + .build(); + + alert_dialog.add_responses(&[RESPONSE_CANCEL, RESPONSE_APPLY]); + + // Decorate default response preset + alert_dialog.set_response_appearance(RESPONSE_APPLY.0, ResponseAppearance::Suggested); + /* contrast issue with Ubuntu orange accents + alert_dialog.set_response_appearance(RESPONSE_CANCEL.0, ResponseAppearance::Destructive); */ + + // Init events + + alert_dialog.connect_response(None, { + let form = form.clone(); + move |this, response| { + this.set_response_enabled(response, false); // prevent double-click + if response == RESPONSE_APPLY.0 { + callback(Self { + mime: form.mime(), + token: form.token(), + }) + } else { + // @TODO restore + } + } + }); + + // Show + alert_dialog.present(widget); + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/header/form.rs b/src/app/browser/window/tab/item/page/input/titan/header/form.rs new file mode 100644 index 00000000..ba2aef66 --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/header/form.rs @@ -0,0 +1,56 @@ +mod mime; +mod token; + +use mime::Mime; +use token::Token; + +use gtk::{ + glib::GString, + prelude::{BoxExt, EditableExt}, + Box, Entry, Orientation, +}; + +pub struct Form { + pub g_box: Box, + mime: Entry, + token: Entry, +} + +impl Form { + // Constructors + + pub fn build(mime_value: &str, token_value: &str) -> Self { + // Init components + let mime = Entry::mime(mime_value); + let token = Entry::token(token_value); + + // Init `Self` + let g_box = Box::builder().orientation(Orientation::Vertical).build(); + + g_box.append(&mime); + g_box.append(&token); + + Self { g_box, mime, token } + } + + // Getters + + pub fn mime(&self) -> Option { + value(&self.mime) + } + + pub fn token(&self) -> Option { + value(&self.token) + } +} + +// Tools + +fn value(label: &Entry) -> Option { + let text = label.text(); + if !text.is_empty() { + Some(text) + } else { + None + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/header/form/mime.rs b/src/app/browser/window/tab/item/page/input/titan/header/form/mime.rs new file mode 100644 index 00000000..cdc0a81f --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/header/form/mime.rs @@ -0,0 +1,44 @@ +pub trait Mime { + fn mime(text: &str) -> Self; + fn validate(&self); +} + +impl Mime for gtk::Entry { + fn mime(text: &str) -> Self { + use gtk::prelude::EditableExt; + + let mime = gtk::Entry::builder() + .placeholder_text("Content type (MIME)") + .margin_bottom(8) + .text(text) + .build(); + + mime.connect_changed(|this| { + this.validate(); + }); + + mime + } + + fn validate(&self) { + use gtk::prelude::{EditableExt, WidgetExt}; + + const CLASS: (&str, &str) = ("error", "success"); + + self.remove_css_class(CLASS.0); + self.remove_css_class(CLASS.1); + + if !self.text().is_empty() { + if gtk::glib::Regex::match_simple( + r"^\w+/\w+$", + self.text(), + gtk::glib::RegexCompileFlags::DEFAULT, + gtk::glib::RegexMatchFlags::DEFAULT, + ) { + self.add_css_class(CLASS.1) + } else { + self.add_css_class(CLASS.0) + } + } + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/header/form/token.rs b/src/app/browser/window/tab/item/page/input/titan/header/form/token.rs new file mode 100644 index 00000000..9ffe34ce --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/header/form/token.rs @@ -0,0 +1,14 @@ +use gtk::Entry; + +pub trait Token { + fn token(text: &str) -> Self; +} + +impl Token for Entry { + fn token(text: &str) -> Self { + Entry::builder() + .placeholder_text("Token") + .text(text) + .build() + } +} diff --git a/src/app/browser/window/tab/item/page/input/titan/text.rs b/src/app/browser/window/tab/item/page/input/titan/text.rs index 2ca41029..9d4612a1 100644 --- a/src/app/browser/window/tab/item/page/input/titan/text.rs +++ b/src/app/browser/window/tab/item/page/input/titan/text.rs @@ -1,28 +1,39 @@ mod control; mod form; +use super::Header; use control::Control; use control::Upload; use form::Form; +use gtk::glib::Bytes; use gtk::{ prelude::{BoxExt, ButtonExt, TextBufferExt, TextViewExt}, Orientation, TextView, }; +use std::cell::Cell; use std::rc::Rc; const MARGIN: i32 = 8; const SPACING: i32 = 8; pub trait Text { - fn text(callback: impl Fn(&[u8], Box) + 'static) -> Self; + fn text( + header: &Rc>, + callback: impl Fn(Header, Bytes, Box) + 'static, + ) -> Self; } impl Text for gtk::Box { - fn text(callback: impl Fn(&[u8], Box) + 'static) -> Self { + fn text( + header: &Rc>, + callback: impl Fn(Header, Bytes, Box) + 'static, + ) -> Self { // Init components - let control = Rc::new(Control::build()); + let control = Rc::new(Control::build(header)); let form = TextView::form(); + //header.take().dialog(); + // Init widget let g_box = gtk::Box::builder() .margin_bottom(MARGIN) @@ -37,12 +48,28 @@ impl Text for gtk::Box { g_box.append(&control.g_box); // Connect events + + form.buffer().connect_changed({ + let control = control.clone(); + let form = form.clone(); + move |this| control.update(this.char_count(), form.text().len()) + }); + control.upload.connect_clicked({ let form = form.clone(); + let header = header.clone(); move |this| { this.set_uploading(); + let header = header.take(); callback( - form.text().as_bytes(), + Header { + mime: match header.mime { + Some(mime) => Some(mime), + None => Some("text/plain".into()), // server may reject the request without content type + }, + token: header.token, + }, + Bytes::from(form.text().as_bytes()), Box::new({ let this = this.clone(); move || this.set_resend() // on failure @@ -51,12 +78,6 @@ impl Text for gtk::Box { } }); - form.buffer().connect_changed({ - let control = control.clone(); - let form = form.clone(); - move |this| control.update(this.char_count(), form.text().len()) - }); - g_box } } diff --git a/src/app/browser/window/tab/item/page/input/titan/text/control.rs b/src/app/browser/window/tab/item/page/input/titan/text/control.rs index 91f2347c..1889a764 100644 --- a/src/app/browser/window/tab/item/page/input/titan/text/control.rs +++ b/src/app/browser/window/tab/item/page/input/titan/text/control.rs @@ -1,11 +1,15 @@ mod counter; +mod options; mod upload; +use super::Header; use counter::Counter; use gtk::{ prelude::{BoxExt, WidgetExt}, Align, Box, Button, Label, Orientation, }; +use options::Options; +use std::{cell::Cell, rc::Rc}; pub use upload::Upload; const SPACING: i32 = 8; @@ -20,9 +24,10 @@ impl Control { // Constructors /// Build new `Self` - pub fn build() -> Self { + pub fn build(header: &Rc>) -> Self { // Init components let counter = Label::counter(); + let options = Button::options(header); let upload = Button::upload(); // Init main widget @@ -33,6 +38,7 @@ impl Control { .build(); g_box.append(&counter); + g_box.append(&options); g_box.append(&upload); // Return activated struct diff --git a/src/app/browser/window/tab/item/page/input/titan/text/control/options.rs b/src/app/browser/window/tab/item/page/input/titan/text/control/options.rs new file mode 100644 index 00000000..4dc709d0 --- /dev/null +++ b/src/app/browser/window/tab/item/page/input/titan/text/control/options.rs @@ -0,0 +1,36 @@ +use super::Header; +use gtk::{ + prelude::{ButtonExt, WidgetExt}, + Button, +}; +use std::{cell::Cell, rc::Rc}; + +pub trait Options { + fn options(header: &Rc>) -> Self; +} + +impl Options for Button { + fn options(header: &Rc>) -> Self { + let button = Button::builder() + .icon_name("emblem-system-symbolic") + .tooltip_text("Options") + .build(); + + button.connect_clicked({ + let header = header.clone(); + move |this| { + this.set_sensitive(false); // lock + header.take().dialog(Some(this), { + let this = this.clone(); + let header = header.clone(); + move |options| { + header.replace(options); + this.set_sensitive(true); // unlock + } + }) + } + }); + + button + } +}