use std::cell::OnceCell;
use adw::prelude::*;
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, clone},
subclass::prelude::*,
};
use crate::{
application::Application,
config,
models::{keyring, Account, OTPUri, ProvidersModel, SETTINGS},
utils::spawn_tokio_blocking,
widgets::{
accounts::AccountDetailsPage,
providers::{ProvidersList, ProvidersListView},
AccountAddDialog, ErrorRevealer,
},
};
pub enum View {
Login,
Accounts,
Account(Account),
}
mod imp {
use adw::subclass::prelude::*;
use glib::subclass;
use super::*;
#[derive(Default, gtk::CompositeTemplate, glib::Properties)]
#[template(resource = "/com/belmoussaoui/Authenticator/window.ui")]
#[properties(wrapper_type = super::Window)]
pub struct Window {
#[property(get, set, construct_only)]
pub model: OnceCell<ProvidersModel>,
#[template_child]
pub main_stack: TemplateChild<gtk::Stack>,
#[template_child]
pub providers: TemplateChild<ProvidersList>,
#[template_child]
pub account_details: TemplateChild<AccountDetailsPage>,
#[template_child]
pub search_bar: TemplateChild<gtk::SearchBar>,
#[template_child]
pub search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub navigation_view: TemplateChild<adw::NavigationView>,
#[template_child]
pub error_revealer: TemplateChild<ErrorRevealer>,
#[template_child]
pub search_btn: TemplateChild<gtk::ToggleButton>,
#[template_child]
pub password_entry: TemplateChild<gtk::PasswordEntry>,
#[template_child]
pub locked_status_page: TemplateChild<adw::StatusPage>,
#[template_child]
pub accounts_stack: TemplateChild<gtk::Stack>,
#[template_child]
pub empty_status_page: TemplateChild<adw::StatusPage>,
#[template_child]
pub unlock_button: TemplateChild<gtk::Button>,
#[template_child]
pub toast_overlay: TemplateChild<adw::ToastOverlay>,
}
#[glib::object_subclass]
impl ObjectSubclass for Window {
const NAME: &'static str = "Window";
type Type = super::Window;
type ParentType = adw::ApplicationWindow;
type Interfaces = (gio::Initable,);
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_instance_callbacks();
klass.install_action("win.search", None, |win, _, _| {
let search_btn = &win.imp().search_btn;
search_btn.set_active(!search_btn.is_active());
});
klass.install_action("win.add_account", None, |win, _, _| {
win.open_add_account(None);
});
klass.install_action("win.back", None, |win, _, _| {
win.set_view(View::Accounts);
});
klass.install_action("win.unlock", None, |win, _, _| {
let imp = win.imp();
let app = win.app();
let password = imp.password_entry.text();
let is_current_password = spawn_tokio_blocking(async move {
keyring::is_current_password(&password)
.await
.unwrap_or_else(|err| {
tracing::debug!("Could not verify password: {:?}", err);
false
})
});
if is_current_password {
imp.password_entry.set_text("");
app.set_is_locked(false);
app.restart_lock_timeout();
win.set_view(View::Accounts);
win.model().load();
} else {
imp.error_revealer.popup(&gettext("Wrong Password"));
}
});
}
fn instance_init(obj: &subclass::InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for Window {
fn constructed(&self) {
self.parent_constructed();
let win = self.obj();
self.providers.set_model(win.model());
self.providers.model().connect_items_changed(clone!(
#[weak]
win,
move |_, _, _, _| {
if !win.app().is_locked() {
win.set_view(View::Accounts);
}
}
));
win.set_icon_name(Some(config::APP_ID));
self.empty_status_page.set_icon_name(Some(config::APP_ID));
self.locked_status_page.set_icon_name(Some(config::APP_ID));
let width = SETTINGS.window_width();
let height = SETTINGS.window_height();
if width > -1 && height > -1 {
win.set_default_size(width, height);
}
let is_maximized = SETTINGS.is_maximized();
if is_maximized {
win.maximize();
}
self.account_details.set_providers_model(win.model());
if config::PROFILE == "Devel" {
win.add_css_class("devel");
}
win.set_view(View::Accounts);
self.search_bar.connect_entry(&*self.search_entry);
}
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {
fn enable_debugging(&self, toggle: bool) -> bool {
if config::PROFILE != "Devel" {
tracing::warn!("Inspector is disabled for non development builds");
false
} else {
self.parent_enable_debugging(toggle)
}
}
fn close_request(&self) -> glib::Propagation {
if let Err(err) = self.obj().save_window_state() {
tracing::warn!("Failed to save window state {:#?}", err);
}
self.parent_close_request()
}
}
impl ApplicationWindowImpl for Window {}
impl AdwApplicationWindowImpl for Window {}
impl InitableImpl for Window {
fn init(&self, _cancellable: Option<&gio::Cancellable>) -> Result<(), glib::Error> {
let win = self.obj();
let app = win.app();
win.action_set_enabled("win.add_account", !app.is_locked());
app.connect_is_locked_notify(clone!(
#[weak]
win,
move |app| {
let is_locked = app.is_locked();
win.action_set_enabled("win.add_account", !is_locked);
if is_locked {
win.set_view(View::Login);
} else {
win.set_view(View::Accounts);
};
}
));
if app.is_locked() {
win.set_view(View::Login);
}
Ok(())
}
}
}
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
@implements gio::Initable, gio::ActionMap, gio::ActionGroup, gtk::Native, gtk::Root;
}
#[gtk::template_callbacks]
impl Window {
pub fn new(model: &ProvidersModel, app: &Application) -> Self {
gio::Initable::builder()
.property("application", app)
.property("model", model)
.build(gio::Cancellable::NONE)
.unwrap()
}
pub fn set_view(&self, view: View) {
let imp = self.imp();
match view {
View::Login => {
self.set_default_widget(Some(&*imp.unlock_button));
imp.main_stack.set_visible_child_name("login");
imp.search_bar.set_key_capture_widget(gtk::Widget::NONE);
imp.password_entry.grab_focus();
}
View::Accounts => {
self.set_default_widget(gtk::Widget::NONE);
imp.main_stack.set_visible_child_name("unlocked");
imp.navigation_view.pop();
if imp.providers.model().n_items() == 0 {
if self.model().has_providers() {
imp.providers.set_view(ProvidersListView::NoSearchResults);
} else {
imp.accounts_stack.set_visible_child_name("empty");
imp.search_bar.set_key_capture_widget(gtk::Widget::NONE);
}
} else {
imp.providers.set_view(ProvidersListView::List);
imp.accounts_stack.set_visible_child_name("accounts");
imp.search_bar.set_key_capture_widget(Some(self));
}
}
View::Account(account) => {
self.set_default_widget(gtk::Widget::NONE);
imp.search_bar.set_key_capture_widget(gtk::Widget::NONE);
imp.main_stack.set_visible_child_name("unlocked");
imp.navigation_view.push_by_tag("account");
imp.account_details.set_account(&account);
}
}
}
pub fn add_toast(&self, toast: adw::Toast) {
self.imp().toast_overlay.add_toast(toast);
}
pub fn open_add_account(&self, otp_uri: Option<&OTPUri>) {
let model = self.model();
let dialog = AccountAddDialog::new(&model);
if let Some(uri) = otp_uri {
dialog.set_from_otp_uri(uri);
}
dialog.connect_added(clone!(
#[weak(rename_to = win)]
self,
move |_| {
win.providers().refilter();
}
));
dialog.present(Some(self));
}
pub fn providers(&self) -> ProvidersList {
self.imp().providers.clone()
}
fn app(&self) -> Application {
self.application().and_downcast::<Application>().unwrap()
}
fn save_window_state(&self) -> anyhow::Result<()> {
let size = self.default_size();
SETTINGS.set_window_width(size.0)?;
SETTINGS.set_window_height(size.1)?;
SETTINGS.set_is_maximized(self.is_maximized())?;
Ok(())
}
#[template_callback]
fn on_password_entry_activate(&self) {
WidgetExt::activate_action(self, "win.unlock", None).unwrap();
}
#[template_callback]
fn on_account_removed(&self, account: Account) {
let provider = account.provider();
account.delete().unwrap();
provider.remove_account(&account);
self.providers().refilter();
self.set_view(View::Accounts);
}
#[template_callback]
fn on_provider_changed(&self) {
self.providers().refilter();
}
#[template_callback]
fn on_account_shared(&self, account: Account) {
self.set_view(View::Account(account));
}
#[template_callback]
fn on_gesture_click_pressed(&self) {
self.app().restart_lock_timeout();
}
#[template_callback]
fn on_key_pressed(&self) -> glib::Propagation {
self.app().restart_lock_timeout();
glib::Propagation::Proceed
}
#[template_callback]
fn on_search_changed(&self, entry: >k::SearchEntry) {
let text = entry.text().to_string();
self.imp().providers.search(text);
}
#[template_callback]
fn on_search_started(&self, _entry: >k::SearchEntry) {
self.imp().search_btn.set_active(true);
}
#[template_callback]
fn on_search_stopped(&self, _entry: >k::SearchEntry) {
self.imp().search_btn.set_active(false);
}
}