mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 00:34: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>
|
||||
|
||||
<!-- 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_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>
|
||||
|
|
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>
|
||||
</object>
|
||||
<template class="PreferencesWindow" parent="AdwPreferencesDialog">
|
||||
<property name="content-width">550</property>
|
||||
<property name="content-height">570</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesPage">
|
||||
|
@ -74,21 +73,5 @@
|
|||
</child>
|
||||
</object>
|
||||
</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>
|
||||
</interface>
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
<attribute name="label" translatable="yes">_Preferences</attribute>
|
||||
<attribute name="action">app.preferences</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Backup & Restore</attribute>
|
||||
<attribute name="action">app.show-backup-dialog</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
|
|
|
@ -4,6 +4,7 @@ data/com.belmoussaoui.Authenticator.metainfo.xml.in.in
|
|||
data/resources/ui/account_add.ui
|
||||
data/resources/ui/account_details_page.ui
|
||||
data/resources/ui/account_row.ui
|
||||
data/resources/ui/backup_dialog.ui
|
||||
data/resources/ui/camera.ui
|
||||
data/resources/ui/keyring_error_dialog.ui
|
||||
data/resources/ui/preferences.ui
|
||||
|
@ -28,8 +29,8 @@ src/models/algorithm.rs
|
|||
src/widgets/accounts/add.rs
|
||||
src/widgets/accounts/details.rs
|
||||
src/widgets/accounts/row.rs
|
||||
src/widgets/backup/dialog.rs
|
||||
src/widgets/preferences/password_page.rs
|
||||
src/widgets/preferences/window.rs
|
||||
src/widgets/providers/dialog.rs
|
||||
src/widgets/providers/page.rs
|
||||
src/widgets/window.rs
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::{
|
|||
SearchProviderAction, FAVICONS_PATH, RUNTIME, SECRET_SERVICE, SETTINGS,
|
||||
},
|
||||
utils::{spawn, spawn_tokio_blocking},
|
||||
widgets::{KeyringErrorDialog, PreferencesWindow, ProvidersDialog, Window},
|
||||
widgets::{BackupDialog, KeyringErrorDialog, PreferencesWindow, ProvidersDialog, Window},
|
||||
};
|
||||
|
||||
mod imp {
|
||||
|
@ -66,12 +66,11 @@ mod imp {
|
|||
.activate(|app: &Self::Type, _, _| app.quit())
|
||||
.build();
|
||||
|
||||
let preferences_action = gio::ActionEntry::builder("preferences")
|
||||
let show_backup_dialog_action = gio::ActionEntry::builder("show-backup-dialog")
|
||||
.activate(|app: &Self::Type, _, _| {
|
||||
let model = &app.imp().model;
|
||||
let window = app.active_window();
|
||||
let preferences = PreferencesWindow::new(model);
|
||||
preferences.set_has_set_password(app.can_be_locked());
|
||||
let preferences = BackupDialog::new(model);
|
||||
preferences.connect_restore_completed(clone!(
|
||||
#[weak]
|
||||
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!(
|
||||
#[weak]
|
||||
app,
|
||||
|
@ -146,6 +154,7 @@ mod imp {
|
|||
lock_action,
|
||||
providers_action,
|
||||
preferences_action,
|
||||
show_backup_dialog_action,
|
||||
]);
|
||||
|
||||
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 backup;
|
||||
mod camera;
|
||||
mod camera_row;
|
||||
mod error_revealer;
|
||||
|
@ -11,6 +12,7 @@ mod window;
|
|||
|
||||
pub use self::{
|
||||
accounts::AccountAddDialog,
|
||||
backup::BackupDialog,
|
||||
camera::{screenshot, Camera},
|
||||
camera_row::CameraRow,
|
||||
error_revealer::ErrorRevealer,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
mod camera_page;
|
||||
mod password_page;
|
||||
mod window;
|
||||
|
||||
|
|
|
@ -1,32 +1,17 @@
|
|||
use adw::prelude::*;
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, clone},
|
||||
subclass::prelude::*,
|
||||
};
|
||||
|
||||
use super::{camera_page::CameraPage, password_page::PasswordPage};
|
||||
use crate::{
|
||||
backup::{
|
||||
Aegis, AndOTP, Backupable, Bitwarden, FreeOTP, FreeOTPJSON, Google, LegacyAuthenticator,
|
||||
Operation, RaivoOTP, Restorable, RestorableItem,
|
||||
},
|
||||
models::{ProvidersModel, SETTINGS},
|
||||
utils::spawn,
|
||||
widgets::screenshot,
|
||||
};
|
||||
use super::password_page::PasswordPage;
|
||||
use crate::models::SETTINGS;
|
||||
|
||||
mod imp {
|
||||
use std::{
|
||||
cell::{Cell, OnceCell, RefCell},
|
||||
collections::HashMap,
|
||||
sync::LazyLock,
|
||||
};
|
||||
use std::cell::Cell;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use glib::subclass::Signal;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -34,19 +19,10 @@ mod imp {
|
|||
#[properties(wrapper_type = super::PreferencesWindow)]
|
||||
#[template(resource = "/com/belmoussaoui/Authenticator/preferences.ui")]
|
||||
pub struct PreferencesWindow {
|
||||
#[property(get, set, construct_only)]
|
||||
pub model: OnceCell<ProvidersModel>,
|
||||
#[property(get, set, construct)]
|
||||
pub has_set_password: Cell<bool>,
|
||||
pub actions: gio::SimpleActionGroup,
|
||||
pub backup_actions: gio::SimpleActionGroup,
|
||||
pub restore_actions: gio::SimpleActionGroup,
|
||||
pub camera_page: CameraPage,
|
||||
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")]
|
||||
pub auto_lock: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child(id = "download_favicons_switch")]
|
||||
|
@ -55,7 +31,6 @@ mod imp {
|
|||
pub download_favicons_metered: TemplateChild<adw::SwitchRow>,
|
||||
#[template_child(id = "lock_timeout_spin_btn")]
|
||||
pub lock_timeout: TemplateChild<adw::SpinRow>,
|
||||
pub key_entries: RefCell<HashMap<String, adw::PasswordEntryRow>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
@ -69,19 +44,12 @@ mod imp {
|
|||
|
||||
Self {
|
||||
has_set_password: Cell::default(), // Synced from the application
|
||||
camera_page: CameraPage::new(&actions),
|
||||
password_page: PasswordPage::new(&actions),
|
||||
actions,
|
||||
model: OnceCell::default(),
|
||||
backup_actions: gio::SimpleActionGroup::new(),
|
||||
restore_actions: gio::SimpleActionGroup::new(),
|
||||
auto_lock: TemplateChild::default(),
|
||||
download_favicons: TemplateChild::default(),
|
||||
download_favicons_metered: 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]
|
||||
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) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
@ -121,28 +83,8 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl PreferencesWindow {
|
||||
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
|
||||
}
|
||||
),
|
||||
)
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
fn setup_widget(&self) {
|
||||
|
@ -164,407 +106,11 @@ impl PreferencesWindow {
|
|||
.sync_create()
|
||||
.bidirectional()
|
||||
.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) {
|
||||
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")
|
||||
.activate(clone!(
|
||||
#[weak(rename_to = win)]
|
||||
|
@ -586,10 +132,8 @@ impl PreferencesWindow {
|
|||
.build();
|
||||
|
||||
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("backup", Some(&imp.backup_actions));
|
||||
self.insert_action_group("restore", Some(&imp.restore_actions));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue