mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 08:44:40 +01:00
Split general and backup settings
Into different dialogs. In principle, backup and restore are not app preferences and should go in a different dialog. This commit does not do any changes besides splitting methods into two parts and: - Rename the "preferences" action group to "camera-page" on the backup dialog - Remove content-width on the PreferencesWindow
This commit is contained in:
parent
2721b9145d
commit
293a76d67f
12 changed files with 605 additions and 485 deletions
|
@ -16,6 +16,7 @@
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="providers_list.ui">resources/ui/providers_list.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="providers_list.ui">resources/ui/providers_list.ui</file>
|
||||||
|
|
||||||
<!-- UI Files -->
|
<!-- UI Files -->
|
||||||
|
<file compressed="true" preprocess="xml-stripblanks" alias="backup_dialog.ui">resources/ui/backup_dialog.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="preferences.ui">resources/ui/preferences.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="preferences.ui">resources/ui/preferences.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="preferences_camera_page.ui">resources/ui/preferences_camera_page.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="preferences_camera_page.ui">resources/ui/preferences_camera_page.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="preferences_password_page.ui">resources/ui/preferences_password_page.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="preferences_password_page.ui">resources/ui/preferences_password_page.ui</file>
|
||||||
|
|
22
data/resources/ui/backup_dialog.ui
Normal file
22
data/resources/ui/backup_dialog.ui
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<template class="BackupDialog" parent="AdwPreferencesDialog">
|
||||||
|
<property name="title" translatable="yes">Backup & Restore</property>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesPage">
|
||||||
|
<property name="icon-name">document-save-as-symbolic</property>
|
||||||
|
<property name="title" translatable="yes">Backup & Restore</property>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup" id="backup_group">
|
||||||
|
<property name="title" translatable="yes">Backup</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup" id="restore_group">
|
||||||
|
<property name="title" translatable="yes">Restore</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</template>
|
||||||
|
</interface>
|
|
@ -7,7 +7,6 @@
|
||||||
<property name="page-increment">10</property>
|
<property name="page-increment">10</property>
|
||||||
</object>
|
</object>
|
||||||
<template class="PreferencesWindow" parent="AdwPreferencesDialog">
|
<template class="PreferencesWindow" parent="AdwPreferencesDialog">
|
||||||
<property name="content-width">550</property>
|
|
||||||
<property name="content-height">570</property>
|
<property name="content-height">570</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="AdwPreferencesPage">
|
<object class="AdwPreferencesPage">
|
||||||
|
@ -74,21 +73,5 @@
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
|
||||||
<object class="AdwPreferencesPage">
|
|
||||||
<property name="icon-name">document-save-as-symbolic</property>
|
|
||||||
<property name="title" translatable="yes">Backup/Restore</property>
|
|
||||||
<child>
|
|
||||||
<object class="AdwPreferencesGroup" id="backup_group">
|
|
||||||
<property name="title" translatable="yes">Backup</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="AdwPreferencesGroup" id="restore_group">
|
|
||||||
<property name="title" translatable="yes">Restore</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</template>
|
</template>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
<attribute name="label" translatable="yes">_Preferences</attribute>
|
<attribute name="label" translatable="yes">_Preferences</attribute>
|
||||||
<attribute name="action">app.preferences</attribute>
|
<attribute name="action">app.preferences</attribute>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">_Backup & Restore</attribute>
|
||||||
|
<attribute name="action">app.show-backup-dialog</attribute>
|
||||||
|
</item>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<item>
|
<item>
|
||||||
|
|
|
@ -4,6 +4,7 @@ data/com.belmoussaoui.Authenticator.metainfo.xml.in.in
|
||||||
data/resources/ui/account_add.ui
|
data/resources/ui/account_add.ui
|
||||||
data/resources/ui/account_details_page.ui
|
data/resources/ui/account_details_page.ui
|
||||||
data/resources/ui/account_row.ui
|
data/resources/ui/account_row.ui
|
||||||
|
data/resources/ui/backup_dialog.ui
|
||||||
data/resources/ui/camera.ui
|
data/resources/ui/camera.ui
|
||||||
data/resources/ui/keyring_error_dialog.ui
|
data/resources/ui/keyring_error_dialog.ui
|
||||||
data/resources/ui/preferences.ui
|
data/resources/ui/preferences.ui
|
||||||
|
@ -28,8 +29,8 @@ src/models/algorithm.rs
|
||||||
src/widgets/accounts/add.rs
|
src/widgets/accounts/add.rs
|
||||||
src/widgets/accounts/details.rs
|
src/widgets/accounts/details.rs
|
||||||
src/widgets/accounts/row.rs
|
src/widgets/accounts/row.rs
|
||||||
|
src/widgets/backup/dialog.rs
|
||||||
src/widgets/preferences/password_page.rs
|
src/widgets/preferences/password_page.rs
|
||||||
src/widgets/preferences/window.rs
|
|
||||||
src/widgets/providers/dialog.rs
|
src/widgets/providers/dialog.rs
|
||||||
src/widgets/providers/page.rs
|
src/widgets/providers/page.rs
|
||||||
src/widgets/window.rs
|
src/widgets/window.rs
|
||||||
|
|
|
@ -20,7 +20,7 @@ use crate::{
|
||||||
SearchProviderAction, FAVICONS_PATH, RUNTIME, SECRET_SERVICE, SETTINGS,
|
SearchProviderAction, FAVICONS_PATH, RUNTIME, SECRET_SERVICE, SETTINGS,
|
||||||
},
|
},
|
||||||
utils::{spawn, spawn_tokio_blocking},
|
utils::{spawn, spawn_tokio_blocking},
|
||||||
widgets::{KeyringErrorDialog, PreferencesWindow, ProvidersDialog, Window},
|
widgets::{BackupDialog, KeyringErrorDialog, PreferencesWindow, ProvidersDialog, Window},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
|
@ -66,12 +66,11 @@ mod imp {
|
||||||
.activate(|app: &Self::Type, _, _| app.quit())
|
.activate(|app: &Self::Type, _, _| app.quit())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let preferences_action = gio::ActionEntry::builder("preferences")
|
let show_backup_dialog_action = gio::ActionEntry::builder("show-backup-dialog")
|
||||||
.activate(|app: &Self::Type, _, _| {
|
.activate(|app: &Self::Type, _, _| {
|
||||||
let model = &app.imp().model;
|
let model = &app.imp().model;
|
||||||
let window = app.active_window();
|
let window = app.active_window();
|
||||||
let preferences = PreferencesWindow::new(model);
|
let preferences = BackupDialog::new(model);
|
||||||
preferences.set_has_set_password(app.can_be_locked());
|
|
||||||
preferences.connect_restore_completed(clone!(
|
preferences.connect_restore_completed(clone!(
|
||||||
#[weak]
|
#[weak]
|
||||||
window,
|
window,
|
||||||
|
@ -85,6 +84,15 @@ mod imp {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
preferences.present(Some(&window));
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let preferences_action = gio::ActionEntry::builder("preferences")
|
||||||
|
.activate(|app: &Self::Type, _, _| {
|
||||||
|
let window = app.active_window();
|
||||||
|
let preferences = PreferencesWindow::new();
|
||||||
|
preferences.set_has_set_password(app.can_be_locked());
|
||||||
preferences.connect_has_set_password_notify(clone!(
|
preferences.connect_has_set_password_notify(clone!(
|
||||||
#[weak]
|
#[weak]
|
||||||
app,
|
app,
|
||||||
|
@ -146,6 +154,7 @@ mod imp {
|
||||||
lock_action,
|
lock_action,
|
||||||
providers_action,
|
providers_action,
|
||||||
preferences_action,
|
preferences_action,
|
||||||
|
show_backup_dialog_action,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let lock_action = app.lookup_action("lock").unwrap();
|
let lock_action = app.lookup_action("lock").unwrap();
|
||||||
|
|
551
src/widgets/backup/dialog.rs
Normal file
551
src/widgets/backup/dialog.rs
Normal file
|
@ -0,0 +1,551 @@
|
||||||
|
use adw::prelude::*;
|
||||||
|
use anyhow::Result;
|
||||||
|
use gettextrs::gettext;
|
||||||
|
use gtk::{
|
||||||
|
gio,
|
||||||
|
glib::{self, clone},
|
||||||
|
subclass::prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::camera_page::CameraPage;
|
||||||
|
use crate::{
|
||||||
|
backup::{
|
||||||
|
Aegis, AndOTP, Backupable, Bitwarden, FreeOTP, FreeOTPJSON, Google, LegacyAuthenticator,
|
||||||
|
Operation, RaivoOTP, Restorable, RestorableItem,
|
||||||
|
},
|
||||||
|
models::ProvidersModel,
|
||||||
|
utils::spawn,
|
||||||
|
widgets::screenshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use std::{
|
||||||
|
cell::{OnceCell, RefCell},
|
||||||
|
collections::HashMap,
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use adw::subclass::prelude::*;
|
||||||
|
use glib::subclass::Signal;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(gtk::CompositeTemplate, glib::Properties)]
|
||||||
|
#[properties(wrapper_type = super::BackupDialog)]
|
||||||
|
#[template(resource = "/com/belmoussaoui/Authenticator/backup_dialog.ui")]
|
||||||
|
pub struct BackupDialog {
|
||||||
|
#[property(get, set, construct_only)]
|
||||||
|
pub model: OnceCell<ProvidersModel>,
|
||||||
|
|
||||||
|
pub actions: gio::SimpleActionGroup,
|
||||||
|
pub backup_actions: gio::SimpleActionGroup,
|
||||||
|
pub restore_actions: gio::SimpleActionGroup,
|
||||||
|
pub camera_page: CameraPage,
|
||||||
|
pub key_entries: RefCell<HashMap<String, adw::PasswordEntryRow>>,
|
||||||
|
|
||||||
|
#[template_child]
|
||||||
|
pub backup_group: TemplateChild<adw::PreferencesGroup>,
|
||||||
|
#[template_child]
|
||||||
|
pub restore_group: TemplateChild<adw::PreferencesGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for BackupDialog {
|
||||||
|
const NAME: &'static str = "BackupDialog";
|
||||||
|
type Type = super::BackupDialog;
|
||||||
|
type ParentType = adw::PreferencesDialog;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
let actions = gio::SimpleActionGroup::new();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
camera_page: CameraPage::new(&actions),
|
||||||
|
actions,
|
||||||
|
model: OnceCell::default(),
|
||||||
|
backup_actions: gio::SimpleActionGroup::new(),
|
||||||
|
restore_actions: gio::SimpleActionGroup::new(),
|
||||||
|
backup_group: TemplateChild::default(),
|
||||||
|
restore_group: TemplateChild::default(),
|
||||||
|
key_entries: RefCell::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn class_init(klass: &mut Self::Class) {
|
||||||
|
klass.bind_template();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||||
|
obj.init_template();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::derived_properties]
|
||||||
|
impl ObjectImpl for BackupDialog {
|
||||||
|
fn signals() -> &'static [Signal] {
|
||||||
|
static SIGNALS: LazyLock<Vec<Signal>> =
|
||||||
|
LazyLock::new(|| vec![Signal::builder("restore-completed").action().build()]);
|
||||||
|
SIGNALS.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constructed(&self) {
|
||||||
|
self.parent_constructed();
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
|
obj.setup_actions();
|
||||||
|
obj.setup_widget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl WidgetImpl for BackupDialog {}
|
||||||
|
impl AdwDialogImpl for BackupDialog {}
|
||||||
|
impl PreferencesDialogImpl for BackupDialog {}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct BackupDialog(ObjectSubclass<imp::BackupDialog>)
|
||||||
|
@extends gtk::Widget, adw::Dialog, adw::PreferencesDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackupDialog {
|
||||||
|
pub fn new(model: &ProvidersModel) -> Self {
|
||||||
|
glib::Object::builder().property("model", model).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect_restore_completed<F>(&self, callback: F) -> glib::SignalHandlerId
|
||||||
|
where
|
||||||
|
F: Fn(&Self) + 'static,
|
||||||
|
{
|
||||||
|
self.connect_local(
|
||||||
|
"restore-completed",
|
||||||
|
false,
|
||||||
|
clone!(
|
||||||
|
#[weak(rename_to = win)]
|
||||||
|
self,
|
||||||
|
#[upgrade_or]
|
||||||
|
None,
|
||||||
|
move |_| {
|
||||||
|
callback(&win);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_widget(&self) {
|
||||||
|
// FreeOTP is first in all of these lists, since its the way to backup
|
||||||
|
// Authenticator for use with Authenticator. Others are sorted
|
||||||
|
// alphabetically.
|
||||||
|
|
||||||
|
self.register_backup::<FreeOTP>(&["text/plain"]);
|
||||||
|
self.register_backup::<Aegis>(&["application/json"]);
|
||||||
|
self.register_backup::<AndOTP>(&["application/json"]);
|
||||||
|
|
||||||
|
self.register_restore::<FreeOTP>(&["text/plain"]);
|
||||||
|
self.register_restore::<FreeOTPJSON>(&["application/json"]);
|
||||||
|
self.register_restore::<Aegis>(&["application/json"]);
|
||||||
|
self.register_restore::<AndOTP>(&["application/json"]);
|
||||||
|
self.register_restore::<Bitwarden>(&["application/json"]);
|
||||||
|
self.register_restore::<Google>(&[]);
|
||||||
|
self.register_restore::<LegacyAuthenticator>(&["application/json"]);
|
||||||
|
self.register_restore::<RaivoOTP>(&["application/zip"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_backup<T: Backupable>(&self, filters: &'static [&str]) {
|
||||||
|
let imp = self.imp();
|
||||||
|
if T::ENCRYPTABLE {
|
||||||
|
let row = adw::ExpanderRow::builder()
|
||||||
|
.title(T::title())
|
||||||
|
.subtitle(T::subtitle())
|
||||||
|
.show_enable_switch(false)
|
||||||
|
.enable_expansion(true)
|
||||||
|
.use_underline(true)
|
||||||
|
.build();
|
||||||
|
let key_entry = adw::PasswordEntryRow::builder()
|
||||||
|
.title(gettext("Key / Passphrase"))
|
||||||
|
.build();
|
||||||
|
row.add_row(&key_entry);
|
||||||
|
imp.key_entries
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(format!("backup.{}", T::IDENTIFIER), key_entry);
|
||||||
|
|
||||||
|
let button_row = adw::ActionRow::new();
|
||||||
|
let key_button = gtk::Button::builder()
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.halign(gtk::Align::End)
|
||||||
|
.label(gettext("Select File"))
|
||||||
|
.action_name(format!("backup.{}", T::IDENTIFIER))
|
||||||
|
.build();
|
||||||
|
button_row.add_suffix(&key_button);
|
||||||
|
row.add_row(&button_row);
|
||||||
|
|
||||||
|
imp.backup_group.add(&row);
|
||||||
|
} else {
|
||||||
|
let row = adw::ActionRow::builder()
|
||||||
|
.title(T::title())
|
||||||
|
.subtitle(T::subtitle())
|
||||||
|
.activatable(true)
|
||||||
|
.use_underline(true)
|
||||||
|
.action_name(format!("backup.{}", T::IDENTIFIER))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
imp.backup_group.add(&row);
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = gio::ActionEntry::builder(T::IDENTIFIER)
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak(rename_to = win)]
|
||||||
|
self,
|
||||||
|
move |_, _, _| {
|
||||||
|
spawn(clone!(
|
||||||
|
#[weak]
|
||||||
|
win,
|
||||||
|
async move {
|
||||||
|
if let Err(err) = win.backup_into_file::<T>(filters).await {
|
||||||
|
tracing::error!("Failed to backup into a file {err}");
|
||||||
|
win.add_toast(adw::Toast::new(&gettext(
|
||||||
|
"Failed to create a backup",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
imp.backup_actions.add_action_entries([action]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn backup_into_file<T: Backupable>(&self, filters: &'static [&str]) -> Result<()> {
|
||||||
|
let model = self.model();
|
||||||
|
let file = self.select_file(filters, Operation::Backup).await?;
|
||||||
|
let key = T::ENCRYPTABLE
|
||||||
|
.then(|| self.encryption_key(Operation::Backup, T::IDENTIFIER))
|
||||||
|
.flatten();
|
||||||
|
let content = T::backup(&model, key.as_deref())?;
|
||||||
|
file.replace_contents_future(
|
||||||
|
content,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
gio::FileCreateFlags::REPLACE_DESTINATION,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.1)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_restore<T: Restorable>(&self, filters: &'static [&str]) {
|
||||||
|
let imp = self.imp();
|
||||||
|
if T::ENCRYPTABLE {
|
||||||
|
let row = adw::ExpanderRow::builder()
|
||||||
|
.title(T::title())
|
||||||
|
.subtitle(T::subtitle())
|
||||||
|
.show_enable_switch(false)
|
||||||
|
.enable_expansion(true)
|
||||||
|
.use_underline(true)
|
||||||
|
.build();
|
||||||
|
let key_entry = adw::PasswordEntryRow::builder()
|
||||||
|
.title(gettext("Key / Passphrase"))
|
||||||
|
.build();
|
||||||
|
row.add_row(&key_entry);
|
||||||
|
|
||||||
|
imp.key_entries
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(format!("restore.{}", T::IDENTIFIER), key_entry);
|
||||||
|
|
||||||
|
let button_row = adw::ActionRow::new();
|
||||||
|
let key_button = gtk::Button::builder()
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.halign(gtk::Align::End)
|
||||||
|
.label(gettext("Select File"))
|
||||||
|
.action_name(format!("restore.{}", T::IDENTIFIER))
|
||||||
|
.build();
|
||||||
|
button_row.add_suffix(&key_button);
|
||||||
|
row.add_row(&button_row);
|
||||||
|
imp.restore_group.add(&row);
|
||||||
|
} else if T::SCANNABLE {
|
||||||
|
let menu_button = gtk::MenuButton::builder()
|
||||||
|
.css_classes(vec!["flat".to_string()])
|
||||||
|
.halign(gtk::Align::Fill)
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.icon_name("qrscanner-symbolic")
|
||||||
|
.tooltip_text(gettext("Scan QR Code"))
|
||||||
|
.menu_model(&{
|
||||||
|
let menu = gio::Menu::new();
|
||||||
|
|
||||||
|
menu.append(
|
||||||
|
Some(&gettext("_Camera")),
|
||||||
|
Some(&format!("restore.{}.camera", T::IDENTIFIER)),
|
||||||
|
);
|
||||||
|
menu.append(
|
||||||
|
Some(&gettext("_Screenshot")),
|
||||||
|
Some(&format!("restore.{}.screenshot", T::IDENTIFIER)),
|
||||||
|
);
|
||||||
|
|
||||||
|
menu.append(
|
||||||
|
Some(&gettext("_QR Code Image")),
|
||||||
|
Some(&format!("restore.{}.file", T::IDENTIFIER)),
|
||||||
|
);
|
||||||
|
|
||||||
|
menu
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let row = adw::ActionRow::builder()
|
||||||
|
.title(T::title())
|
||||||
|
.subtitle(T::subtitle())
|
||||||
|
.activatable(true)
|
||||||
|
.activatable_widget(&menu_button)
|
||||||
|
.use_underline(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
row.add_suffix(&menu_button);
|
||||||
|
|
||||||
|
imp.restore_group.add(&row);
|
||||||
|
} else {
|
||||||
|
let row = adw::ActionRow::builder()
|
||||||
|
.title(T::title())
|
||||||
|
.subtitle(T::subtitle())
|
||||||
|
.activatable(true)
|
||||||
|
.use_underline(true)
|
||||||
|
.action_name(format!("restore.{}", T::IDENTIFIER))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
imp.restore_group.add(&row);
|
||||||
|
}
|
||||||
|
if T::SCANNABLE {
|
||||||
|
let camera_action = gio::ActionEntry::builder(&format!("{}.camera", T::IDENTIFIER))
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak(rename_to = win)]
|
||||||
|
self,
|
||||||
|
move |_, _, _| {
|
||||||
|
win.imp().actions.activate_action("show_camera_page", None);
|
||||||
|
spawn(clone!(
|
||||||
|
#[weak]
|
||||||
|
win,
|
||||||
|
async move {
|
||||||
|
if let Err(err) = win.restore_from_camera::<T, T::Item>().await {
|
||||||
|
tracing::error!("Failed to restore from camera {err}");
|
||||||
|
win.add_toast(adw::Toast::new(&gettext(
|
||||||
|
"Failed to restore from camera",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
let screenshot_action =
|
||||||
|
gio::ActionEntry::builder(&format!("{}.screenshot", T::IDENTIFIER))
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak(rename_to = win)]
|
||||||
|
self,
|
||||||
|
move |_, _, _| {
|
||||||
|
spawn(clone!(
|
||||||
|
#[weak]
|
||||||
|
win,
|
||||||
|
async move {
|
||||||
|
if let Err(err) =
|
||||||
|
win.restore_from_screenshot::<T, T::Item>().await
|
||||||
|
{
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to restore from a screenshot {err}"
|
||||||
|
);
|
||||||
|
win.add_toast(adw::Toast::new(&gettext(
|
||||||
|
"Failed to restore from a screenshot",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
let file_action = gio::ActionEntry::builder(&format!("{}.file", T::IDENTIFIER))
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak(rename_to = win)]
|
||||||
|
self,
|
||||||
|
move |_, _, _| {
|
||||||
|
spawn(clone!(
|
||||||
|
#[weak]
|
||||||
|
win,
|
||||||
|
async move {
|
||||||
|
if let Err(err) = win.restore_from_image::<T, T::Item>().await {
|
||||||
|
tracing::error!("Failed to restore from an image {err}");
|
||||||
|
win.add_toast(adw::Toast::new(&gettext(
|
||||||
|
"Failed to restore from an image",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
imp.restore_actions
|
||||||
|
.add_action_entries([camera_action, file_action, screenshot_action]);
|
||||||
|
} else {
|
||||||
|
let action = gio::ActionEntry::builder(T::IDENTIFIER)
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak(rename_to = win)]
|
||||||
|
self,
|
||||||
|
move |_, _, _| {
|
||||||
|
spawn(clone!(
|
||||||
|
#[weak]
|
||||||
|
win,
|
||||||
|
async move {
|
||||||
|
if let Err(err) = win.restore_from_file::<T, T::Item>(filters).await
|
||||||
|
{
|
||||||
|
tracing::error!("Failed to restore from a file {err}");
|
||||||
|
win.add_toast(adw::Toast::new(&gettext(
|
||||||
|
"Failed to restore from a file",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
imp.restore_actions.add_action_entries([action]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async fn restore_from_file<T: Restorable<Item = Q>, Q: RestorableItem>(
|
||||||
|
&self,
|
||||||
|
filters: &'static [&str],
|
||||||
|
) -> Result<()> {
|
||||||
|
let file = self.select_file(filters, Operation::Restore).await?;
|
||||||
|
let key = T::ENCRYPTABLE
|
||||||
|
.then(|| self.encryption_key(Operation::Restore, T::IDENTIFIER))
|
||||||
|
.flatten();
|
||||||
|
let content = file.load_contents_future().await?;
|
||||||
|
let items = T::restore_from_data(&content.0, key.as_deref())?;
|
||||||
|
self.restore_items::<T, T::Item>(items);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_from_camera<T: Restorable<Item = Q>, Q: RestorableItem>(&self) -> Result<()> {
|
||||||
|
let code = self.imp().camera_page.scan_from_camera().await?;
|
||||||
|
let items = T::restore_from_data(code.as_bytes(), None)?;
|
||||||
|
self.restore_items::<T, T::Item>(items);
|
||||||
|
self.imp().actions.activate_action("close_page", None);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_from_screenshot<T: Restorable<Item = Q>, Q: RestorableItem>(
|
||||||
|
&self,
|
||||||
|
) -> Result<()> {
|
||||||
|
let code = self.imp().camera_page.scan_from_screenshot().await?;
|
||||||
|
let items = T::restore_from_data(code.as_bytes(), None)?;
|
||||||
|
self.restore_items::<T, T::Item>(items);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_from_image<T: Restorable<Item = Q>, Q: RestorableItem>(&self) -> Result<()> {
|
||||||
|
let window = 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 dialog = gtk::FileDialog::builder()
|
||||||
|
.modal(true)
|
||||||
|
.filters(&model)
|
||||||
|
.title(gettext("Select QR Code"))
|
||||||
|
.build();
|
||||||
|
let file = dialog.open_future(Some(&window)).await?;
|
||||||
|
let (data, _) = file.load_contents_future().await?;
|
||||||
|
let code = screenshot::scan(&data)?;
|
||||||
|
let items = T::restore_from_data(code.as_bytes(), None)?;
|
||||||
|
self.restore_items::<T, T::Item>(items);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encryption_key(&self, mode: Operation, identifier: &str) -> Option<glib::GString> {
|
||||||
|
let identifier = match mode {
|
||||||
|
Operation::Backup => format!("backup.{identifier}",),
|
||||||
|
Operation::Restore => format!("restore.{identifier}"),
|
||||||
|
};
|
||||||
|
self.imp()
|
||||||
|
.key_entries
|
||||||
|
.borrow()
|
||||||
|
.get(&identifier)
|
||||||
|
.map(|entry| entry.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_items<T: Restorable<Item = Q>, Q: RestorableItem>(&self, items: Vec<Q>) {
|
||||||
|
let model = self.model();
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(move |item| item.restore(&model))
|
||||||
|
.for_each(|item| {
|
||||||
|
if let Err(err) = item {
|
||||||
|
tracing::warn!("Failed to restore item {}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.emit_by_name::<()>("restore-completed", &[]);
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn select_file(
|
||||||
|
&self,
|
||||||
|
filters: &'static [&str],
|
||||||
|
operation: Operation,
|
||||||
|
) -> Result<gio::File, glib::Error> {
|
||||||
|
let filters_model = gio::ListStore::new::<gtk::FileFilter>();
|
||||||
|
let window = self.root().and_downcast::<gtk::Window>().unwrap();
|
||||||
|
filters.iter().for_each(|f| {
|
||||||
|
let filter = gtk::FileFilter::new();
|
||||||
|
filter.add_mime_type(f);
|
||||||
|
filter.set_name(Some(f));
|
||||||
|
filters_model.append(&filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
match operation {
|
||||||
|
Operation::Backup => {
|
||||||
|
let dialog = gtk::FileDialog::builder()
|
||||||
|
.modal(true)
|
||||||
|
.filters(&filters_model)
|
||||||
|
.title(gettext("Backup"))
|
||||||
|
.build();
|
||||||
|
dialog.save_future(Some(&window)).await
|
||||||
|
}
|
||||||
|
Operation::Restore => {
|
||||||
|
let dialog = gtk::FileDialog::builder()
|
||||||
|
.modal(true)
|
||||||
|
.filters(&filters_model)
|
||||||
|
.title(gettext("Restore"))
|
||||||
|
.build();
|
||||||
|
dialog.open_future(Some(&window)).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_actions(&self) {
|
||||||
|
let imp = self.imp();
|
||||||
|
|
||||||
|
let show_camera_page = gio::ActionEntry::builder("show_camera_page")
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak(rename_to = win)]
|
||||||
|
self,
|
||||||
|
move |_, _, _| {
|
||||||
|
win.push_subpage(&win.imp().camera_page);
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let close_page = gio::ActionEntry::builder("close_page")
|
||||||
|
.activate(clone!(
|
||||||
|
#[weak(rename_to = win)]
|
||||||
|
self,
|
||||||
|
move |_, _, _| {
|
||||||
|
win.pop_subpage();
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
imp.actions
|
||||||
|
.add_action_entries([show_camera_page, close_page]);
|
||||||
|
|
||||||
|
self.insert_action_group("backup", Some(&imp.backup_actions));
|
||||||
|
self.insert_action_group("restore", Some(&imp.restore_actions));
|
||||||
|
self.insert_action_group("camera-page", Some(&imp.actions));
|
||||||
|
}
|
||||||
|
}
|
4
src/widgets/backup/mod.rs
Normal file
4
src/widgets/backup/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
mod camera_page;
|
||||||
|
mod dialog;
|
||||||
|
|
||||||
|
pub use dialog::BackupDialog;
|
|
@ -1,4 +1,5 @@
|
||||||
mod accounts;
|
mod accounts;
|
||||||
|
mod backup;
|
||||||
mod camera;
|
mod camera;
|
||||||
mod camera_row;
|
mod camera_row;
|
||||||
mod error_revealer;
|
mod error_revealer;
|
||||||
|
@ -11,6 +12,7 @@ mod window;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
accounts::AccountAddDialog,
|
accounts::AccountAddDialog,
|
||||||
|
backup::BackupDialog,
|
||||||
camera::{screenshot, Camera},
|
camera::{screenshot, Camera},
|
||||||
camera_row::CameraRow,
|
camera_row::CameraRow,
|
||||||
error_revealer::ErrorRevealer,
|
error_revealer::ErrorRevealer,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
mod camera_page;
|
|
||||||
mod password_page;
|
mod password_page;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,17 @@
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use anyhow::Result;
|
|
||||||
use gettextrs::gettext;
|
|
||||||
use gtk::{
|
use gtk::{
|
||||||
gio,
|
gio,
|
||||||
glib::{self, clone},
|
glib::{self, clone},
|
||||||
subclass::prelude::*,
|
subclass::prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{camera_page::CameraPage, password_page::PasswordPage};
|
use super::password_page::PasswordPage;
|
||||||
use crate::{
|
use crate::models::SETTINGS;
|
||||||
backup::{
|
|
||||||
Aegis, AndOTP, Backupable, Bitwarden, FreeOTP, FreeOTPJSON, Google, LegacyAuthenticator,
|
|
||||||
Operation, RaivoOTP, Restorable, RestorableItem,
|
|
||||||
},
|
|
||||||
models::{ProvidersModel, SETTINGS},
|
|
||||||
utils::spawn,
|
|
||||||
widgets::screenshot,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use std::{
|
use std::cell::Cell;
|
||||||
cell::{Cell, OnceCell, RefCell},
|
|
||||||
collections::HashMap,
|
|
||||||
sync::LazyLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use glib::subclass::Signal;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -34,19 +19,10 @@ mod imp {
|
||||||
#[properties(wrapper_type = super::PreferencesWindow)]
|
#[properties(wrapper_type = super::PreferencesWindow)]
|
||||||
#[template(resource = "/com/belmoussaoui/Authenticator/preferences.ui")]
|
#[template(resource = "/com/belmoussaoui/Authenticator/preferences.ui")]
|
||||||
pub struct PreferencesWindow {
|
pub struct PreferencesWindow {
|
||||||
#[property(get, set, construct_only)]
|
|
||||||
pub model: OnceCell<ProvidersModel>,
|
|
||||||
#[property(get, set, construct)]
|
#[property(get, set, construct)]
|
||||||
pub has_set_password: Cell<bool>,
|
pub has_set_password: Cell<bool>,
|
||||||
pub actions: gio::SimpleActionGroup,
|
pub actions: gio::SimpleActionGroup,
|
||||||
pub backup_actions: gio::SimpleActionGroup,
|
|
||||||
pub restore_actions: gio::SimpleActionGroup,
|
|
||||||
pub camera_page: CameraPage,
|
|
||||||
pub password_page: PasswordPage,
|
pub password_page: PasswordPage,
|
||||||
#[template_child]
|
|
||||||
pub backup_group: TemplateChild<adw::PreferencesGroup>,
|
|
||||||
#[template_child]
|
|
||||||
pub restore_group: TemplateChild<adw::PreferencesGroup>,
|
|
||||||
#[template_child(id = "auto_lock_switch")]
|
#[template_child(id = "auto_lock_switch")]
|
||||||
pub auto_lock: TemplateChild<adw::SwitchRow>,
|
pub auto_lock: TemplateChild<adw::SwitchRow>,
|
||||||
#[template_child(id = "download_favicons_switch")]
|
#[template_child(id = "download_favicons_switch")]
|
||||||
|
@ -55,7 +31,6 @@ mod imp {
|
||||||
pub download_favicons_metered: TemplateChild<adw::SwitchRow>,
|
pub download_favicons_metered: TemplateChild<adw::SwitchRow>,
|
||||||
#[template_child(id = "lock_timeout_spin_btn")]
|
#[template_child(id = "lock_timeout_spin_btn")]
|
||||||
pub lock_timeout: TemplateChild<adw::SpinRow>,
|
pub lock_timeout: TemplateChild<adw::SpinRow>,
|
||||||
pub key_entries: RefCell<HashMap<String, adw::PasswordEntryRow>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
|
@ -69,19 +44,12 @@ mod imp {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
has_set_password: Cell::default(), // Synced from the application
|
has_set_password: Cell::default(), // Synced from the application
|
||||||
camera_page: CameraPage::new(&actions),
|
|
||||||
password_page: PasswordPage::new(&actions),
|
password_page: PasswordPage::new(&actions),
|
||||||
actions,
|
actions,
|
||||||
model: OnceCell::default(),
|
|
||||||
backup_actions: gio::SimpleActionGroup::new(),
|
|
||||||
restore_actions: gio::SimpleActionGroup::new(),
|
|
||||||
auto_lock: TemplateChild::default(),
|
auto_lock: TemplateChild::default(),
|
||||||
download_favicons: TemplateChild::default(),
|
download_favicons: TemplateChild::default(),
|
||||||
download_favicons_metered: TemplateChild::default(),
|
download_favicons_metered: TemplateChild::default(),
|
||||||
lock_timeout: TemplateChild::default(),
|
lock_timeout: TemplateChild::default(),
|
||||||
backup_group: TemplateChild::default(),
|
|
||||||
restore_group: TemplateChild::default(),
|
|
||||||
key_entries: RefCell::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,12 +64,6 @@ mod imp {
|
||||||
|
|
||||||
#[glib::derived_properties]
|
#[glib::derived_properties]
|
||||||
impl ObjectImpl for PreferencesWindow {
|
impl ObjectImpl for PreferencesWindow {
|
||||||
fn signals() -> &'static [Signal] {
|
|
||||||
static SIGNALS: LazyLock<Vec<Signal>> =
|
|
||||||
LazyLock::new(|| vec![Signal::builder("restore-completed").action().build()]);
|
|
||||||
SIGNALS.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn constructed(&self) {
|
fn constructed(&self) {
|
||||||
self.parent_constructed();
|
self.parent_constructed();
|
||||||
let obj = self.obj();
|
let obj = self.obj();
|
||||||
|
@ -121,28 +83,8 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreferencesWindow {
|
impl PreferencesWindow {
|
||||||
pub fn new(model: &ProvidersModel) -> Self {
|
pub fn new() -> Self {
|
||||||
glib::Object::builder().property("model", model).build()
|
glib::Object::new()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect_restore_completed<F>(&self, callback: F) -> glib::SignalHandlerId
|
|
||||||
where
|
|
||||||
F: Fn(&Self) + 'static,
|
|
||||||
{
|
|
||||||
self.connect_local(
|
|
||||||
"restore-completed",
|
|
||||||
false,
|
|
||||||
clone!(
|
|
||||||
#[weak(rename_to = win)]
|
|
||||||
self,
|
|
||||||
#[upgrade_or]
|
|
||||||
None,
|
|
||||||
move |_| {
|
|
||||||
callback(&win);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_widget(&self) {
|
fn setup_widget(&self) {
|
||||||
|
@ -164,407 +106,11 @@ impl PreferencesWindow {
|
||||||
.sync_create()
|
.sync_create()
|
||||||
.bidirectional()
|
.bidirectional()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// FreeOTP is first in all of these lists, since its the way to backup
|
|
||||||
// Authenticator for use with Authenticator. Others are sorted
|
|
||||||
// alphabetically.
|
|
||||||
|
|
||||||
self.register_backup::<FreeOTP>(&["text/plain"]);
|
|
||||||
self.register_backup::<Aegis>(&["application/json"]);
|
|
||||||
self.register_backup::<AndOTP>(&["application/json"]);
|
|
||||||
|
|
||||||
self.register_restore::<FreeOTP>(&["text/plain"]);
|
|
||||||
self.register_restore::<FreeOTPJSON>(&["application/json"]);
|
|
||||||
self.register_restore::<Aegis>(&["application/json"]);
|
|
||||||
self.register_restore::<AndOTP>(&["application/json"]);
|
|
||||||
self.register_restore::<Bitwarden>(&["application/json"]);
|
|
||||||
self.register_restore::<Google>(&[]);
|
|
||||||
self.register_restore::<LegacyAuthenticator>(&["application/json"]);
|
|
||||||
self.register_restore::<RaivoOTP>(&["application/zip"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_backup<T: Backupable>(&self, filters: &'static [&str]) {
|
|
||||||
let imp = self.imp();
|
|
||||||
if T::ENCRYPTABLE {
|
|
||||||
let row = adw::ExpanderRow::builder()
|
|
||||||
.title(T::title())
|
|
||||||
.subtitle(T::subtitle())
|
|
||||||
.show_enable_switch(false)
|
|
||||||
.enable_expansion(true)
|
|
||||||
.use_underline(true)
|
|
||||||
.build();
|
|
||||||
let key_entry = adw::PasswordEntryRow::builder()
|
|
||||||
.title(gettext("Key / Passphrase"))
|
|
||||||
.build();
|
|
||||||
row.add_row(&key_entry);
|
|
||||||
imp.key_entries
|
|
||||||
.borrow_mut()
|
|
||||||
.insert(format!("backup.{}", T::IDENTIFIER), key_entry);
|
|
||||||
|
|
||||||
let button_row = adw::ActionRow::new();
|
|
||||||
let key_button = gtk::Button::builder()
|
|
||||||
.valign(gtk::Align::Center)
|
|
||||||
.halign(gtk::Align::End)
|
|
||||||
.label(gettext("Select File"))
|
|
||||||
.action_name(format!("backup.{}", T::IDENTIFIER))
|
|
||||||
.build();
|
|
||||||
button_row.add_suffix(&key_button);
|
|
||||||
row.add_row(&button_row);
|
|
||||||
|
|
||||||
imp.backup_group.add(&row);
|
|
||||||
} else {
|
|
||||||
let row = adw::ActionRow::builder()
|
|
||||||
.title(T::title())
|
|
||||||
.subtitle(T::subtitle())
|
|
||||||
.activatable(true)
|
|
||||||
.use_underline(true)
|
|
||||||
.action_name(format!("backup.{}", T::IDENTIFIER))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
imp.backup_group.add(&row);
|
|
||||||
}
|
|
||||||
|
|
||||||
let action = gio::ActionEntry::builder(T::IDENTIFIER)
|
|
||||||
.activate(clone!(
|
|
||||||
#[weak(rename_to = win)]
|
|
||||||
self,
|
|
||||||
move |_, _, _| {
|
|
||||||
spawn(clone!(
|
|
||||||
#[weak]
|
|
||||||
win,
|
|
||||||
async move {
|
|
||||||
if let Err(err) = win.backup_into_file::<T>(filters).await {
|
|
||||||
tracing::error!("Failed to backup into a file {err}");
|
|
||||||
win.add_toast(adw::Toast::new(&gettext(
|
|
||||||
"Failed to create a backup",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
imp.backup_actions.add_action_entries([action]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn backup_into_file<T: Backupable>(&self, filters: &'static [&str]) -> Result<()> {
|
|
||||||
let model = self.model();
|
|
||||||
let file = self.select_file(filters, Operation::Backup).await?;
|
|
||||||
let key = T::ENCRYPTABLE
|
|
||||||
.then(|| self.encryption_key(Operation::Backup, T::IDENTIFIER))
|
|
||||||
.flatten();
|
|
||||||
let content = T::backup(&model, key.as_deref())?;
|
|
||||||
file.replace_contents_future(
|
|
||||||
content,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
gio::FileCreateFlags::REPLACE_DESTINATION,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.1)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_restore<T: Restorable>(&self, filters: &'static [&str]) {
|
|
||||||
let imp = self.imp();
|
|
||||||
if T::ENCRYPTABLE {
|
|
||||||
let row = adw::ExpanderRow::builder()
|
|
||||||
.title(T::title())
|
|
||||||
.subtitle(T::subtitle())
|
|
||||||
.show_enable_switch(false)
|
|
||||||
.enable_expansion(true)
|
|
||||||
.use_underline(true)
|
|
||||||
.build();
|
|
||||||
let key_entry = adw::PasswordEntryRow::builder()
|
|
||||||
.title(gettext("Key / Passphrase"))
|
|
||||||
.build();
|
|
||||||
row.add_row(&key_entry);
|
|
||||||
|
|
||||||
imp.key_entries
|
|
||||||
.borrow_mut()
|
|
||||||
.insert(format!("restore.{}", T::IDENTIFIER), key_entry);
|
|
||||||
|
|
||||||
let button_row = adw::ActionRow::new();
|
|
||||||
let key_button = gtk::Button::builder()
|
|
||||||
.valign(gtk::Align::Center)
|
|
||||||
.halign(gtk::Align::End)
|
|
||||||
.label(gettext("Select File"))
|
|
||||||
.action_name(format!("restore.{}", T::IDENTIFIER))
|
|
||||||
.build();
|
|
||||||
button_row.add_suffix(&key_button);
|
|
||||||
row.add_row(&button_row);
|
|
||||||
imp.restore_group.add(&row);
|
|
||||||
} else if T::SCANNABLE {
|
|
||||||
let menu_button = gtk::MenuButton::builder()
|
|
||||||
.css_classes(vec!["flat".to_string()])
|
|
||||||
.halign(gtk::Align::Fill)
|
|
||||||
.valign(gtk::Align::Center)
|
|
||||||
.icon_name("qrscanner-symbolic")
|
|
||||||
.tooltip_text(gettext("Scan QR Code"))
|
|
||||||
.menu_model(&{
|
|
||||||
let menu = gio::Menu::new();
|
|
||||||
|
|
||||||
menu.append(
|
|
||||||
Some(&gettext("_Camera")),
|
|
||||||
Some(&format!("restore.{}.camera", T::IDENTIFIER)),
|
|
||||||
);
|
|
||||||
menu.append(
|
|
||||||
Some(&gettext("_Screenshot")),
|
|
||||||
Some(&format!("restore.{}.screenshot", T::IDENTIFIER)),
|
|
||||||
);
|
|
||||||
|
|
||||||
menu.append(
|
|
||||||
Some(&gettext("_QR Code Image")),
|
|
||||||
Some(&format!("restore.{}.file", T::IDENTIFIER)),
|
|
||||||
);
|
|
||||||
|
|
||||||
menu
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let row = adw::ActionRow::builder()
|
|
||||||
.title(T::title())
|
|
||||||
.subtitle(T::subtitle())
|
|
||||||
.activatable(true)
|
|
||||||
.activatable_widget(&menu_button)
|
|
||||||
.use_underline(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
row.add_suffix(&menu_button);
|
|
||||||
|
|
||||||
imp.restore_group.add(&row);
|
|
||||||
} else {
|
|
||||||
let row = adw::ActionRow::builder()
|
|
||||||
.title(T::title())
|
|
||||||
.subtitle(T::subtitle())
|
|
||||||
.activatable(true)
|
|
||||||
.use_underline(true)
|
|
||||||
.action_name(format!("restore.{}", T::IDENTIFIER))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
imp.restore_group.add(&row);
|
|
||||||
}
|
|
||||||
if T::SCANNABLE {
|
|
||||||
let camera_action = gio::ActionEntry::builder(&format!("{}.camera", T::IDENTIFIER))
|
|
||||||
.activate(clone!(
|
|
||||||
#[weak(rename_to = win)]
|
|
||||||
self,
|
|
||||||
move |_, _, _| {
|
|
||||||
win.imp().actions.activate_action("show_camera_page", None);
|
|
||||||
spawn(clone!(
|
|
||||||
#[weak]
|
|
||||||
win,
|
|
||||||
async move {
|
|
||||||
if let Err(err) = win.restore_from_camera::<T, T::Item>().await {
|
|
||||||
tracing::error!("Failed to restore from camera {err}");
|
|
||||||
win.add_toast(adw::Toast::new(&gettext(
|
|
||||||
"Failed to restore from camera",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
let screenshot_action =
|
|
||||||
gio::ActionEntry::builder(&format!("{}.screenshot", T::IDENTIFIER))
|
|
||||||
.activate(clone!(
|
|
||||||
#[weak(rename_to = win)]
|
|
||||||
self,
|
|
||||||
move |_, _, _| {
|
|
||||||
spawn(clone!(
|
|
||||||
#[weak]
|
|
||||||
win,
|
|
||||||
async move {
|
|
||||||
if let Err(err) =
|
|
||||||
win.restore_from_screenshot::<T, T::Item>().await
|
|
||||||
{
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to restore from a screenshot {err}"
|
|
||||||
);
|
|
||||||
win.add_toast(adw::Toast::new(&gettext(
|
|
||||||
"Failed to restore from a screenshot",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
let file_action = gio::ActionEntry::builder(&format!("{}.file", T::IDENTIFIER))
|
|
||||||
.activate(clone!(
|
|
||||||
#[weak(rename_to = win)]
|
|
||||||
self,
|
|
||||||
move |_, _, _| {
|
|
||||||
spawn(clone!(
|
|
||||||
#[weak]
|
|
||||||
win,
|
|
||||||
async move {
|
|
||||||
if let Err(err) = win.restore_from_image::<T, T::Item>().await {
|
|
||||||
tracing::error!("Failed to restore from an image {err}");
|
|
||||||
win.add_toast(adw::Toast::new(&gettext(
|
|
||||||
"Failed to restore from an image",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
imp.restore_actions
|
|
||||||
.add_action_entries([camera_action, file_action, screenshot_action]);
|
|
||||||
} else {
|
|
||||||
let action = gio::ActionEntry::builder(T::IDENTIFIER)
|
|
||||||
.activate(clone!(
|
|
||||||
#[weak(rename_to = win)]
|
|
||||||
self,
|
|
||||||
move |_, _, _| {
|
|
||||||
spawn(clone!(
|
|
||||||
#[weak]
|
|
||||||
win,
|
|
||||||
async move {
|
|
||||||
if let Err(err) = win.restore_from_file::<T, T::Item>(filters).await
|
|
||||||
{
|
|
||||||
tracing::error!("Failed to restore from a file {err}");
|
|
||||||
win.add_toast(adw::Toast::new(&gettext(
|
|
||||||
"Failed to restore from a file",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
imp.restore_actions.add_action_entries([action]);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
async fn restore_from_file<T: Restorable<Item = Q>, Q: RestorableItem>(
|
|
||||||
&self,
|
|
||||||
filters: &'static [&str],
|
|
||||||
) -> Result<()> {
|
|
||||||
let file = self.select_file(filters, Operation::Restore).await?;
|
|
||||||
let key = T::ENCRYPTABLE
|
|
||||||
.then(|| self.encryption_key(Operation::Restore, T::IDENTIFIER))
|
|
||||||
.flatten();
|
|
||||||
let content = file.load_contents_future().await?;
|
|
||||||
let items = T::restore_from_data(&content.0, key.as_deref())?;
|
|
||||||
self.restore_items::<T, T::Item>(items);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_from_camera<T: Restorable<Item = Q>, Q: RestorableItem>(&self) -> Result<()> {
|
|
||||||
let code = self.imp().camera_page.scan_from_camera().await?;
|
|
||||||
let items = T::restore_from_data(code.as_bytes(), None)?;
|
|
||||||
self.restore_items::<T, T::Item>(items);
|
|
||||||
self.imp().actions.activate_action("close_page", None);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_from_screenshot<T: Restorable<Item = Q>, Q: RestorableItem>(
|
|
||||||
&self,
|
|
||||||
) -> Result<()> {
|
|
||||||
let code = self.imp().camera_page.scan_from_screenshot().await?;
|
|
||||||
let items = T::restore_from_data(code.as_bytes(), None)?;
|
|
||||||
self.restore_items::<T, T::Item>(items);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_from_image<T: Restorable<Item = Q>, Q: RestorableItem>(&self) -> Result<()> {
|
|
||||||
let window = 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 dialog = gtk::FileDialog::builder()
|
|
||||||
.modal(true)
|
|
||||||
.filters(&model)
|
|
||||||
.title(gettext("Select QR Code"))
|
|
||||||
.build();
|
|
||||||
let file = dialog.open_future(Some(&window)).await?;
|
|
||||||
let (data, _) = file.load_contents_future().await?;
|
|
||||||
let code = screenshot::scan(&data)?;
|
|
||||||
let items = T::restore_from_data(code.as_bytes(), None)?;
|
|
||||||
self.restore_items::<T, T::Item>(items);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encryption_key(&self, mode: Operation, identifier: &str) -> Option<glib::GString> {
|
|
||||||
let identifier = match mode {
|
|
||||||
Operation::Backup => format!("backup.{identifier}",),
|
|
||||||
Operation::Restore => format!("restore.{identifier}"),
|
|
||||||
};
|
|
||||||
self.imp()
|
|
||||||
.key_entries
|
|
||||||
.borrow()
|
|
||||||
.get(&identifier)
|
|
||||||
.map(|entry| entry.text())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_items<T: Restorable<Item = Q>, Q: RestorableItem>(&self, items: Vec<Q>) {
|
|
||||||
let model = self.model();
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.map(move |item| item.restore(&model))
|
|
||||||
.for_each(|item| {
|
|
||||||
if let Err(err) = item {
|
|
||||||
tracing::warn!("Failed to restore item {}", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.emit_by_name::<()>("restore-completed", &[]);
|
|
||||||
self.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn select_file(
|
|
||||||
&self,
|
|
||||||
filters: &'static [&str],
|
|
||||||
operation: Operation,
|
|
||||||
) -> Result<gio::File, glib::Error> {
|
|
||||||
let filters_model = gio::ListStore::new::<gtk::FileFilter>();
|
|
||||||
let window = self.root().and_downcast::<gtk::Window>().unwrap();
|
|
||||||
filters.iter().for_each(|f| {
|
|
||||||
let filter = gtk::FileFilter::new();
|
|
||||||
filter.add_mime_type(f);
|
|
||||||
filter.set_name(Some(f));
|
|
||||||
filters_model.append(&filter);
|
|
||||||
});
|
|
||||||
|
|
||||||
match operation {
|
|
||||||
Operation::Backup => {
|
|
||||||
let dialog = gtk::FileDialog::builder()
|
|
||||||
.modal(true)
|
|
||||||
.filters(&filters_model)
|
|
||||||
.title(gettext("Backup"))
|
|
||||||
.build();
|
|
||||||
dialog.save_future(Some(&window)).await
|
|
||||||
}
|
|
||||||
Operation::Restore => {
|
|
||||||
let dialog = gtk::FileDialog::builder()
|
|
||||||
.modal(true)
|
|
||||||
.filters(&filters_model)
|
|
||||||
.title(gettext("Restore"))
|
|
||||||
.build();
|
|
||||||
dialog.open_future(Some(&window)).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_actions(&self) {
|
fn setup_actions(&self) {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
|
|
||||||
let show_camera_page = gio::ActionEntry::builder("show_camera_page")
|
|
||||||
.activate(clone!(
|
|
||||||
#[weak(rename_to = win)]
|
|
||||||
self,
|
|
||||||
move |_, _, _| {
|
|
||||||
win.push_subpage(&win.imp().camera_page);
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let show_password_page = gio::ActionEntry::builder("show_password_page")
|
let show_password_page = gio::ActionEntry::builder("show_password_page")
|
||||||
.activate(clone!(
|
.activate(clone!(
|
||||||
#[weak(rename_to = win)]
|
#[weak(rename_to = win)]
|
||||||
|
@ -586,10 +132,8 @@ impl PreferencesWindow {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
imp.actions
|
imp.actions
|
||||||
.add_action_entries([show_camera_page, show_password_page, close_page]);
|
.add_action_entries([show_password_page, close_page]);
|
||||||
|
|
||||||
self.insert_action_group("preferences", Some(&imp.actions));
|
self.insert_action_group("preferences", Some(&imp.actions));
|
||||||
self.insert_action_group("backup", Some(&imp.backup_actions));
|
|
||||||
self.insert_action_group("restore", Some(&imp.restore_actions));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue