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)
|
||||
* [ ] 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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<dyn Fn()>) + 'static) -> Self;
|
||||
fn titan(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self;
|
||||
}
|
||||
|
||||
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()
|
||||
.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();
|
||||
|
||||
|
|
|
|||
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 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<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 {
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Cell<Header>>) -> 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
|
||||
|
|
|
|||
|
|
@ -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