move item property components to separated mods

This commit is contained in:
yggverse 2024-12-06 23:27:47 +02:00
parent 2799ce37fe
commit a7e809bb39
11 changed files with 254 additions and 195 deletions

View File

@ -3,15 +3,9 @@ use widget::{form::list::item::value::Value, Widget};
use crate::app::browser::window::Action;
use crate::profile::Profile;
use gtk::{
gio::{prelude::TlsCertificateExt, TlsCertificate},
glib::{gformat, Uri},
prelude::IsA,
};
use gtk::{glib::Uri, prelude::IsA};
use std::rc::Rc;
const DATE_FORMAT: &str = "%Y.%m.%d";
pub struct Gemini {
// profile: Rc<Profile>,
widget: Rc<Widget>,
@ -20,136 +14,13 @@ pub struct Gemini {
impl Gemini {
// Construct
/// Create new `Self` for given Profile
/// Create new `Self` for given `Profile`
pub fn new(profile: Rc<Profile>, action: Rc<Action>, auth_uri: Uri) -> Self {
// Init widget
let widget = Rc::new(Widget::new(profile.clone()));
// Init shared URL string from URI
let url = auth_uri.to_string();
let auth_url = auth_uri.to_string();
// Add guest option
widget.form.list.append(
Value::UseGuestSession,
"Guest session",
"No identity for this request",
None,
false,
);
// Add new identity option
widget.form.list.append(
Value::GenerateNewAuth,
"Create new",
"Generate long-term certificate",
None,
false,
);
// Add import existing identity option
widget.form.list.append(
Value::ImportPem,
"Import identity",
"Use existing certificate",
None,
false,
);
// Collect identities as options from profile database
// * memory cache synced also and could be faster @TODO
match profile.identity.gemini.database.records() {
Ok(identities) => {
for identity in identities {
// Get certificate details
let certificate = match TlsCertificate::from_pem(&identity.pem) {
Ok(certificate) => certificate,
Err(reason) => todo!("{reason}"),
};
// Init tooltip components
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>"
));
}
}
// Collect scope info
let mut scope = Vec::new();
for auth in profile
.identity
.gemini
.auth
.database
.records_scope(None)
.unwrap()
.iter()
.filter(|this| this.profile_identity_gemini_id == identity.id)
{
scope.push(format!("<small>{}</small>", auth.scope.clone()))
}
if !scope.is_empty() {
tooltip.push_str(&format!("\n\n<b>Scope</b>\n\n{}", scope.join("\n")));
}
// Append record option
widget.form.list.append(
Value::ProfileIdentityGeminiId(identity.id),
// title
&certificate
.subject_name()
.unwrap_or(gformat!("Unknown"))
.replace("CN=", ""), // trim prefix
// subtitle
&format!(
"{} - {} | scope: {}",
certificate
.not_valid_before()
.unwrap() // @TODO
.format(DATE_FORMAT)
.unwrap(),
certificate
.not_valid_after()
.unwrap() // @TODO
.format(DATE_FORMAT)
.unwrap(),
scope.len(),
),
Some(&tooltip),
profile
.identity
.gemini
.auth
.memory
.match_scope(&url)
.is_some_and(|auth| auth.profile_identity_gemini_id == identity.id), // is selected
);
}
}
Err(e) => todo!("{e}"),
} // @TODO separate markup
// Init widget
let widget = Rc::new(Widget::new(profile.clone(), &auth_url));
// Init events
widget.on_apply({
@ -158,8 +29,8 @@ impl Gemini {
// Get option match user choice
let option = match response {
Value::ProfileIdentityGeminiId(value) => Some(value),
Value::UseGuestSession => None,
Value::GenerateNewAuth => Some(
Value::GuestSession => None,
Value::GeneratePem => Some(
match profile
.identity
.gemini
@ -189,14 +60,14 @@ impl Gemini {
.identity
.gemini
.auth
.apply(profile_identity_gemini_id, &url)
.apply(profile_identity_gemini_id, &auth_url)
{
todo!("{}", reason.to_string())
};
}
// Remove all identity auths for `auth_uri`
None => {
if let Err(reason) = profile.identity.gemini.auth.remove_scope(&url) {
if let Err(reason) = profile.identity.gemini.auth.remove_scope(&auth_url) {
todo!("{}", reason.to_string())
};
}

View File

@ -33,12 +33,12 @@ impl Widget {
// Constructors
/// Create new `Self`
pub fn new(profile: Rc<Profile>) -> Self {
pub fn new(profile: Rc<Profile>, auth_url: &str) -> Self {
// Init actions
let action = Rc::new(Action::new());
// Init child container
let form = Rc::new(Form::new(profile, action.clone()));
let form = Rc::new(Form::new(profile, action.clone(), auth_url));
// Init main widget
let alert_dialog = AlertDialog::builder()

View File

@ -32,10 +32,10 @@ impl Form {
// Constructors
/// Create new `Self`
pub fn new(profile: Rc<Profile>, action: Rc<Action>) -> Self {
pub fn new(profile: Rc<Profile>, action: Rc<Action>, auth_url: &str) -> Self {
// Init components
let file = Rc::new(File::new(action.clone()));
let list = Rc::new(List::new());
let list = Rc::new(List::new(profile.clone(), auth_url));
let name = Rc::new(Name::new(action.clone()));
let save = Rc::new(Save::new(profile.clone()));
let drop = Rc::new(Drop::new(profile.clone(), action.clone(), list.clone()));
@ -61,7 +61,7 @@ impl Form {
let update = action.update.clone();
move |item| {
// Change name entry visibility
name.update(matches!(item, Value::GenerateNewAuth));
name.update(matches!(item, Value::GeneratePem));
// Change file choose button visibility
file.update(matches!(item, Value::ImportPem));
@ -103,7 +103,7 @@ impl Form {
/// Validate `Self` components match current selection
pub fn is_applicable(&self) -> bool {
match self.list.selected_item().value_enum() {
Value::GenerateNewAuth => self.name.is_valid(),
Value::GeneratePem => self.name.is_valid(),
Value::ImportPem => self.file.is_valid(),
Value::ProfileIdentityGeminiId(_) => !self.list.selected_item().is_active(),
_ => true,

View File

@ -1,6 +1,9 @@
pub mod item;
use std::rc::Rc;
use item::{value::Value, Item};
use crate::profile::Profile;
use gtk::{
gdk::Cursor,
gio::{
@ -20,10 +23,33 @@ impl List {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
// Init `ListStore` with custom `DropDown` properties
pub fn new(profile: Rc<Profile>, auth_url: &str) -> Self {
// Init model
let list_store = ListStore::new::<Item>();
list_store.append(&Item::new_guest_session());
list_store.append(&Item::new_generate_pem());
list_store.append(&Item::new_import_pem());
// Append identities from profile database
// * memory cache synced also and could be faster @TODO
match profile.identity.gemini.database.records() {
Ok(identities) => {
for identity in identities {
match Item::new_profile_identity_gemini_id(
profile.clone(),
identity.id,
&identity.pem,
auth_url,
) {
Ok(item) => list_store.append(&item),
Err(_) => todo!(),
}
}
}
Err(_) => todo!(),
}
// Setup item factory
// * wanted only to append items after `DropDown` init
let factory = SignalListItemFactory::new();
@ -99,6 +125,13 @@ impl List {
.factory(&factory)
.build();
// Select active record
dropdown.set_selected(
list_store
.find_with_equal_func(|item| item.dynamic_cast_ref::<Item>().unwrap().is_active())
.unwrap(),
); // @TODO panic or handle?
// Return activated `Self`
Self {
list_store,
@ -108,25 +141,6 @@ impl List {
// Actions
/// Append new item
pub fn append(
&self,
value: Value,
title: &str,
subtitle: &str,
tooltip: Option<&str>,
is_active: bool,
) {
let item = Item::new(value, title, subtitle, tooltip, is_active);
self.list_store.append(&item);
if is_active {
self.dropdown
.set_selected(self.list_store.find(&item).unwrap()); // @TODO panic or handle?
}
}
/// Find list item by `value` (stores ID)
/// * return `position` found
pub fn find(&self, value: i64) -> Option<u32> {

View File

@ -1,8 +1,21 @@
mod error;
mod imp;
mod is_active;
mod subtitle;
mod title;
mod tooltip;
pub mod value;
use gtk::glib::{self, Object};
use value::Value;
use std::rc::Rc;
pub use error::Error;
pub use value::Value;
use crate::profile::Profile;
use gtk::{
gio::TlsCertificate,
glib::{self, Object},
};
glib::wrapper! {
pub struct Item(ObjectSubclass<imp::Item>);
@ -10,47 +23,105 @@ glib::wrapper! {
// C-type property `value` conversion for `Item`
// * values > 0 reserved for `profile_identity_gemini_id`
const G_VALUE_GENERATE_NEW_AUTH: i64 = 0;
const G_VALUE_GENERATE_PEM: i64 = 0;
const G_VALUE_IMPORT_PEM: i64 = -1;
const G_VALUE_USE_GUEST_SESSION: i64 = -2;
const G_VALUE_GUEST_SESSION: i64 = -2;
impl Item {
// Constructors
/// Create new `GObject`
pub fn new(
value: Value,
title: &str,
subtitle: &str,
tooltip: Option<&str>,
is_active: bool,
) -> Self {
pub fn new_guest_session() -> Self {
Object::builder()
.property(
"value",
match value {
Value::GenerateNewAuth => G_VALUE_GENERATE_NEW_AUTH,
Value::ImportPem => G_VALUE_IMPORT_PEM,
Value::UseGuestSession => G_VALUE_USE_GUEST_SESSION,
Value::ProfileIdentityGeminiId(value) => value,
},
)
.property("title", title)
.property("subtitle", subtitle)
.property("tooltip", tooltip.unwrap_or_default())
.property("is_active", is_active)
.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_gemini_id(
profile: Rc<Profile>,
profile_identity_gemini_id: i64,
pem: &str,
auth_url: &str,
) -> Result<Self, Error> {
match TlsCertificate::from_pem(pem) {
Ok(certificate) => {
// Collect shared certificate scope
let scope = scope(profile.clone(), profile_identity_gemini_id);
// Build GObject
Ok(Object::builder()
.property("value", profile_identity_gemini_id)
.property(
"title",
title::new_for_profile_identity_gemini_id(&certificate),
)
.property(
"subtitle",
subtitle::new_for_profile_identity_gemini_id(&certificate, &scope),
)
.property(
"tooltip",
tooltip::new_for_profile_identity_gemini_id(&certificate, &scope),
)
.property(
"is_active",
is_active::new_for_profile_identity_gemini_id(
profile,
profile_identity_gemini_id,
auth_url,
),
)
.build())
}
Err(e) => Err(Error::TlsCertificate(e)),
}
}
// Getters
/// Get `value` as enum `Value`
/// Get `Self` C-value as `Value`
pub fn value_enum(&self) -> Value {
match self.value() {
G_VALUE_GENERATE_NEW_AUTH => Value::GenerateNewAuth,
G_VALUE_GENERATE_PEM => Value::GeneratePem,
G_VALUE_GUEST_SESSION => Value::GuestSession,
G_VALUE_IMPORT_PEM => Value::ImportPem,
G_VALUE_USE_GUEST_SESSION => Value::UseGuestSession,
value => Value::ProfileIdentityGeminiId(value),
}
}
}
// Tools
fn scope(profile: Rc<Profile>, profile_identity_gemini_id: i64) -> Vec<String> {
let mut scope = Vec::new();
for auth in profile
.identity
.gemini
.auth
.database
.records_scope(None)
.unwrap()
.iter()
.filter(|this| this.profile_identity_gemini_id == profile_identity_gemini_id)
{
scope.push(auth.scope.clone())
}
scope
}

View File

@ -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}`")
}
}
}
}

View File

@ -0,0 +1,16 @@
use crate::profile::Profile;
use std::rc::Rc;
pub fn new_for_profile_identity_gemini_id(
profile: Rc<Profile>,
profile_identity_gemini_id: i64,
auth_url: &str,
) -> bool {
profile
.identity
.gemini
.auth
.memory
.match_scope(&auth_url)
.is_some_and(|auth| auth.profile_identity_gemini_id == profile_identity_gemini_id)
}

View File

@ -0,0 +1,23 @@
use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt};
const DATE_FORMAT: &str = "%Y.%m.%d";
pub fn new_for_profile_identity_gemini_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(),
)
}

View File

@ -0,0 +1,8 @@
use gtk::{gio::TlsCertificate, glib::gformat, prelude::TlsCertificateExt};
pub fn new_for_profile_identity_gemini_id(certificate: &TlsCertificate) -> String {
certificate
.subject_name()
.unwrap_or(gformat!("Unknown"))
.replace("CN=", "")
}

View File

@ -0,0 +1,40 @@
use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt};
pub fn new_for_profile_identity_gemini_id(
certificate: &TlsCertificate,
scope: &Vec<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{}", path));
}
}
tooltip
}

View File

@ -1,7 +1,7 @@
#[derive(Debug)]
pub enum Value {
GenerateNewAuth,
GeneratePem,
GuestSession,
ImportPem,
ProfileIdentityGeminiId(i64),
UseGuestSession,
}