From 14c31734fd15407a176ccdf5199842f101435295 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 17 Nov 2024 16:28:45 +0200 Subject: [PATCH] begin identity dialog implementation --- src/app/browser/window/tab/item.rs | 23 ++++- src/app/browser/window/tab/item/action.rs | 14 +-- .../browser/window/tab/item/action/auth.rs | 43 ---------- .../browser/window/tab/item/action/ident.rs | 33 +++++++ src/app/browser/window/tab/item/identity.rs | 19 ++++ .../window/tab/item/identity/gemini.rs | 45 ++++++++++ .../window/tab/item/identity/gemini/widget.rs | 86 +++++++++++++++++++ .../tab/item/identity/gemini/widget/form.rs | 51 +++++++++++ .../item/identity/gemini/widget/form/list.rs | 40 +++++++++ .../item/identity/gemini/widget/form/name.rs | 29 +++++++ .../window/tab/item/identity/unsupported.rs | 27 ++++++ .../tab/item/identity/unsupported/widget.rs | 51 +++++++++++ .../browser/window/tab/item/page/content.rs | 2 +- .../window/tab/item/page/content/status.rs | 7 +- .../tab/item/page/content/status/identity.rs | 7 +- .../item/page/navigation/identity/widget.rs | 6 +- src/profile/identity.rs | 2 +- src/profile/identity/gemini/database.rs | 26 ++++-- 18 files changed, 439 insertions(+), 72 deletions(-) delete mode 100644 src/app/browser/window/tab/item/action/auth.rs create mode 100644 src/app/browser/window/tab/item/action/ident.rs create mode 100644 src/app/browser/window/tab/item/identity.rs create mode 100644 src/app/browser/window/tab/item/identity/gemini.rs create mode 100644 src/app/browser/window/tab/item/identity/gemini/widget.rs create mode 100644 src/app/browser/window/tab/item/identity/gemini/widget/form.rs create mode 100644 src/app/browser/window/tab/item/identity/gemini/widget/form/list.rs create mode 100644 src/app/browser/window/tab/item/identity/gemini/widget/form/name.rs create mode 100644 src/app/browser/window/tab/item/identity/unsupported.rs create mode 100644 src/app/browser/window/tab/item/identity/unsupported/widget.rs diff --git a/src/app/browser/window/tab/item.rs b/src/app/browser/window/tab/item.rs index fdd49efb..19f2aa6c 100644 --- a/src/app/browser/window/tab/item.rs +++ b/src/app/browser/window/tab/item.rs @@ -1,5 +1,6 @@ mod action; mod database; +mod identity; mod page; mod widget; @@ -15,7 +16,7 @@ use crate::Profile; use adw::TabView; use gtk::{ glib::{uuid_string_random, GString}, - prelude::EditableExt, + prelude::{Cast, EditableExt}, }; use sqlite::Transaction; use std::rc::Rc; @@ -49,7 +50,7 @@ impl Item { let page = Rc::new(Page::new( id.clone(), - profile, + profile.clone(), (actions.0, actions.1, action.clone()), )); @@ -76,10 +77,24 @@ impl Item { } } - action.auth().connect_activate(|request| { - // @TODO + // Show identity selection for item + action.ident().connect_activate({ + let page = page.clone(); + let parent = tab_view.clone().upcast::(); + move || { + // Request should match valid URI for all drivers supported + if let Some(uri) = page.navigation().request().uri() { + // Rout by scheme + if uri.scheme().to_lowercase() == "gemini" { + return identity::new_gemini(profile.clone(), uri).present(Some(&parent)); + } + } + // Show dialog with unsupported request message + identity::new_unsupported().present(Some(&parent)); + } }); + // Load new request for item action.load().connect_activate({ let page = page.clone(); move |request, is_history| { diff --git a/src/app/browser/window/tab/item/action.rs b/src/app/browser/window/tab/item/action.rs index 7d418357..e8ce29b4 100644 --- a/src/app/browser/window/tab/item/action.rs +++ b/src/app/browser/window/tab/item/action.rs @@ -1,14 +1,14 @@ -mod auth; +mod ident; mod load; -use auth::Auth; +use ident::Ident; use load::Load; use std::rc::Rc; /// [SimpleActionGroup](https://docs.gtk.org/gio/class.SimpleActionGroup.html) wrapper for `Browser` actions pub struct Action { - auth: Rc, + ident: Rc, load: Rc, } @@ -18,16 +18,16 @@ impl Action { /// Create new `Self` pub fn new() -> Self { Self { - auth: Rc::new(Auth::new()), + ident: Rc::new(Ident::new()), load: Rc::new(Load::new()), } } // Getters - /// Get reference to `Auth` action - pub fn auth(&self) -> &Rc { - &self.auth + /// Get reference to `Ident` action + pub fn ident(&self) -> &Rc { + &self.ident } /// Get reference to `Load` action diff --git a/src/app/browser/window/tab/item/action/auth.rs b/src/app/browser/window/tab/item/action/auth.rs deleted file mode 100644 index ebca6b2c..00000000 --- a/src/app/browser/window/tab/item/action/auth.rs +++ /dev/null @@ -1,43 +0,0 @@ -use gtk::{ - gio::SimpleAction, - glib::uuid_string_random, - prelude::{ActionExt, StaticVariantType, ToVariant}, -}; - -/// [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html) wrapper for `Load` action of `Item` group -pub struct Auth { - gobject: SimpleAction, -} - -impl Auth { - // Constructors - - /// Create new `Self` - pub fn new() -> Self { - Self { - gobject: SimpleAction::new(&uuid_string_random(), Some(&String::static_variant_type())), - } - } - - // Actions - - /// Emit [activate](https://docs.gtk.org/gio/signal.SimpleAction.activate.html) signal - /// with formatted for this action [Variant](https://docs.gtk.org/glib/struct.Variant.html) value - pub fn activate(&self, request: &str) { - self.gobject.activate(Some(&request.to_variant())); - } - - // Events - - /// Define callback function for - /// [SimpleAction::activate](https://docs.gtk.org/gio/signal.SimpleAction.activate.html) signal - pub fn connect_activate(&self, callback: impl Fn(String) + 'static) { - self.gobject.connect_activate(move |_, this| { - callback( - this.expect("Expected variant value") - .get::() - .expect("Parameter type does not match `String` type"), - ) - }); - } -} diff --git a/src/app/browser/window/tab/item/action/ident.rs b/src/app/browser/window/tab/item/action/ident.rs new file mode 100644 index 00000000..5537efe5 --- /dev/null +++ b/src/app/browser/window/tab/item/action/ident.rs @@ -0,0 +1,33 @@ +use gtk::{gio::SimpleAction, glib::uuid_string_random, prelude::ActionExt}; + +/// [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html) wrapper for `Ident` action of `Item` group +pub struct Ident { + gobject: SimpleAction, +} + +impl Ident { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + Self { + gobject: SimpleAction::new(&uuid_string_random(), None), + } + } + + // Actions + + /// Emit [activate](https://docs.gtk.org/gio/signal.SimpleAction.activate.html) signal + /// with formatted for this action [Variant](https://docs.gtk.org/glib/struct.Variant.html) value + pub fn activate(&self) { + self.gobject.activate(None); + } + + // Events + + /// Define callback function for + /// [SimpleAction::activate](https://docs.gtk.org/gio/signal.SimpleAction.activate.html) signal + pub fn connect_activate(&self, callback: impl Fn() + 'static) { + self.gobject.connect_activate(move |_, _| callback()); + } +} diff --git a/src/app/browser/window/tab/item/identity.rs b/src/app/browser/window/tab/item/identity.rs new file mode 100644 index 00000000..8253d5d0 --- /dev/null +++ b/src/app/browser/window/tab/item/identity.rs @@ -0,0 +1,19 @@ +mod gemini; +mod unsupported; + +use gemini::Gemini; +use unsupported::Unsupported; + +use crate::profile::Profile; +use gtk::glib::Uri; +use std::rc::Rc; + +/// Create new identity widget for Gemini protocol match given URI +pub fn new_gemini(profile: Rc, auth_uri: Uri) -> Gemini { + Gemini::new(profile, auth_uri) +} + +/// Create new identity widget for unknown request +pub fn new_unsupported() -> Unsupported { + Unsupported::new() +} diff --git a/src/app/browser/window/tab/item/identity/gemini.rs b/src/app/browser/window/tab/item/identity/gemini.rs new file mode 100644 index 00000000..46e8f3ba --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini.rs @@ -0,0 +1,45 @@ +mod widget; +use widget::Widget; + +use crate::profile::Profile; +use gtk::{glib::Uri, prelude::IsA}; +use std::rc::Rc; + +pub struct Gemini { + profile: Rc, + widget: Rc, +} + +impl Gemini { + // Construct + + /// Create new `Self` for given Profile + pub fn new(profile: Rc, auth_uri: Uri) -> Self { + let widget = Rc::new(Widget::new()); + + // Init events + widget.connect_response({ + let profile = profile.clone(); + move |value| { + match value { + Some(id) => { + // Activate selected identity ID + } + None => { + // Create and select new identity + } + } // @TODO handle result + } + }); + + // Return activated `Self` + Self { profile, widget } + } + + // Actions + + /// Show dialog for parent [Widget](https://docs.gtk.org/gtk4/class.Widget.html) + pub fn present(&self, parent: Option<&impl IsA>) { + self.widget.present(parent); + } +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget.rs b/src/app/browser/window/tab/item/identity/gemini/widget.rs new file mode 100644 index 00000000..b3610cb1 --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget.rs @@ -0,0 +1,86 @@ +mod form; + +use form::Form; + +use adw::{ + prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, + AlertDialog, ResponseAppearance, +}; +use gtk::prelude::IsA; + +// Defaults +const HEADING: &str = "Ident"; +const BODY: &str = "Select identity certificate"; + +// Response variants +const RESPONSE_APPLY: (&str, &str) = ("apply", "Apply"); +const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel"); +// const RESPONSE_MANAGE: (&str, &str) = ("manage", "Manage"); + +// List options +const OPTION_CREATE: (Option, &str) = (None, "Create new.."); + +// Select options + +pub struct Widget { + gobject: AlertDialog, +} + +impl Widget { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + // Collect identity certificates + let mut options: Vec<(Option, String, bool)> = Vec::new(); + options.push((OPTION_CREATE.0, OPTION_CREATE.1.to_owned(), false)); + + // Init child container + let form = Form::new(options); + + // Init main `GObject` + let gobject = AlertDialog::builder() + .heading(HEADING) + .body(BODY) + .close_response(RESPONSE_CANCEL.0) + .default_response(RESPONSE_APPLY.0) + .extra_child(form.gobject()) + .build(); + + // Set response variants + gobject.add_responses(&[ + RESPONSE_CANCEL, + // RESPONSE_MANAGE, + RESPONSE_APPLY, + ]); + + // Deactivate not implemented feature @TODO + // gobject.set_response_enabled(RESPONSE_MANAGE.0, false); + + // Decorate default response preset + gobject.set_response_appearance(RESPONSE_APPLY.0, ResponseAppearance::Suggested); + gobject.set_response_appearance(RESPONSE_CANCEL.0, ResponseAppearance::Destructive); + + // Return new activated `Self` + Self { gobject } + } + + // Actions + + /// Wrapper for default [response](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/signal.AlertDialog.response.html) signal + /// * return `profile_identity_gemini_id` or new record request on `None` + pub fn connect_response(&self, callback: impl Fn(Option) + 'static) { + self.gobject.connect_response(None, move |_, response| { + if response == RESPONSE_APPLY.0 { + callback(None) + } else { + callback(None) + } // @TODO + }); + } + + /// Show dialog with new preset + pub fn present(&self, parent: Option<&impl IsA>) { + self.gobject.present(parent) + } +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form.rs new file mode 100644 index 00000000..e1654eba --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form.rs @@ -0,0 +1,51 @@ +mod list; +mod name; + +use list::List; +use name::Name; + +use gtk::{ + prelude::{BoxExt, WidgetExt}, + Box, Orientation, +}; + +pub struct Form { + gobject: Box, +} + +impl Form { + // Constructors + + /// Create new `Self` + pub fn new(list_options: Vec<(Option, String, bool)>) -> Self { + // Init components + let list = List::new(&list_options); + let name = Name::new(); + + // Init main container + let gobject = Box::builder().orientation(Orientation::Vertical).build(); + + gobject.append(list.gobject()); + gobject.append(name.gobject()); + + // Init events + list.gobject().connect_selected_notify(move |this| { + // Get selection ID from vector @TODO use GObject storage instead + // https://gtk-rs.org/gtk4-rs/stable/latest/book/list_widgets.html + match list_options.get(this.selected() as usize) { + // Hide name entry on existing identity selected + Some((id, _, _)) => name.gobject().set_visible(id.is_none()), + None => todo!(), + } + }); + + // Return activated `Self` + Self { gobject } + } + + // Getters + + pub fn gobject(&self) -> &Box { + &self.gobject + } +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list.rs new file mode 100644 index 00000000..78cc8f15 --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list.rs @@ -0,0 +1,40 @@ +use gtk::{DropDown, StringList}; + +pub struct List { + gobject: DropDown, +} + +impl List { + // Constructors + + /// Create new `Self` + pub fn new(list_options: &Vec<(Option, String, bool)>) -> Self { + // Init empty list model + let model = StringList::new(&[]); + + // Init `GObject` + let gobject = DropDown::builder().model(&model).build(); + + // Build selection list + let mut index = 0; + + for (_key, value, is_selected) in list_options { + model.append(&value); + + if *is_selected { + gobject.set_selected(index); + } + + index += 1; + } + + // Done + Self { gobject } + } + + // Getters + + pub fn gobject(&self) -> &DropDown { + &self.gobject + } +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/name.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/name.rs new file mode 100644 index 00000000..fea85193 --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/name.rs @@ -0,0 +1,29 @@ +use gtk::Entry; + +const PLACEHOLDER_TEXT: &str = "Identity name (optional)"; +const MARGIN: i32 = 8; + +pub struct Name { + gobject: Entry, +} + +impl Name { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + Self { + gobject: Entry::builder() + .max_length(36) // @TODO use profile const + .placeholder_text(PLACEHOLDER_TEXT) + .margin_top(MARGIN) + .build(), + } + } + + // Getters + + pub fn gobject(&self) -> &Entry { + &self.gobject + } +} diff --git a/src/app/browser/window/tab/item/identity/unsupported.rs b/src/app/browser/window/tab/item/identity/unsupported.rs new file mode 100644 index 00000000..0becc6a2 --- /dev/null +++ b/src/app/browser/window/tab/item/identity/unsupported.rs @@ -0,0 +1,27 @@ +mod widget; +use widget::Widget; + +use gtk::prelude::IsA; +use std::rc::Rc; + +pub struct Unsupported { + widget: Rc, +} + +impl Unsupported { + // Construct + + /// Create new `Self` + pub fn new() -> Self { + Self { + widget: Rc::new(Widget::new()), + } + } + + // Actions + + /// Show dialog for given parent + pub fn present(&self, parent: Option<&impl IsA>) { + self.widget.present(parent) + } +} diff --git a/src/app/browser/window/tab/item/identity/unsupported/widget.rs b/src/app/browser/window/tab/item/identity/unsupported/widget.rs new file mode 100644 index 00000000..961522f5 --- /dev/null +++ b/src/app/browser/window/tab/item/identity/unsupported/widget.rs @@ -0,0 +1,51 @@ +use adw::{ + prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, + AlertDialog, ResponseAppearance, +}; +use gtk::prelude::IsA; + +const HEADING: &str = "Oops"; +const BODY: &str = "Identity not supported for this request"; +const RESPONSE_QUIT: (&str, &str) = ("close", "Close"); + +pub struct Widget { + gobject: AlertDialog, +} + +impl Widget { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + // Init gobject + let gobject = AlertDialog::builder() + .heading(HEADING) + .body(BODY) + .close_response(RESPONSE_QUIT.0) + .default_response(RESPONSE_QUIT.0) + .build(); + + // Set response variants + gobject.add_responses(&[RESPONSE_QUIT]); + + // Decorate default response preset + gobject.set_response_appearance(RESPONSE_QUIT.0, ResponseAppearance::Destructive); + + // Init events + gobject.connect_response(None, move |dialog, response| { + if response == RESPONSE_QUIT.0 { + dialog.close(); + } + }); + + // Return new activated `Self` + Self { gobject } + } + + // Actions + + /// Show dialog for given parent + pub fn present(&self, parent: Option<&impl IsA>) { + self.gobject.present(parent) + } +} diff --git a/src/app/browser/window/tab/item/page/content.rs b/src/app/browser/window/tab/item/page/content.rs index c65a3d54..fab26f46 100644 --- a/src/app/browser/window/tab/item/page/content.rs +++ b/src/app/browser/window/tab/item/page/content.rs @@ -60,7 +60,7 @@ impl Content { /// * action removes previous children component from `Self` pub fn to_status_identity(&self) -> Status { self.clean(); - let status = Status::new_identity(); + let status = Status::new_identity(self.tab_action.clone()); self.gobject.append(status.gobject()); status } diff --git a/src/app/browser/window/tab/item/page/content/status.rs b/src/app/browser/window/tab/item/page/content/status.rs index 67aa4e00..f81487a1 100644 --- a/src/app/browser/window/tab/item/page/content/status.rs +++ b/src/app/browser/window/tab/item/page/content/status.rs @@ -2,8 +2,9 @@ mod failure; mod identity; mod loading; +use crate::app::browser::window::tab::item::Action; use adw::StatusPage; -use std::time::Duration; +use std::{rc::Rc, time::Duration}; pub struct Status { gobject: StatusPage, @@ -25,9 +26,9 @@ impl Status { /// /// Useful as placeholder for 60 status code /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - pub fn new_identity() -> Self { + pub fn new_identity(action: Rc) -> Self { Self { - gobject: identity::new_gobject(), + gobject: identity::new_gobject(action), } } diff --git a/src/app/browser/window/tab/item/page/content/status/identity.rs b/src/app/browser/window/tab/item/page/content/status/identity.rs index 2d06f3df..fb01923f 100644 --- a/src/app/browser/window/tab/item/page/content/status/identity.rs +++ b/src/app/browser/window/tab/item/page/content/status/identity.rs @@ -1,3 +1,6 @@ +use std::rc::Rc; + +use crate::app::browser::window::tab::item::Action; use adw::StatusPage; use gtk::{prelude::ButtonExt, Align, Button}; @@ -11,7 +14,7 @@ const DEFAULT_BUTTON_CLASS: &str = "suggested-action"; /// Create new default preset for `Identity` /// [StatusPage](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.StatusPage.html) -pub fn new_gobject() -> StatusPage { +pub fn new_gobject(action: Rc) -> StatusPage { // Init certificate selection let button = &Button::builder() .css_classes([DEFAULT_BUTTON_CLASS]) @@ -21,7 +24,7 @@ pub fn new_gobject() -> StatusPage { .build(); // Init events - button.connect_activate(|_| {}); // @TODO + button.connect_clicked(move |_| action.ident().activate()); // Init status page StatusPage::builder() diff --git a/src/app/browser/window/tab/item/page/navigation/identity/widget.rs b/src/app/browser/window/tab/item/page/navigation/identity/widget.rs index 424d1f25..7242dc58 100644 --- a/src/app/browser/window/tab/item/page/navigation/identity/widget.rs +++ b/src/app/browser/window/tab/item/page/navigation/identity/widget.rs @@ -16,11 +16,11 @@ impl Widget { let gobject = Button::builder() .icon_name("avatar-default-symbolic") .tooltip_text("Identity") - .sensitive(false) + //.sensitive(false) .build(); // Init events @TODO dialog window required - // gobject.connect_clicked(move |_| action.auth().activate()); + gobject.connect_clicked(move |_| action.ident().activate()); // Return activated `Self` Self { gobject } @@ -28,7 +28,7 @@ impl Widget { // Actions pub fn update(&self, is_sensitive: bool) { - self.gobject.set_sensitive(is_sensitive); + //self.gobject.set_sensitive(is_sensitive); } // Getters diff --git a/src/profile/identity.rs b/src/profile/identity.rs index 9b11e6f4..fc407ae5 100644 --- a/src/profile/identity.rs +++ b/src/profile/identity.rs @@ -13,7 +13,7 @@ use std::{rc::Rc, sync::RwLock}; /// Authorization wrapper for different protocols pub struct Identity { // database: Rc, - gemini: Rc, + pub gemini: Rc, } impl Identity { diff --git a/src/profile/identity/gemini/database.rs b/src/profile/identity/gemini/database.rs index f579e956..27fa7273 100644 --- a/src/profile/identity/gemini/database.rs +++ b/src/profile/identity/gemini/database.rs @@ -1,10 +1,13 @@ use sqlite::{Connection, Error, Transaction}; use std::{rc::Rc, sync::RwLock}; +pub const NAME_MAX_LEN: i32 = 36; + pub struct Table { pub id: i64, //pub profile_identity_id: i64, pub pem: String, + pub name: String, } /// Storage for Gemini auth certificates @@ -36,14 +39,19 @@ impl Database { pub fn init(tx: &Transaction) -> Result { tx.execute( - "CREATE TABLE IF NOT EXISTS `profile_identity_gemini` - ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - `profile_identity_id` INTEGER NOT NULL, - `pem` TEXT NOT NULL, + format!( + "CREATE TABLE IF NOT EXISTS `profile_identity_gemini` + ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `profile_identity_id` INTEGER NOT NULL, + `pem` TEXT NOT NULL, + `name` VARCHAR({}), - FOREIGN KEY (`profile_identity_id`) REFERENCES `profile_identity`(`id`) - )", + FOREIGN KEY (`profile_identity_id`) REFERENCES `profile_identity`(`id`) + )", + NAME_MAX_LEN + ) + .as_str(), [], ) } @@ -52,7 +60,8 @@ pub fn select(tx: &Transaction, profile_id: i64) -> Result, Error> { let mut stmt = tx.prepare( "SELECT `id`, `profile_identity_id`, - `pem` + `pem`, + `name` FROM `profile_identity_gemini` WHERE `profile_identity_id` = ?", )?; @@ -62,6 +71,7 @@ pub fn select(tx: &Transaction, profile_id: i64) -> Result, Error> { id: row.get(0)?, //profile_identity_id: row.get(1)?, pem: row.get(2)?, + name: row.get(3)?, }) })?;