mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-03-31 16:45:27 +00:00
implement titan header options
This commit is contained in:
parent
6267691af2
commit
f6fb73c241
11 changed files with 289 additions and 25 deletions
|
|
@ -110,7 +110,9 @@ GTK 4 / Libadwaita client written in Rust
|
||||||
* [ ] [Titan](https://transjovian.org/titan/page/The%20Titan%20Specification)
|
* [ ] [Titan](https://transjovian.org/titan/page/The%20Titan%20Specification)
|
||||||
* [ ] Binary data (file uploads)
|
* [ ] Binary data (file uploads)
|
||||||
* [x] Text input
|
* [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))
|
* [ ] [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)
|
* [ ] [NPS](https://nightfall.city/nps/info/specification.txt)
|
||||||
* [ ] System
|
* [ ] System
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ use ggemini::{
|
||||||
client::{Client, Request, Response},
|
client::{Client, Request, Response},
|
||||||
gio::{file_output_stream, memory_input_stream},
|
gio::{file_output_stream, memory_input_stream},
|
||||||
};
|
};
|
||||||
use gtk::glib::Bytes;
|
|
||||||
use gtk::glib::GString;
|
use gtk::glib::GString;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
gdk::Texture,
|
gdk::Texture,
|
||||||
|
|
@ -85,14 +84,13 @@ impl Gemini {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let page = self.page.clone();
|
let page = self.page.clone();
|
||||||
let redirects = self.redirects.clone();
|
let redirects = self.redirects.clone();
|
||||||
move |data, on_failure| {
|
move |header, bytes, on_failure| {
|
||||||
handle(
|
handle(
|
||||||
Request::Titan {
|
Request::Titan {
|
||||||
uri: uri.clone(),
|
uri: uri.clone(),
|
||||||
data: Bytes::from(data),
|
data: bytes,
|
||||||
// * some servers may reject the request without content type
|
mime: header.mime.map(|mime| mime.into()),
|
||||||
mime: Some("text/plain".to_string()),
|
token: header.token.map(|token| token.into()),
|
||||||
token: None, // @TODO
|
|
||||||
},
|
},
|
||||||
client.clone(),
|
client.clone(),
|
||||||
page.clone(),
|
page.clone(),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ mod titan;
|
||||||
use super::ItemAction;
|
use super::ItemAction;
|
||||||
use adw::Clamp;
|
use adw::Clamp;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
glib::Uri,
|
glib::{Bytes, Uri},
|
||||||
prelude::{IsA, WidgetExt},
|
prelude::{IsA, WidgetExt},
|
||||||
Widget,
|
Widget,
|
||||||
};
|
};
|
||||||
|
|
@ -74,7 +74,7 @@ impl Input {
|
||||||
self.update(Some(>k::Box::sensitive(action, base, title, max_length)));
|
self.update(Some(>k::Box::sensitive(action, base, title, max_length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_new_titan(&self, on_send: impl Fn(&[u8], Box<dyn Fn()>) + 'static) {
|
pub fn set_new_titan(&self, on_send: impl Fn(titan::Header, Bytes, Box<dyn Fn()>) + 'static) {
|
||||||
self.update(Some(>k::Notebook::titan(on_send)));
|
self.update(Some(>k::Notebook::titan(on_send)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,35 @@
|
||||||
mod file;
|
mod file;
|
||||||
|
mod header;
|
||||||
mod text;
|
mod text;
|
||||||
mod title;
|
mod title;
|
||||||
|
|
||||||
use file::File;
|
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 text::Text;
|
||||||
use title::Title;
|
use title::Title;
|
||||||
|
|
||||||
pub trait Titan {
|
pub trait Titan {
|
||||||
fn titan(callback: impl Fn(&[u8], Box<dyn Fn()>) + 'static) -> Self;
|
fn titan(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Titan for Notebook {
|
impl Titan for Notebook {
|
||||||
fn titan(callback: impl Fn(&[u8], Box<dyn Fn()>) + 'static) -> Self {
|
fn titan(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self {
|
||||||
|
use gtk::Box;
|
||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
|
||||||
let notebook = Notebook::builder()
|
let notebook = Notebook::builder()
|
||||||
.name(format!("s{}", uuid_string_random()))
|
.name(format!("s{}", uuid_string_random()))
|
||||||
.show_border(false)
|
.show_border(false)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
notebook.append_page(>k::Box::text(callback), Some(&Label::title("Text")));
|
let header = Rc::new(Cell::new(Header::new()));
|
||||||
notebook.append_page(>k::Box::file(), Some(&Label::title("File")));
|
|
||||||
|
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_css_patch(¬ebook);
|
||||||
notebook
|
notebook
|
||||||
|
|
@ -29,6 +39,8 @@ impl Titan for Notebook {
|
||||||
// Tools
|
// Tools
|
||||||
|
|
||||||
fn notebook_css_patch(notebook: &Notebook) {
|
fn notebook_css_patch(notebook: &Notebook) {
|
||||||
|
use gtk::prelude::WidgetExt;
|
||||||
|
|
||||||
let name = notebook.widget_name();
|
let name = notebook.widget_name();
|
||||||
let provider = gtk::CssProvider::new();
|
let provider = gtk::CssProvider::new();
|
||||||
|
|
||||||
|
|
|
||||||
75
src/app/browser/window/tab/item/page/input/titan/header.rs
Normal file
75
src/app/browser/window/tab/item/page/input/titan/header.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
mod form;
|
||||||
|
|
||||||
|
use gtk::{glib::GString, prelude::IsA, Widget};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Header {
|
||||||
|
pub mime: Option<GString>,
|
||||||
|
pub token: Option<GString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Widget>>, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<GString> {
|
||||||
|
value(&self.mime)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(&self) -> Option<GString> {
|
||||||
|
value(&self.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
|
||||||
|
fn value(label: &Entry) -> Option<GString> {
|
||||||
|
let text = label.text();
|
||||||
|
if !text.is_empty() {
|
||||||
|
Some(text)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,39 @@
|
||||||
mod control;
|
mod control;
|
||||||
mod form;
|
mod form;
|
||||||
|
|
||||||
|
use super::Header;
|
||||||
use control::Control;
|
use control::Control;
|
||||||
use control::Upload;
|
use control::Upload;
|
||||||
use form::Form;
|
use form::Form;
|
||||||
|
use gtk::glib::Bytes;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
prelude::{BoxExt, ButtonExt, TextBufferExt, TextViewExt},
|
prelude::{BoxExt, ButtonExt, TextBufferExt, TextViewExt},
|
||||||
Orientation, TextView,
|
Orientation, TextView,
|
||||||
};
|
};
|
||||||
|
use std::cell::Cell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
const MARGIN: i32 = 8;
|
const MARGIN: i32 = 8;
|
||||||
const SPACING: i32 = 8;
|
const SPACING: i32 = 8;
|
||||||
|
|
||||||
pub trait Text {
|
pub trait Text {
|
||||||
fn text(callback: impl Fn(&[u8], Box<dyn Fn()>) + 'static) -> Self;
|
fn text(
|
||||||
|
header: &Rc<Cell<Header>>,
|
||||||
|
callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static,
|
||||||
|
) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Text for gtk::Box {
|
impl Text for gtk::Box {
|
||||||
fn text(callback: impl Fn(&[u8], Box<dyn Fn()>) + 'static) -> Self {
|
fn text(
|
||||||
|
header: &Rc<Cell<Header>>,
|
||||||
|
callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static,
|
||||||
|
) -> Self {
|
||||||
// Init components
|
// Init components
|
||||||
let control = Rc::new(Control::build());
|
let control = Rc::new(Control::build(header));
|
||||||
let form = TextView::form();
|
let form = TextView::form();
|
||||||
|
|
||||||
|
//header.take().dialog();
|
||||||
|
|
||||||
// Init widget
|
// Init widget
|
||||||
let g_box = gtk::Box::builder()
|
let g_box = gtk::Box::builder()
|
||||||
.margin_bottom(MARGIN)
|
.margin_bottom(MARGIN)
|
||||||
|
|
@ -37,12 +48,28 @@ impl Text for gtk::Box {
|
||||||
g_box.append(&control.g_box);
|
g_box.append(&control.g_box);
|
||||||
|
|
||||||
// Connect events
|
// 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({
|
control.upload.connect_clicked({
|
||||||
let form = form.clone();
|
let form = form.clone();
|
||||||
|
let header = header.clone();
|
||||||
move |this| {
|
move |this| {
|
||||||
this.set_uploading();
|
this.set_uploading();
|
||||||
|
let header = header.take();
|
||||||
callback(
|
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({
|
Box::new({
|
||||||
let this = this.clone();
|
let this = this.clone();
|
||||||
move || this.set_resend() // on failure
|
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
|
g_box
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
mod counter;
|
mod counter;
|
||||||
|
mod options;
|
||||||
mod upload;
|
mod upload;
|
||||||
|
|
||||||
|
use super::Header;
|
||||||
use counter::Counter;
|
use counter::Counter;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
prelude::{BoxExt, WidgetExt},
|
prelude::{BoxExt, WidgetExt},
|
||||||
Align, Box, Button, Label, Orientation,
|
Align, Box, Button, Label, Orientation,
|
||||||
};
|
};
|
||||||
|
use options::Options;
|
||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
pub use upload::Upload;
|
pub use upload::Upload;
|
||||||
|
|
||||||
const SPACING: i32 = 8;
|
const SPACING: i32 = 8;
|
||||||
|
|
@ -20,9 +24,10 @@ impl Control {
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
/// Build new `Self`
|
/// Build new `Self`
|
||||||
pub fn build() -> Self {
|
pub fn build(header: &Rc<Cell<Header>>) -> Self {
|
||||||
// Init components
|
// Init components
|
||||||
let counter = Label::counter();
|
let counter = Label::counter();
|
||||||
|
let options = Button::options(header);
|
||||||
let upload = Button::upload();
|
let upload = Button::upload();
|
||||||
|
|
||||||
// Init main widget
|
// Init main widget
|
||||||
|
|
@ -33,6 +38,7 @@ impl Control {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
g_box.append(&counter);
|
g_box.append(&counter);
|
||||||
|
g_box.append(&options);
|
||||||
g_box.append(&upload);
|
g_box.append(&upload);
|
||||||
|
|
||||||
// Return activated struct
|
// Return activated struct
|
||||||
|
|
|
||||||
|
|
@ -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<Cell<Header>>) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options for Button {
|
||||||
|
fn options(header: &Rc<Cell<Header>>) -> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue