authenticator/widgets/providers/
page.rs
use adw::{
prelude::*,
subclass::{navigation_page::*, prelude::*},
};
use gettextrs::gettext;
use gtk::{
gdk_pixbuf, gio,
glib::{self, translate::IntoGlib},
};
use crate::{
models::{i18n, Algorithm, Method, Provider, ProviderPatch, FAVICONS_PATH, OTP},
widgets::{ErrorRevealer, ProviderImage},
};
mod imp {
use std::{cell::RefCell, sync::LazyLock};
use glib::subclass::Signal;
use super::*;
#[derive(gtk::CompositeTemplate)]
#[template(resource = "/com/belmoussaoui/Authenticator/provider_page.ui")]
pub struct ProviderPage {
pub actions: gio::SimpleActionGroup,
pub methods_model: adw::EnumListModel,
pub algorithms_model: adw::EnumListModel,
#[template_child]
pub error_revealer: TemplateChild<ErrorRevealer>,
#[template_child]
pub image: TemplateChild<ProviderImage>,
#[template_child]
pub name_entry: TemplateChild<adw::EntryRow>,
#[template_child]
pub period_spinbutton: TemplateChild<adw::SpinRow>,
#[template_child]
pub digits_spinbutton: TemplateChild<adw::SpinRow>,
#[template_child]
pub default_counter_spinbutton: TemplateChild<adw::SpinRow>,
#[template_child]
pub provider_website_entry: TemplateChild<adw::EntryRow>,
#[template_child]
pub provider_help_entry: TemplateChild<adw::EntryRow>,
#[template_child]
pub method_comborow: TemplateChild<adw::ComboRow>,
#[template_child]
pub algorithm_comborow: TemplateChild<adw::ComboRow>,
#[template_child]
pub delete_button: TemplateChild<gtk::Button>,
pub selected_provider: RefCell<Option<Provider>>,
pub selected_image: RefCell<Option<gio::File>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ProviderPage {
const NAME: &'static str = "ProviderPage";
type Type = super::ProviderPage;
type ParentType = adw::NavigationPage;
fn new() -> Self {
let methods_model = adw::EnumListModel::new(Method::static_type());
let algorithms_model = adw::EnumListModel::new(Algorithm::static_type());
Self {
actions: gio::SimpleActionGroup::new(),
image: TemplateChild::default(),
error_revealer: TemplateChild::default(),
name_entry: TemplateChild::default(),
period_spinbutton: TemplateChild::default(),
digits_spinbutton: TemplateChild::default(),
default_counter_spinbutton: TemplateChild::default(),
provider_website_entry: TemplateChild::default(),
provider_help_entry: TemplateChild::default(),
method_comborow: TemplateChild::default(),
algorithm_comborow: TemplateChild::default(),
delete_button: TemplateChild::default(),
methods_model,
algorithms_model,
selected_provider: RefCell::default(),
selected_image: RefCell::default(),
}
}
fn class_init(klass: &mut Self::Class) {
Method::static_type();
Algorithm::static_type();
klass.bind_template();
klass.bind_template_instance_callbacks();
klass.install_action("providers.save", None, |page, _, _| {
if let Err(err) = page.save() {
tracing::warn!("Failed to save provider {}", err);
}
});
klass.install_action("providers.delete", None, |page, _, _| {
if let Err(err) = page.delete_provider() {
tracing::warn!("Failed to delete the provider {}", err);
}
});
klass.install_action("providers.reset_image", None, |page, _, _| {
page.reset_image();
});
klass.install_action_async("providers.select_image", None, |page, _, _| async move {
page.open_select_image().await;
});
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ProviderPage {
fn signals() -> &'static [Signal] {
static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
vec![
Signal::builder("created")
.param_types([Provider::static_type()])
.build(),
Signal::builder("updated")
.param_types([Provider::static_type()])
.build(),
Signal::builder("deleted")
.param_types([Provider::static_type()])
.build(),
]
});
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
self.algorithm_comborow
.set_model(Some(&self.algorithms_model));
self.method_comborow.set_model(Some(&self.methods_model));
self.obj().action_set_enabled("providers.save", false);
}
}
impl WidgetImpl for ProviderPage {}
impl NavigationPageImpl for ProviderPage {}
}
glib::wrapper! {
pub struct ProviderPage(ObjectSubclass<imp::ProviderPage>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl ProviderPage {
pub fn set_provider(&self, provider: Option<Provider>) {
let imp = self.imp();
if let Some(provider) = provider {
imp.delete_button.set_visible(true);
imp.name_entry.set_text(&provider.name());
imp.period_spinbutton.set_value(provider.period() as f64);
if let Some(ref website) = provider.website() {
imp.provider_website_entry.set_text(website);
} else {
imp.provider_website_entry.set_text("");
}
if let Some(ref website) = provider.help_url() {
imp.provider_help_entry.set_text(website);
} else {
imp.provider_help_entry.set_text("");
}
imp.algorithm_comborow.set_selected(
imp.algorithms_model
.find_position(provider.algorithm().into_glib()),
);
imp.default_counter_spinbutton
.set_value(provider.default_counter() as f64);
imp.digits_spinbutton.set_value(provider.digits() as f64);
imp.method_comborow.set_selected(
imp.methods_model
.find_position(provider.method().into_glib()),
);
imp.image.set_provider(Some(&provider));
self.set_title(&i18n::i18n_f("Editing Provider: {}", &[&provider.name()]));
imp.selected_provider.replace(Some(provider));
} else {
imp.name_entry.set_text("");
imp.delete_button.set_visible(false);
imp.period_spinbutton.set_value(OTP::DEFAULT_PERIOD as f64);
imp.provider_website_entry.set_text("");
imp.provider_help_entry.set_text("");
imp.algorithm_comborow.set_selected(
imp.algorithms_model
.find_position(Algorithm::default().into_glib()),
);
imp.default_counter_spinbutton
.set_value(OTP::DEFAULT_COUNTER as f64);
imp.digits_spinbutton.set_value(OTP::DEFAULT_DIGITS as f64);
imp.method_comborow.set_selected(
imp.methods_model
.find_position(Method::default().into_glib()),
);
imp.image.set_provider(None::<Provider>);
self.set_title(&gettext("New Provider"));
imp.selected_provider.replace(None);
}
}
pub fn name_entry(&self) -> adw::EntryRow {
self.imp().name_entry.clone()
}
fn save(&self) -> anyhow::Result<()> {
let imp = self.imp();
let name = imp.name_entry.text();
let website = imp.provider_website_entry.text().to_string();
let help_url = imp.provider_help_entry.text().to_string();
let period = imp.period_spinbutton.value() as u32;
let digits = imp.digits_spinbutton.value() as u32;
let method = Method::from(imp.method_comborow.selected());
let algorithm = Algorithm::from(imp.algorithm_comborow.selected());
let default_counter = imp.default_counter_spinbutton.value() as u32;
let image_uri = if let Some(file) = imp.selected_image.borrow().as_ref() {
let basename = file.basename().unwrap();
let icon_name = glib::base64_encode(basename.to_str().unwrap().as_bytes());
let small_icon_name = format!("{icon_name}_32x32");
let large_icon_name = format!("{icon_name}_96x96");
let stream = file.read(gio::Cancellable::NONE)?;
let pixbuf = gdk_pixbuf::Pixbuf::from_stream(&stream, gio::Cancellable::NONE)?;
tracing::debug!("Creating a 32x32 variant of the selected favicon");
let small_pixbuf = pixbuf
.scale_simple(32, 32, gdk_pixbuf::InterpType::Bilinear)
.unwrap();
small_pixbuf.savev(FAVICONS_PATH.join(small_icon_name), "png", &[])?;
tracing::debug!("Creating a 96x96 variant of the selected favicon");
let large_pixbuf = pixbuf
.scale_simple(96, 96, gdk_pixbuf::InterpType::Bilinear)
.unwrap();
large_pixbuf.savev(FAVICONS_PATH.join(large_icon_name), "png", &[])?;
Some(icon_name.to_string())
} else {
None
};
if let Some(provider) = imp.selected_provider.borrow().as_ref() {
provider.update(&ProviderPatch {
name: name.to_string(),
website: Some(website),
help_url: Some(help_url),
image_uri,
period: period as i32,
digits: digits as i32,
default_counter: default_counter as i32,
algorithm: algorithm.to_string(),
method: method.to_string(),
is_backup_restore: false,
})?;
self.emit_by_name::<()>("updated", &[provider]);
} else {
let provider = Provider::create(
&name,
period,
algorithm,
Some(website),
method,
digits,
default_counter,
Some(help_url),
image_uri,
)?;
self.emit_by_name::<()>("created", &[&provider]);
}
Ok(())
}
async fn open_select_image(&self) {
let parent = self.root().and_downcast::<gtk::Window>().unwrap();
let images_filter = gtk::FileFilter::new();
images_filter.set_name(Some(&gettext("Image")));
images_filter.add_pixbuf_formats();
let model = gio::ListStore::new::<gtk::FileFilter>();
model.append(&images_filter);
let file_chooser = gtk::FileDialog::builder()
.modal(true)
.filters(&model)
.build();
if let Ok(file) = file_chooser.open_future(Some(&parent)).await {
self.set_image(file);
};
}
fn set_image(&self, file: gio::File) {
let imp = self.imp();
imp.image.set_from_file(&file);
imp.selected_image.replace(Some(file));
}
fn reset_image(&self) {
let imp = self.imp();
imp.image.reset();
imp.selected_image.replace(None);
}
fn delete_provider(&self) -> anyhow::Result<()> {
let imp = self.imp();
if let Some(provider) = imp.selected_provider.borrow().as_ref() {
if provider.has_accounts() {
imp.error_revealer.popup(&gettext(
"The provider has accounts assigned to it, please remove them first",
));
} else if provider.delete().is_ok() {
self.emit_by_name::<()>("deleted", &[provider]);
}
} else {
anyhow::bail!("Can't remove a provider as none are selected");
}
Ok(())
}
#[template_callback]
fn entry_validate(&self, _entry: adw::EntryRow) {
let imp = self.imp();
let provider_name = imp.name_entry.text();
let provider_website = imp.provider_website_entry.text();
let provider_help_url = imp.provider_help_entry.text();
let is_valid = !provider_name.is_empty()
&& (provider_website.is_empty() || url::Url::parse(&provider_website).is_ok())
&& (provider_help_url.is_empty() || url::Url::parse(&provider_help_url).is_ok());
self.action_set_enabled("providers.save", is_valid);
}
#[template_callback]
fn on_method_changed(&self, _pspec: glib::ParamSpec, combo_row: adw::ComboRow) {
let imp = self.imp();
let selected = Method::from(combo_row.selected());
match selected {
Method::TOTP => {
imp.default_counter_spinbutton.set_visible(false);
imp.period_spinbutton.set_visible(true);
imp.digits_spinbutton.set_value(OTP::DEFAULT_DIGITS as f64);
imp.period_spinbutton.set_value(OTP::DEFAULT_PERIOD as f64);
}
Method::HOTP => {
imp.default_counter_spinbutton.set_visible(true);
imp.period_spinbutton.set_visible(false);
imp.default_counter_spinbutton
.set_value(OTP::DEFAULT_COUNTER as f64);
imp.digits_spinbutton.set_value(OTP::DEFAULT_DIGITS as f64);
}
Method::Steam => {
imp.default_counter_spinbutton.set_visible(false);
imp.period_spinbutton.set_visible(true);
imp.digits_spinbutton
.set_value(OTP::STEAM_DEFAULT_DIGITS as f64);
imp.period_spinbutton
.set_value(OTP::STEAM_DEFAULT_PERIOD as f64);
imp.algorithm_comborow
.set_selected(Algorithm::default().into_glib() as u32);
}
}
imp.algorithm_comborow
.set_sensitive(selected != Method::Steam);
imp.period_spinbutton
.set_sensitive(selected != Method::Steam);
imp.digits_spinbutton
.set_sensitive(selected != Method::Steam);
}
#[template_callback]
fn otp_method_to_locale_string(item: adw::EnumListItem) -> String {
let method = Method::from(item.value() as u32);
method.to_locale_string()
}
#[template_callback]
fn algorithm_to_locale_string(item: adw::EnumListItem) -> String {
let algorithm = Algorithm::from(item.value() as u32);
algorithm.to_locale_string()
}
}
impl Default for ProviderPage {
fn default() -> Self {
glib::Object::new()
}
}