mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-04-01 09:05:27 +00:00
move identity feature to request area
This commit is contained in:
parent
78957ff85b
commit
2dc6154d54
32 changed files with 59 additions and 84 deletions
|
|
@ -5,7 +5,7 @@ mod text;
|
|||
use image::Image;
|
||||
use text::Text;
|
||||
|
||||
use super::{ItemAction, WindowAction};
|
||||
use super::{ItemAction, TabAction, WindowAction};
|
||||
use adw::StatusPage;
|
||||
use gtk::{
|
||||
gdk::Paintable,
|
||||
|
|
@ -19,6 +19,7 @@ use std::{rc::Rc, time::Duration};
|
|||
pub struct Content {
|
||||
window_action: Rc<WindowAction>,
|
||||
item_action: Rc<ItemAction>,
|
||||
tab_action: Rc<TabAction>,
|
||||
pub g_box: Box,
|
||||
}
|
||||
|
||||
|
|
@ -26,11 +27,18 @@ impl Content {
|
|||
// Construct
|
||||
|
||||
/// Create new container for different components
|
||||
pub fn build((window_action, item_action): (&Rc<WindowAction>, &Rc<ItemAction>)) -> Self {
|
||||
pub fn build(
|
||||
(window_action, tab_action, item_action): (
|
||||
&Rc<WindowAction>,
|
||||
&Rc<TabAction>,
|
||||
&Rc<ItemAction>,
|
||||
),
|
||||
) -> Self {
|
||||
Self {
|
||||
g_box: Box::builder().orientation(Orientation::Vertical).build(),
|
||||
window_action: window_action.clone(),
|
||||
item_action: item_action.clone(),
|
||||
tab_action: tab_action.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +98,7 @@ impl Content {
|
|||
/// * action removes previous children component from `Self`
|
||||
pub fn to_status_identity(&self) -> StatusPage {
|
||||
self.clean();
|
||||
let status = status::identity::build(self.item_action.clone());
|
||||
let status = status::identity::build((&self.tab_action, &self.item_action));
|
||||
self.g_box.append(&status);
|
||||
status
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ pub mod identity;
|
|||
pub mod loading;
|
||||
pub mod mime;
|
||||
|
||||
use super::ItemAction;
|
||||
use super::{ItemAction, TabAction};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::app::browser::window::tab::item::Action;
|
||||
use super::{ItemAction, TabAction};
|
||||
use adw::StatusPage;
|
||||
use gtk::{prelude::ButtonExt, Align, Button};
|
||||
use gtk::{prelude::ActionExt, Align, Button};
|
||||
use std::rc::Rc;
|
||||
|
||||
// Defaults
|
||||
|
|
@ -13,18 +13,16 @@ 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 build(action: Rc<Action>) -> StatusPage {
|
||||
pub fn build((tab_action, item_action): (&Rc<TabAction>, &Rc<ItemAction>)) -> StatusPage {
|
||||
// Init certificate selection
|
||||
let button = &Button::builder()
|
||||
.action_name(format!("{}.{}", tab_action.id, item_action.identity.name()))
|
||||
.css_classes([DEFAULT_BUTTON_CLASS])
|
||||
.label(DEFAULT_BUTTON_LABEL)
|
||||
.tooltip_text(DEFAULT_BUTTON_TOOLTIP_TEXT)
|
||||
.halign(Align::Center)
|
||||
.build();
|
||||
|
||||
// Init events
|
||||
button.connect_clicked(move |_| action.ident.activate());
|
||||
|
||||
// Init status page
|
||||
StatusPage::builder()
|
||||
.description(DEFAULT_DESCRIPTION)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
mod database;
|
||||
mod identity;
|
||||
mod primary_icon;
|
||||
|
||||
use adw::prelude::AdwDialogExt;
|
||||
use primary_icon::PrimaryIcon;
|
||||
|
||||
use super::{ItemAction, Profile};
|
||||
|
|
@ -71,8 +73,25 @@ impl Request for Entry {
|
|||
// Connect events
|
||||
entry.connect_icon_release({
|
||||
let item_action = item_action.clone();
|
||||
let profile = profile.clone();
|
||||
move |this, position| match position {
|
||||
EntryIconPosition::Primary => item_action.ident.activate(), // @TODO PrimaryIcon impl
|
||||
EntryIconPosition::Primary => {
|
||||
if let Some(request) = this.uri() {
|
||||
if ["gemini", "titan"].contains(&request.scheme().as_str()) {
|
||||
return identity::default(&profile, &request, {
|
||||
let item_action = item_action.clone();
|
||||
let profile = profile.clone();
|
||||
let this = this.clone();
|
||||
move || {
|
||||
this.update(&profile);
|
||||
item_action.load.activate(Some(&this.text()), false);
|
||||
} // on apply
|
||||
})
|
||||
.present(Some(this));
|
||||
}
|
||||
}
|
||||
identity::unsupported().present(Some(this));
|
||||
}
|
||||
EntryIconPosition::Secondary => item_action.load.activate(Some(&this.text()), true),
|
||||
_ => todo!(), // unexpected
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
mod default;
|
||||
mod unsupported;
|
||||
|
||||
use adw::AlertDialog;
|
||||
use default::Default;
|
||||
use unsupported::Unsupported;
|
||||
|
||||
use super::Profile;
|
||||
use gtk::glib::Uri;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Create new identity widget for Gemini protocol match given URI
|
||||
pub fn default(profile: &Rc<Profile>, request: &Uri, on_apply: impl Fn() + 'static) -> Default {
|
||||
Default::build(profile, request, on_apply)
|
||||
}
|
||||
|
||||
/// Create new identity widget for unknown request
|
||||
pub fn unsupported() -> AlertDialog {
|
||||
AlertDialog::unsupported()
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
mod widget;
|
||||
use widget::{form::list::item::value::Value, Widget};
|
||||
|
||||
use super::Profile;
|
||||
use gtk::{glib::Uri, prelude::IsA};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct Default {
|
||||
// profile: Rc<Profile>,
|
||||
widget: Rc<Widget>,
|
||||
}
|
||||
|
||||
impl Default {
|
||||
// Construct
|
||||
|
||||
/// Create new `Self` for given `Profile`
|
||||
pub fn build(profile: &Rc<Profile>, request: &Uri, on_apply: impl Fn() + 'static) -> Self {
|
||||
// Init widget
|
||||
let widget = Rc::new(Widget::build(profile, request));
|
||||
|
||||
// Init events
|
||||
widget.on_apply({
|
||||
let profile = profile.clone();
|
||||
let request = request.clone();
|
||||
let widget = widget.clone();
|
||||
move |response| {
|
||||
// Get option match user choice
|
||||
let option = match response {
|
||||
Value::ProfileIdentityId(value) => Some(value),
|
||||
Value::GuestSession => None,
|
||||
Value::GeneratePem => Some(
|
||||
profile
|
||||
.identity
|
||||
.make(None, &widget.form.name.value().unwrap())
|
||||
.unwrap(), // @TODO handle
|
||||
),
|
||||
Value::ImportPem => Some(
|
||||
profile
|
||||
.identity
|
||||
.add(&widget.form.file.pem.take().unwrap())
|
||||
.unwrap(), // @TODO handle
|
||||
),
|
||||
};
|
||||
|
||||
// Apply auth
|
||||
match option {
|
||||
// Activate identity for `scope`
|
||||
Some(profile_identity_id) => {
|
||||
if profile
|
||||
.identity
|
||||
.auth
|
||||
.apply(profile_identity_id, &request.to_string())
|
||||
.is_err()
|
||||
{
|
||||
panic!() // unexpected @TODO
|
||||
}
|
||||
}
|
||||
// Remove all identity auths for `scope`
|
||||
None => {
|
||||
if profile.identity.auth.remove(&request.to_string()).is_err() {
|
||||
panic!() // unexpected @TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run callback function
|
||||
on_apply()
|
||||
}
|
||||
});
|
||||
|
||||
// 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<gtk::Widget>>) {
|
||||
self.widget.present(parent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
mod action;
|
||||
pub mod form;
|
||||
|
||||
use action::Action as WidgetAction;
|
||||
use form::{list::item::value::Value, Form};
|
||||
|
||||
use crate::Profile;
|
||||
use adw::{
|
||||
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
|
||||
AlertDialog, ResponseAppearance,
|
||||
};
|
||||
use gtk::{glib::Uri, prelude::IsA};
|
||||
use std::rc::Rc;
|
||||
|
||||
// Defaults
|
||||
const HEADING: &str = "Identity";
|
||||
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");
|
||||
|
||||
// Select options
|
||||
|
||||
pub struct Widget {
|
||||
// pub action: Rc<Action>,
|
||||
pub form: Rc<Form>,
|
||||
pub alert_dialog: AlertDialog,
|
||||
}
|
||||
|
||||
impl Widget {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(profile: &Rc<Profile>, request: &Uri) -> Self {
|
||||
// Init actions
|
||||
let action = Rc::new(WidgetAction::new());
|
||||
|
||||
// Init child container
|
||||
let form = Rc::new(Form::build(&action, profile, request));
|
||||
|
||||
// Init main widget
|
||||
let alert_dialog = AlertDialog::builder()
|
||||
.heading(HEADING)
|
||||
.body(BODY)
|
||||
.close_response(RESPONSE_CANCEL.0)
|
||||
.default_response(RESPONSE_APPLY.0)
|
||||
.extra_child(&form.g_box)
|
||||
.build();
|
||||
|
||||
// Set response variants
|
||||
alert_dialog.add_responses(&[
|
||||
RESPONSE_CANCEL,
|
||||
// RESPONSE_MANAGE,
|
||||
RESPONSE_APPLY,
|
||||
]);
|
||||
|
||||
// Deactivate not implemented feature @TODO
|
||||
// alert_dialog.set_response_enabled(RESPONSE_MANAGE.0, false);
|
||||
|
||||
// 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
|
||||
action.update.connect_activate({
|
||||
let form = form.clone();
|
||||
let alert_dialog = alert_dialog.clone();
|
||||
move || {
|
||||
// Update child components
|
||||
form.update();
|
||||
|
||||
// Deactivate apply button if the form values could not be processed
|
||||
alert_dialog.set_response_enabled(RESPONSE_APPLY.0, form.is_applicable());
|
||||
}
|
||||
});
|
||||
|
||||
// Make initial update
|
||||
action.update.activate();
|
||||
|
||||
// Return new activated `Self`
|
||||
Self {
|
||||
// action,
|
||||
form,
|
||||
alert_dialog,
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
/// Callback wrapper to apply
|
||||
/// [response](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/signal.AlertDialog.response.html)
|
||||
pub fn on_apply(&self, callback: impl Fn(Value) + 'static) {
|
||||
self.alert_dialog.connect_response(Some(RESPONSE_APPLY.0), {
|
||||
let form = self.form.clone();
|
||||
move |this, response| {
|
||||
// Prevent double-click action
|
||||
this.set_response_enabled(response, false);
|
||||
|
||||
// Result
|
||||
callback(form.list.selected().value_enum())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Show dialog with new preset
|
||||
pub fn present(&self, parent: Option<&impl IsA<gtk::Widget>>) {
|
||||
self.alert_dialog.present(parent)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
mod update;
|
||||
use update::Update;
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
/// [SimpleActionGroup](https://docs.gtk.org/gio/class.SimpleActionGroup.html) wrapper
|
||||
pub struct Action {
|
||||
pub update: Rc<Update>,
|
||||
}
|
||||
|
||||
impl Default for Action {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Action {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
update: Rc::new(Update::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
use gtk::{gio::SimpleAction, glib::uuid_string_random, prelude::ActionExt};
|
||||
|
||||
/// [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html) wrapper for `Update` action
|
||||
pub struct Update {
|
||||
pub gobject: SimpleAction,
|
||||
}
|
||||
|
||||
impl Default for Update {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Update {
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
mod drop;
|
||||
mod exit;
|
||||
mod file;
|
||||
pub mod list;
|
||||
mod name;
|
||||
mod save;
|
||||
|
||||
use drop::Drop;
|
||||
use exit::Exit;
|
||||
use file::File;
|
||||
use list::{item::value::Value, List};
|
||||
use name::Name;
|
||||
use save::Save;
|
||||
|
||||
use super::WidgetAction;
|
||||
use crate::Profile;
|
||||
use gtk::{glib::Uri, prelude::BoxExt, Box, Orientation};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct Form {
|
||||
// pub action_widget: Rc<Action>,
|
||||
pub drop: Rc<Drop>,
|
||||
pub exit: Rc<Exit>,
|
||||
pub file: Rc<File>,
|
||||
pub list: Rc<List>,
|
||||
pub name: Rc<Name>,
|
||||
pub save: Rc<Save>,
|
||||
pub g_box: Box,
|
||||
request: Uri,
|
||||
profile: Rc<Profile>,
|
||||
}
|
||||
|
||||
impl Form {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(widget_action: &Rc<WidgetAction>, profile: &Rc<Profile>, request: &Uri) -> Self {
|
||||
// Init components
|
||||
let list = Rc::new(List::build(widget_action, profile, request));
|
||||
let file = Rc::new(File::build(widget_action));
|
||||
let name = Rc::new(Name::build(widget_action));
|
||||
let save = Rc::new(Save::build(profile, &list));
|
||||
let drop = Rc::new(Drop::build(profile, &list));
|
||||
let exit = Rc::new(Exit::build(widget_action, profile, &list, request));
|
||||
|
||||
// Init main container
|
||||
let g_box = Box::builder().orientation(Orientation::Vertical).build();
|
||||
|
||||
g_box.append(&list.dropdown);
|
||||
g_box.append(&name.entry);
|
||||
g_box.append(&file.button);
|
||||
g_box.append(&exit.button);
|
||||
g_box.append(&drop.button);
|
||||
g_box.append(&save.button);
|
||||
|
||||
// Return activated `Self`
|
||||
Self {
|
||||
// action_widget,
|
||||
drop,
|
||||
exit,
|
||||
file,
|
||||
list,
|
||||
name,
|
||||
save,
|
||||
g_box,
|
||||
request: request.clone(),
|
||||
profile: profile.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
/// Get `Apply` button sensitivity to disable when it does not change anything
|
||||
pub fn is_applicable(&self) -> bool {
|
||||
match self.list.selected().value_enum() {
|
||||
Value::GeneratePem => self.name.is_valid(),
|
||||
Value::ImportPem => self.file.is_valid(),
|
||||
_ => !self.list.selected().is_active(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&self) {
|
||||
// Get shared selected item value
|
||||
let value = self.list.selected().value_enum();
|
||||
|
||||
// Begin children components update
|
||||
self.name.update(matches!(value, Value::GeneratePem));
|
||||
self.file.update(matches!(value, Value::ImportPem));
|
||||
|
||||
match value {
|
||||
Value::ProfileIdentityId(profile_identity_id) => {
|
||||
self.drop.update(true);
|
||||
self.exit.update(
|
||||
true,
|
||||
self.profile
|
||||
.identity
|
||||
.auth
|
||||
.is_matches(&self.request.to_string(), profile_identity_id),
|
||||
);
|
||||
self.save.update(true);
|
||||
}
|
||||
_ => {
|
||||
self.drop.update(false);
|
||||
self.exit.update(false, false);
|
||||
self.save.update(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
use super::list::{item::Value, List};
|
||||
use crate::profile::Profile;
|
||||
use adw::{
|
||||
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
|
||||
AlertDialog, ResponseAppearance,
|
||||
};
|
||||
use gtk::{
|
||||
prelude::{ButtonExt, WidgetExt},
|
||||
Button,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
// Defaults
|
||||
|
||||
const LABEL: &str = "Delete";
|
||||
const TOOLTIP_TEXT: &str = "Drop selected identity from profile";
|
||||
const MARGIN: i32 = 8;
|
||||
|
||||
const HEADING: &str = "Delete";
|
||||
const BODY: &str = "Delete selected identity from profile?";
|
||||
const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel");
|
||||
const RESPONSE_CONFIRM: (&str, &str) = ("confirm", "Confirm");
|
||||
|
||||
pub struct Drop {
|
||||
pub button: Button,
|
||||
}
|
||||
|
||||
impl Drop {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(profile: &Rc<Profile>, list: &Rc<List>) -> Self {
|
||||
// Init main widget
|
||||
let button = Button::builder()
|
||||
.label(LABEL)
|
||||
.margin_top(MARGIN)
|
||||
.tooltip_text(TOOLTIP_TEXT)
|
||||
.visible(false)
|
||||
.build();
|
||||
|
||||
// Init events
|
||||
button.connect_clicked({
|
||||
let button = button.clone();
|
||||
let list = list.clone();
|
||||
let profile = profile.clone();
|
||||
move |_| {
|
||||
match list.selected().value_enum() {
|
||||
Value::ProfileIdentityId(profile_identity_id) => {
|
||||
// Init sub-widget
|
||||
let alert_dialog = AlertDialog::builder()
|
||||
.heading(HEADING)
|
||||
.body(BODY)
|
||||
.close_response(RESPONSE_CANCEL.0)
|
||||
.default_response(RESPONSE_CANCEL.0)
|
||||
.build();
|
||||
|
||||
// Set response variants
|
||||
alert_dialog.add_responses(&[RESPONSE_CANCEL, RESPONSE_CONFIRM]);
|
||||
|
||||
// Decorate default response preset
|
||||
alert_dialog.set_response_appearance(
|
||||
RESPONSE_CONFIRM.0,
|
||||
ResponseAppearance::Suggested,
|
||||
);
|
||||
|
||||
/* contrast issue with Ubuntu orange accents
|
||||
alert_dialog.set_response_appearance(
|
||||
RESPONSE_CANCEL.0,
|
||||
ResponseAppearance::Destructive,
|
||||
); */
|
||||
|
||||
// Connect confirmation event
|
||||
alert_dialog.connect_response(Some(RESPONSE_CONFIRM.0), {
|
||||
let button = button.clone();
|
||||
let list = list.clone();
|
||||
let profile = profile.clone();
|
||||
move |_, _| match profile.identity.delete(profile_identity_id) {
|
||||
Ok(_) => {
|
||||
if list.remove(profile_identity_id).is_some() {
|
||||
button.set_css_classes(&["success"]);
|
||||
button.set_label("Identity successfully deleted")
|
||||
} else {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label("List item not found")
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label(&e.to_string())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show dialog
|
||||
alert_dialog.present(Some(&button))
|
||||
}
|
||||
_ => todo!(), // unexpected
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return activated `Self`
|
||||
Self { button }
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
pub fn update(&self, is_visible: bool) {
|
||||
self.button.set_visible(is_visible)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
use super::{
|
||||
list::{item::Value, List},
|
||||
WidgetAction,
|
||||
};
|
||||
use crate::Profile;
|
||||
use adw::{
|
||||
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
|
||||
AlertDialog, ResponseAppearance,
|
||||
};
|
||||
use gtk::{
|
||||
glib::Uri,
|
||||
prelude::{ButtonExt, WidgetExt},
|
||||
Button,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
// Defaults
|
||||
|
||||
const LABEL: &str = "Disconnect";
|
||||
const TOOLTIP_TEXT: &str = "Stop use selected identity everywhere";
|
||||
const MARGIN: i32 = 8;
|
||||
|
||||
const HEADING: &str = "Disconnect";
|
||||
const BODY: &str = "Stop use selected identity for all scopes?";
|
||||
const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel");
|
||||
const RESPONSE_CONFIRM: (&str, &str) = ("confirm", "Confirm");
|
||||
|
||||
pub struct Exit {
|
||||
pub button: Button,
|
||||
}
|
||||
|
||||
impl Exit {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(
|
||||
widget_action: &Rc<WidgetAction>,
|
||||
profile: &Rc<Profile>,
|
||||
list: &Rc<List>,
|
||||
request: &Uri,
|
||||
) -> Self {
|
||||
// Init main widget
|
||||
let button = Button::builder()
|
||||
.label(LABEL)
|
||||
.margin_top(MARGIN)
|
||||
.tooltip_text(TOOLTIP_TEXT)
|
||||
.visible(false)
|
||||
.build();
|
||||
|
||||
// Init events
|
||||
button.connect_clicked({
|
||||
let button = button.clone();
|
||||
let list = list.clone();
|
||||
let profile = profile.clone();
|
||||
let request = request.clone();
|
||||
let widget_action = widget_action.clone();
|
||||
move |_| {
|
||||
// Get selected identity from holder
|
||||
match list.selected().value_enum() {
|
||||
Value::ProfileIdentityId(profile_identity_id) => {
|
||||
// Init sub-widget
|
||||
let alert_dialog = AlertDialog::builder()
|
||||
.heading(HEADING)
|
||||
.body(BODY)
|
||||
.close_response(RESPONSE_CANCEL.0)
|
||||
.default_response(RESPONSE_CANCEL.0)
|
||||
.build();
|
||||
|
||||
// Set response variants
|
||||
alert_dialog.add_responses(&[RESPONSE_CANCEL, RESPONSE_CONFIRM]);
|
||||
|
||||
// Decorate default response preset
|
||||
alert_dialog.set_response_appearance(
|
||||
RESPONSE_CONFIRM.0,
|
||||
ResponseAppearance::Suggested,
|
||||
);
|
||||
|
||||
/* contrast issue with Ubuntu orange accents
|
||||
alert_dialog.set_response_appearance(
|
||||
RESPONSE_CANCEL.0,
|
||||
ResponseAppearance::Destructive,
|
||||
); */
|
||||
|
||||
// Connect confirmation event
|
||||
alert_dialog.connect_response(Some(RESPONSE_CONFIRM.0), {
|
||||
let button = button.clone();
|
||||
let list = list.clone();
|
||||
let profile = profile.clone();
|
||||
let request = request.clone();
|
||||
let widget_action = widget_action.clone();
|
||||
move |_, _| {
|
||||
match profile.identity.auth.remove_ref(profile_identity_id) {
|
||||
Ok(_) => {
|
||||
match list.selected().update(&profile, &request.to_string())
|
||||
{
|
||||
Ok(_) => {
|
||||
button.set_css_classes(&["success"]);
|
||||
button
|
||||
.set_label("Identity successfully disconnected")
|
||||
}
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label(&e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label(&e.to_string())
|
||||
}
|
||||
}
|
||||
widget_action.update.activate();
|
||||
}
|
||||
});
|
||||
|
||||
// Show dialog
|
||||
alert_dialog.present(Some(&button))
|
||||
}
|
||||
_ => todo!(), // unexpected
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return activated `Self`
|
||||
Self { button }
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
pub fn update(&self, is_visible: bool, is_sensitive: bool) {
|
||||
self.button.set_visible(is_visible);
|
||||
self.button.set_sensitive(is_sensitive);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
use super::WidgetAction;
|
||||
use gtk::{
|
||||
gio::{Cancellable, ListStore, TlsCertificate},
|
||||
glib::{gformat, GString},
|
||||
prelude::{ButtonExt, FileExt, TlsCertificateExt, WidgetExt},
|
||||
Button, FileDialog, FileFilter, Window,
|
||||
};
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
const LABEL: &str = "Choose file..";
|
||||
const TOOLTIP_TEXT: &str = "Import existing identity from file";
|
||||
const MARGIN: i32 = 8;
|
||||
|
||||
pub struct File {
|
||||
pub pem: Rc<RefCell<Option<GString>>>,
|
||||
pub button: Button,
|
||||
}
|
||||
|
||||
impl File {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(widget_action: &Rc<WidgetAction>) -> Self {
|
||||
// Init PEM
|
||||
let pem = Rc::new(RefCell::new(None));
|
||||
|
||||
// Init main gobject
|
||||
let button = Button::builder()
|
||||
.label(LABEL)
|
||||
.margin_top(MARGIN)
|
||||
.tooltip_text(TOOLTIP_TEXT)
|
||||
.visible(false)
|
||||
.build();
|
||||
|
||||
// Init events
|
||||
button.connect_clicked({
|
||||
let widget_action = widget_action.clone();
|
||||
let button = button.clone();
|
||||
let pem = pem.clone();
|
||||
move |_| {
|
||||
// Lock open button (prevent double click)
|
||||
button.set_sensitive(false);
|
||||
|
||||
// Init file filters related with PEM extension
|
||||
let filters = ListStore::new::<FileFilter>();
|
||||
|
||||
let filter_all = FileFilter::new();
|
||||
filter_all.add_pattern("*.*");
|
||||
filter_all.set_name(Some("All"));
|
||||
filters.append(&filter_all);
|
||||
|
||||
let filter_pem = FileFilter::new();
|
||||
filter_pem.add_mime_type("application/x-x509-ca-cert");
|
||||
filter_pem.set_name(Some("Certificate (*.pem)"));
|
||||
filters.append(&filter_pem);
|
||||
|
||||
// Init file dialog
|
||||
FileDialog::builder()
|
||||
.filters(&filters)
|
||||
.default_filter(&filter_pem)
|
||||
.build()
|
||||
.open(Window::NONE, Cancellable::NONE, {
|
||||
let widget_action = widget_action.clone();
|
||||
let button = button.clone();
|
||||
let pem = pem.clone();
|
||||
move |result| {
|
||||
match result {
|
||||
Ok(file) => match file.path() {
|
||||
Some(path) => {
|
||||
let filename = path.to_str().unwrap();
|
||||
match TlsCertificate::from_file(filename) {
|
||||
Ok(certificate) => {
|
||||
pem.replace(to_pem(certificate));
|
||||
button.set_css_classes(&["success"]);
|
||||
button.set_label(filename)
|
||||
}
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label(e.message())
|
||||
}
|
||||
}
|
||||
}
|
||||
None => todo!(),
|
||||
},
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["warning"]);
|
||||
button.set_label(e.message())
|
||||
}
|
||||
}
|
||||
button.set_sensitive(true); // unlock
|
||||
widget_action.update.activate()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Return activated `Self`
|
||||
Self { pem, button }
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
/// Change visibility status
|
||||
pub fn update(&self, is_visible: bool) {
|
||||
self.button.set_visible(is_visible);
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.pem.borrow().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
// Private helpers
|
||||
|
||||
/// Convert [TlsCertificate](https://docs.gtk.org/gio/class.TlsCertificate.html) to GString
|
||||
fn to_pem(certificate: TlsCertificate) -> Option<GString> {
|
||||
let certificate_pem = certificate.certificate_pem()?;
|
||||
let private_key_pem = certificate.private_key_pem()?;
|
||||
Some(gformat!("{certificate_pem}{private_key_pem}"))
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
pub mod item;
|
||||
use std::rc::Rc;
|
||||
|
||||
use item::Item;
|
||||
|
||||
use super::WidgetAction;
|
||||
use crate::profile::Profile;
|
||||
use gtk::{
|
||||
gdk::Cursor,
|
||||
gio::{
|
||||
prelude::{Cast, CastNone},
|
||||
ListStore,
|
||||
},
|
||||
glib::Uri,
|
||||
prelude::{BoxExt, ListItemExt, ObjectExt, WidgetExt},
|
||||
Align, Box, DropDown, Image, Label, ListItem, Orientation, SignalListItemFactory,
|
||||
};
|
||||
|
||||
pub struct List {
|
||||
pub dropdown: DropDown,
|
||||
list_store: ListStore,
|
||||
}
|
||||
|
||||
impl List {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(widget_action: &Rc<WidgetAction>, profile: &Rc<Profile>, request: &Uri) -> Self {
|
||||
// Init dropdown items
|
||||
let guest_session = Item::new_guest_session();
|
||||
let generate_pem = Item::new_generate_pem();
|
||||
let import_pem = Item::new_import_pem();
|
||||
|
||||
// Init model
|
||||
let list_store = ListStore::new::<Item>();
|
||||
|
||||
list_store.append(&guest_session);
|
||||
list_store.append(&generate_pem);
|
||||
list_store.append(&import_pem);
|
||||
|
||||
match profile.identity.database.records() {
|
||||
Ok(identities) => {
|
||||
let mut is_guest_session = true;
|
||||
for identity in identities {
|
||||
match Item::new_profile_identity_id(profile, identity.id, &request.to_string())
|
||||
{
|
||||
Ok(item) => {
|
||||
if item.is_active() {
|
||||
is_guest_session = false;
|
||||
}
|
||||
list_store.append(&item)
|
||||
}
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
}
|
||||
if is_guest_session {
|
||||
guest_session.set_is_active(true);
|
||||
}
|
||||
}
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
|
||||
// Setup item factory
|
||||
// * wanted only to append items after `DropDown` init
|
||||
let factory = SignalListItemFactory::new();
|
||||
|
||||
factory.connect_setup(|_, this| {
|
||||
// Init widget for dropdown item
|
||||
let child = Box::builder().orientation(Orientation::Vertical).build();
|
||||
|
||||
// Title
|
||||
child.append(&Label::builder().halign(Align::Start).build());
|
||||
|
||||
// Subtitle
|
||||
let subtitle = Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.css_classes(["caption", "dim-label"])
|
||||
.halign(Align::Start)
|
||||
.build();
|
||||
|
||||
subtitle.append(
|
||||
&Image::builder()
|
||||
.css_classes(["success"]) // @TODO toggle on certificate issues
|
||||
.cursor(&Cursor::from_name("help", None).unwrap())
|
||||
.icon_name("application-certificate-symbolic")
|
||||
.margin_end(4)
|
||||
.pixel_size(11)
|
||||
.build(),
|
||||
);
|
||||
|
||||
subtitle.append(&Label::new(None));
|
||||
|
||||
// Subtitle
|
||||
child.append(&subtitle);
|
||||
|
||||
// Done
|
||||
this.downcast_ref::<ListItem>()
|
||||
.unwrap()
|
||||
.set_child(Some(&child));
|
||||
});
|
||||
|
||||
factory.connect_bind(|_, this| {
|
||||
// Downcast requirements
|
||||
let list_item = this.downcast_ref::<ListItem>().unwrap();
|
||||
let item = list_item.item().and_downcast::<Item>().unwrap();
|
||||
let child = list_item.child().and_downcast::<Box>().unwrap();
|
||||
|
||||
// Bind `title`
|
||||
match child.first_child().and_downcast::<Label>() {
|
||||
Some(label) => {
|
||||
label.set_label(&item.title());
|
||||
label.set_css_classes(if item.is_active() { &["accent"] } else { &[] });
|
||||
item.bind_property("title", &label, "label").build(); // sync label
|
||||
item.bind_property("is-active", &label, "css-classes")
|
||||
.transform_to(|_, is_active| {
|
||||
if is_active {
|
||||
Some(vec!["accent".to_string()])
|
||||
} else {
|
||||
Some(vec![])
|
||||
}
|
||||
})
|
||||
.build(); // sync class by status
|
||||
}
|
||||
None => todo!(),
|
||||
};
|
||||
|
||||
// Bind `subtitle`
|
||||
let subtitle = child.last_child().and_downcast::<Box>().unwrap();
|
||||
|
||||
match subtitle.last_child().and_downcast::<Label>() {
|
||||
Some(label) => {
|
||||
label.set_label(&item.subtitle());
|
||||
item.bind_property("subtitle", &label, "label").build(); // sync
|
||||
}
|
||||
None => todo!(),
|
||||
};
|
||||
|
||||
// Bind `tooltip`
|
||||
match subtitle.first_child().and_downcast::<Image>() {
|
||||
Some(tooltip) => {
|
||||
tooltip.set_visible(!item.tooltip().is_empty());
|
||||
tooltip.set_tooltip_markup(Some(&item.tooltip()));
|
||||
item.bind_property("tooltip", &tooltip, "tooltip-markup")
|
||||
.build(); // sync
|
||||
}
|
||||
None => todo!(),
|
||||
};
|
||||
});
|
||||
|
||||
// Init main widget
|
||||
let dropdown = DropDown::builder()
|
||||
.model(&list_store)
|
||||
.selected(
|
||||
list_store
|
||||
.find_with_equal_func(|item| {
|
||||
item.dynamic_cast_ref::<Item>().unwrap().is_active()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.factory(&factory)
|
||||
.build();
|
||||
|
||||
// Connect events
|
||||
dropdown.connect_selected_notify({
|
||||
let widget_action = widget_action.clone();
|
||||
move |_| widget_action.update.activate()
|
||||
});
|
||||
|
||||
// Return activated `Self`
|
||||
Self {
|
||||
dropdown,
|
||||
list_store,
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
/// Find list item by `profile_identity_id`
|
||||
/// * return `position` found
|
||||
pub fn find(&self, profile_identity_id: i64) -> Option<u32> {
|
||||
self.list_store.find_with_equal_func(|this| {
|
||||
profile_identity_id == this.downcast_ref::<Item>().unwrap().value()
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove list item by `profile_identity_id`
|
||||
/// * return `position` of removed list item
|
||||
pub fn remove(&self, profile_identity_id: i64) -> Option<u32> {
|
||||
match self.find(profile_identity_id) {
|
||||
Some(position) => {
|
||||
self.list_store.remove(position);
|
||||
Some(position)
|
||||
}
|
||||
None => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get selected `Item` GObject
|
||||
pub fn selected(&self) -> Item {
|
||||
self.dropdown
|
||||
.selected_item()
|
||||
.and_downcast::<Item>()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
mod error;
|
||||
mod imp;
|
||||
mod is_active;
|
||||
mod subtitle;
|
||||
mod title;
|
||||
mod tooltip;
|
||||
pub mod value;
|
||||
|
||||
pub use error::Error;
|
||||
pub use value::Value;
|
||||
|
||||
use crate::profile::Profile;
|
||||
use gtk::{
|
||||
gio::TlsCertificate,
|
||||
glib::{self, Object},
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Item(ObjectSubclass<imp::Item>);
|
||||
}
|
||||
|
||||
// C-type property `value` conversion for `Item`
|
||||
// * values > 0 reserved for `profile_identity_id`
|
||||
const G_VALUE_GENERATE_PEM: i64 = 0;
|
||||
const G_VALUE_IMPORT_PEM: i64 = -1;
|
||||
const G_VALUE_GUEST_SESSION: i64 = -2;
|
||||
|
||||
impl Item {
|
||||
// Constructors
|
||||
|
||||
pub fn new_guest_session() -> Self {
|
||||
Object::builder()
|
||||
.property("value", G_VALUE_GUEST_SESSION)
|
||||
.property("title", "Guest session")
|
||||
.property("subtitle", "No identity for this request")
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn new_generate_pem() -> Self {
|
||||
Object::builder()
|
||||
.property("value", G_VALUE_GENERATE_PEM)
|
||||
.property("title", "Create new")
|
||||
.property("subtitle", "Generate long-term certificate")
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn new_import_pem() -> Self {
|
||||
Object::builder()
|
||||
.property("value", G_VALUE_IMPORT_PEM)
|
||||
.property("title", "Import identity")
|
||||
.property("subtitle", "Use existing certificate")
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn new_profile_identity_id(
|
||||
profile: &Rc<Profile>,
|
||||
profile_identity_id: i64,
|
||||
auth_url: &str,
|
||||
) -> Result<Self, Error> {
|
||||
// Get PEM by ID
|
||||
match profile.identity.memory.get(profile_identity_id) {
|
||||
// Extract certificate details from PEM string
|
||||
Ok(ref pem) => match TlsCertificate::from_pem(pem) {
|
||||
// Collect certificate scopes for item
|
||||
Ok(ref certificate) => {
|
||||
let scope = &profile.identity.auth.scope(profile_identity_id);
|
||||
|
||||
Ok(Object::builder()
|
||||
.property("value", profile_identity_id)
|
||||
.property("title", title::new_for_profile_identity_id(certificate))
|
||||
.property(
|
||||
"subtitle",
|
||||
subtitle::new_for_profile_identity_id(certificate, scope),
|
||||
)
|
||||
.property(
|
||||
"tooltip",
|
||||
tooltip::new_for_profile_identity_id(certificate, scope),
|
||||
)
|
||||
.property(
|
||||
"is-active",
|
||||
is_active::new_for_profile_identity_id(
|
||||
profile,
|
||||
profile_identity_id,
|
||||
auth_url,
|
||||
),
|
||||
)
|
||||
.build())
|
||||
}
|
||||
Err(e) => Err(Error::TlsCertificate(e)),
|
||||
},
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
/// Update properties for `Self` for given `Profile` and `auth_url`
|
||||
pub fn update(&self, profile: &Rc<Profile>, auth_url: &str) -> Result<(), Error> {
|
||||
// Update item depending on value type
|
||||
match self.value_enum() {
|
||||
Value::ProfileIdentityId(profile_identity_id) => {
|
||||
// Get PEM by ID
|
||||
match profile.identity.memory.get(profile_identity_id) {
|
||||
// Extract certificate details from PEM string
|
||||
Ok(ref pem) => match TlsCertificate::from_pem(pem) {
|
||||
Ok(ref certificate) => {
|
||||
// Get current scope
|
||||
let scope = &profile.identity.auth.scope(profile_identity_id);
|
||||
|
||||
// Update properties
|
||||
self.set_title(title::new_for_profile_identity_id(certificate));
|
||||
|
||||
self.set_subtitle(subtitle::new_for_profile_identity_id(
|
||||
certificate,
|
||||
scope,
|
||||
));
|
||||
|
||||
self.set_tooltip(tooltip::new_for_profile_identity_id(
|
||||
certificate,
|
||||
scope,
|
||||
));
|
||||
|
||||
self.set_is_active(is_active::new_for_profile_identity_id(
|
||||
profile,
|
||||
profile_identity_id,
|
||||
auth_url,
|
||||
));
|
||||
|
||||
// @TODO emit update request
|
||||
}
|
||||
Err(e) => return Err(Error::TlsCertificate(e)),
|
||||
},
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// nothing to update yet..
|
||||
}
|
||||
}
|
||||
Ok(()) // @TODO
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get `Self` C-value as `Value`
|
||||
pub fn value_enum(&self) -> Value {
|
||||
match self.value() {
|
||||
G_VALUE_GENERATE_PEM => Value::GeneratePem,
|
||||
G_VALUE_GUEST_SESSION => Value::GuestSession,
|
||||
G_VALUE_IMPORT_PEM => Value::ImportPem,
|
||||
value => Value::ProfileIdentityId(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
TlsCertificate(gtk::glib::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::TlsCertificate(e) => {
|
||||
write!(f, "TLS certificate error `{e}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
//! Custom `GObject` implementation for dropdown
|
||||
//! [ListStore](https://docs.gtk.org/gio/class.ListStore.html) menu item
|
||||
|
||||
use gtk::{
|
||||
gio::subclass::prelude::{DerivedObjectProperties, ObjectImpl, ObjectImplExt, ObjectSubclass},
|
||||
glib::{self, Object, Properties},
|
||||
prelude::ObjectExt,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[derive(Properties, Default)]
|
||||
#[properties(wrapper_type = super::Item)]
|
||||
pub struct Item {
|
||||
#[property(get, set)]
|
||||
value: Cell<i64>,
|
||||
#[property(get, set)]
|
||||
title: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
subtitle: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
tooltip: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
is_active: Cell<bool>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Item {
|
||||
const NAME: &'static str = "Item"; // @TODO make globally unique
|
||||
type Type = super::Item;
|
||||
type ParentType = Object;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for Item {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
use crate::profile::Profile;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub fn new_for_profile_identity_id(
|
||||
profile: &Rc<Profile>,
|
||||
profile_identity_id: i64,
|
||||
auth_url: &str,
|
||||
) -> bool {
|
||||
profile
|
||||
.identity
|
||||
.auth
|
||||
.is_matches(auth_url, profile_identity_id) // @TODO direct call?
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt};
|
||||
|
||||
const DATE_FORMAT: &str = "%Y.%m.%d";
|
||||
|
||||
pub fn new_for_profile_identity_id(certificate: &TlsCertificate, scope: &[String]) -> String {
|
||||
format!(
|
||||
"{} - {} | scope: {}",
|
||||
certificate
|
||||
.not_valid_before()
|
||||
.unwrap() // @TODO
|
||||
.format(DATE_FORMAT)
|
||||
.unwrap(),
|
||||
certificate
|
||||
.not_valid_after()
|
||||
.unwrap() // @TODO
|
||||
.format(DATE_FORMAT)
|
||||
.unwrap(),
|
||||
scope.len(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
use gtk::{gio::TlsCertificate, glib::gformat, prelude::TlsCertificateExt};
|
||||
|
||||
pub fn new_for_profile_identity_id(certificate: &TlsCertificate) -> String {
|
||||
certificate
|
||||
.subject_name()
|
||||
.unwrap_or(gformat!("Unknown"))
|
||||
.replace("CN=", "")
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt};
|
||||
|
||||
pub fn new_for_profile_identity_id(certificate: &TlsCertificate, scope: &[String]) -> String {
|
||||
let mut tooltip = "<b>Certificate</b>\n".to_string();
|
||||
|
||||
if let Some(subject_name) = certificate.subject_name() {
|
||||
tooltip.push_str(&format!("\n<small><b>subject</b>\n{subject_name}</small>"));
|
||||
}
|
||||
|
||||
if let Some(issuer_name) = certificate.issuer_name() {
|
||||
tooltip.push_str(&format!("\n<small><b>issuer</b>\n{issuer_name}</small>"));
|
||||
}
|
||||
|
||||
if let Some(not_valid_before) = certificate.not_valid_before() {
|
||||
if let Ok(timestamp) = not_valid_before.format_iso8601() {
|
||||
tooltip.push_str(&format!("\n<small><b>valid after</b>\n{timestamp}</small>"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(not_valid_after) = certificate.not_valid_after() {
|
||||
if let Ok(timestamp) = not_valid_after.format_iso8601() {
|
||||
tooltip.push_str(&format!(
|
||||
"\n<small><b>valid before</b>\n{timestamp}</small>"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !scope.is_empty() {
|
||||
tooltip.push_str("\n\n<b>Scope</b>\n");
|
||||
|
||||
for path in scope {
|
||||
tooltip.push_str(&format!("\n<small>{}</small>", path));
|
||||
}
|
||||
}
|
||||
|
||||
tooltip
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Value {
|
||||
GeneratePem,
|
||||
GuestSession,
|
||||
ImportPem,
|
||||
ProfileIdentityId(i64),
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
use super::WidgetAction;
|
||||
use gtk::{
|
||||
glib::GString,
|
||||
prelude::{EditableExt, EntryExt, WidgetExt},
|
||||
Entry,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
const PLACEHOLDER_TEXT: &str = "Identity name (required)";
|
||||
const MARGIN: i32 = 8;
|
||||
const MIN_LENGTH: u16 = 1;
|
||||
const MAX_LENGTH: u16 = 36;
|
||||
|
||||
pub struct Name {
|
||||
pub entry: Entry,
|
||||
}
|
||||
|
||||
impl Name {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(widget_action: &Rc<WidgetAction>) -> Self {
|
||||
// Init main gobject
|
||||
let entry = Entry::builder()
|
||||
.margin_top(MARGIN)
|
||||
.max_length(MAX_LENGTH as i32)
|
||||
.placeholder_text(PLACEHOLDER_TEXT)
|
||||
.visible(false)
|
||||
.build();
|
||||
|
||||
// Init events
|
||||
entry.connect_changed({
|
||||
let widget_action = widget_action.clone();
|
||||
move |_| widget_action.update.activate()
|
||||
});
|
||||
|
||||
// Return activated `Self`
|
||||
Self { entry }
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
/// Change visibility status
|
||||
/// * grab focus on `is_visible` is `true`
|
||||
pub fn update(&self, is_visible: bool) {
|
||||
self.entry.set_visible(is_visible);
|
||||
if is_visible && self.entry.focus_child().is_none() {
|
||||
self.entry.grab_focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.entry.text_length() >= MIN_LENGTH && self.entry.text_length() <= MAX_LENGTH
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Option<GString> {
|
||||
let text = self.entry.text();
|
||||
if text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
mod certificate;
|
||||
use certificate::Certificate;
|
||||
|
||||
use super::list::{item::Value, List};
|
||||
use crate::profile::Profile;
|
||||
use gtk::{
|
||||
gio::{Cancellable, FileCreateFlags, ListStore},
|
||||
glib::Priority,
|
||||
prelude::{ButtonExt, FileExt, OutputStreamExtManual, WidgetExt},
|
||||
Button, FileDialog, FileFilter, Window,
|
||||
};
|
||||
use std::{path::MAIN_SEPARATOR, rc::Rc};
|
||||
|
||||
const LABEL: &str = "Export";
|
||||
const TOOLTIP_TEXT: &str = "Export selected identity to file";
|
||||
const MARGIN: i32 = 8;
|
||||
|
||||
pub struct Save {
|
||||
pub button: Button,
|
||||
}
|
||||
|
||||
impl Save {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(profile: &Rc<Profile>, list: &Rc<List>) -> Self {
|
||||
// Init main widget
|
||||
let button = Button::builder()
|
||||
.label(LABEL)
|
||||
.margin_top(MARGIN)
|
||||
.tooltip_text(TOOLTIP_TEXT)
|
||||
.visible(false)
|
||||
.build();
|
||||
|
||||
// Init events
|
||||
button.connect_clicked({
|
||||
let button = button.clone();
|
||||
let list = list.clone();
|
||||
let profile = profile.clone();
|
||||
move |_| {
|
||||
// Get selected identity from holder
|
||||
match list.selected().value_enum() {
|
||||
Value::ProfileIdentityId(profile_identity_id) => {
|
||||
// Lock open button (prevent double click)
|
||||
button.set_sensitive(false);
|
||||
|
||||
// Create PEM file based on option ID selected
|
||||
match Certificate::new(profile.clone(), profile_identity_id) {
|
||||
Ok(certificate) => {
|
||||
// Init file filters related with PEM extension
|
||||
let filters = ListStore::new::<FileFilter>();
|
||||
|
||||
let filter_all = FileFilter::new();
|
||||
filter_all.add_pattern("*.*");
|
||||
filter_all.set_name(Some("All"));
|
||||
filters.append(&filter_all);
|
||||
|
||||
let filter_pem = FileFilter::new();
|
||||
filter_pem.add_mime_type("application/x-x509-ca-cert");
|
||||
filter_pem.set_name(Some("Certificate (*.pem)"));
|
||||
filters.append(&filter_pem);
|
||||
|
||||
// Init file dialog
|
||||
FileDialog::builder()
|
||||
.default_filter(&filter_pem)
|
||||
.filters(&filters)
|
||||
.initial_name(format!(
|
||||
"{}.pem",
|
||||
certificate
|
||||
.name
|
||||
.trim_matches(MAIN_SEPARATOR)
|
||||
.replace(MAIN_SEPARATOR, "-")
|
||||
))
|
||||
.build()
|
||||
.save(Window::NONE, Cancellable::NONE, {
|
||||
let button = button.clone();
|
||||
move |result| {
|
||||
match result {
|
||||
Ok(file) => match file.replace(
|
||||
None,
|
||||
false,
|
||||
FileCreateFlags::NONE,
|
||||
Cancellable::NONE, // @TODO
|
||||
) {
|
||||
Ok(stream) => stream.write_async(
|
||||
certificate.data,
|
||||
Priority::DEFAULT,
|
||||
Cancellable::NONE, // @TODO
|
||||
{
|
||||
let button = button.clone();
|
||||
move |result| match result {
|
||||
Ok(_) => {
|
||||
button.set_css_classes(&[
|
||||
"success",
|
||||
]);
|
||||
button.set_label(&format!(
|
||||
"Saved to {}",
|
||||
file.parse_name()
|
||||
))
|
||||
}
|
||||
Err((_, e)) => {
|
||||
button.set_css_classes(&[
|
||||
"error",
|
||||
]);
|
||||
button.set_label(&e.to_string())
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label(&e.to_string())
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["warning"]);
|
||||
button.set_label(e.message())
|
||||
}
|
||||
}
|
||||
button.set_sensitive(true); // unlock
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label(&e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => todo!(), // unexpected
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return activated `Self`
|
||||
Self { button }
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
pub fn update(&self, is_visible: bool) {
|
||||
self.button.set_visible(is_visible)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
mod error;
|
||||
pub use error::Error;
|
||||
|
||||
use crate::profile::Profile;
|
||||
use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Certificate details holder for export to file action
|
||||
pub struct Certificate {
|
||||
pub data: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Certificate {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn new(profile: Rc<Profile>, profile_identity_id: i64) -> Result<Self, Error> {
|
||||
match profile.identity.database.record(profile_identity_id) {
|
||||
Ok(record) => match record {
|
||||
Some(identity) => match TlsCertificate::from_pem(&identity.pem) {
|
||||
Ok(certificate) => Ok(Self {
|
||||
data: identity.pem,
|
||||
name: certificate.subject_name().unwrap().replace("CN=", ""),
|
||||
}),
|
||||
Err(e) => Err(Error::TlsCertificate(e)),
|
||||
},
|
||||
None => Err(Error::NotFound(profile_identity_id)),
|
||||
},
|
||||
Err(e) => Err(Error::Database(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
use gtk::glib;
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Database(sqlite::Error),
|
||||
NotFound(i64),
|
||||
TlsCertificate(glib::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Database(e) => {
|
||||
write!(f, "Database error: {e}")
|
||||
}
|
||||
Self::NotFound(profile_identity_id) => {
|
||||
write!(f, "Record for `{profile_identity_id}` not found")
|
||||
}
|
||||
Self::TlsCertificate(e) => {
|
||||
write!(f, "TLS certificate error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
use adw::{
|
||||
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
|
||||
AlertDialog,
|
||||
};
|
||||
|
||||
const HEADING: &str = "Oops";
|
||||
const BODY: &str = "Identity not supported for this request";
|
||||
const RESPONSE_QUIT: (&str, &str) = ("close", "Close");
|
||||
|
||||
pub trait Unsupported {
|
||||
fn unsupported() -> Self;
|
||||
}
|
||||
|
||||
impl Unsupported for AlertDialog {
|
||||
// Construct
|
||||
|
||||
/// Create new `Self`
|
||||
fn unsupported() -> Self {
|
||||
// Init gobject
|
||||
let this = AlertDialog::builder()
|
||||
.heading(HEADING)
|
||||
.body(BODY)
|
||||
.close_response(RESPONSE_QUIT.0)
|
||||
.default_response(RESPONSE_QUIT.0)
|
||||
.build();
|
||||
|
||||
// Set response variants
|
||||
this.add_responses(&[RESPONSE_QUIT]);
|
||||
|
||||
// Decorate default response preset
|
||||
/* contrast issue with Ubuntu orange accents
|
||||
this.set_response_appearance(RESPONSE_QUIT.0, ResponseAppearance::Destructive); */
|
||||
|
||||
// Init events
|
||||
this.connect_response(None, move |dialog, response| {
|
||||
if response == RESPONSE_QUIT.0 {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Return new activated `Self`
|
||||
this
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue