provider: download & display images properly

This commit is contained in:
Bilal Elmoussaoui 2020-12-06 23:45:45 +01:00
parent 68aee23222
commit 9ed6188f07
13 changed files with 311 additions and 192 deletions

View file

@ -26,6 +26,12 @@
margin-bottom: 8px;
margin-left: 2px;
}
.provider-image{
margin-bottom: 8px;
margin-bottom: 8px;
margin-right: 8px;
}
.totp-progress,
.totp-progress trough progress,
.totp-progress trough {

View file

@ -52,7 +52,6 @@
<child>
<object class="GtkScrolledWindow">
<child>
<object class="HdyClamp">
<property name="valign">center</property>
<property name="margin-top">12</property>
@ -64,33 +63,6 @@
<property name="spacing">42</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<child>
<object class="GtkStack" id="image_stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="name">image</property>
<property name="child">
<object class="GtkImage" id="image">
<property name="icon-name">image-missing</property>
<property name="pixel-size">96</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner" id="spinner">
<property name="valign">center</property>
<property name="halign">center</property>
</object>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox" id="basic_list">
<property name="selection_mode">none</property>

View file

@ -1,65 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ProviderImage" parent="GtkStack">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">start</property>
<property name="valign">center</property>
<template class="ProviderImage" parent="GtkBox">
<child>
<object class="GtkSpinner" id="provider_spinner">
</object>
</child>
<child>
<object class="GtkEventBox" id="image_eventbox">
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkOverlay">
<child>
<object class="GtkImage" id="provider_image">
<property name="pixel_size">128</property>
<object class="GtkStackPage">
<property name="name">image</property>
<property name="child">
<object class="GtkImage" id="image">
<property name="icon-name">image-missing-symbolic</property>
<property name="pixel-size">96</property>
</object>
</child>
<child type="overlay">
<object class="GtkImage" id="insert_image">
<property name="no_show_all">True</property>
<property name="halign">center</property>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner" id="spinner">
<property name="valign">center</property>
<property name="icon-name">insert-image-symbolic</property>
<style>
<class name="insert-image"/>
</style>
<property name="halign">center</property>
</object>
</child>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="not_found_box">
<property name="width_request">48</property>
<property name="height_request">48</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkEventBox">
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="halign">center</property>
<property name="valign">center</property>
<property name="icon-name">insert-image-symbolic</property>
<style>
<class name="insert-image"/>
</style>
</object>
</child>
</object>
</child>
<style>
<class name="insert-image-box"/>
</style>
</object>
</child>
<style>
<class name="provider-image" />
</style>
</template>
</interface>

View file

@ -72,33 +72,6 @@
<property name="spacing">42</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<child>
<object class="GtkStack" id="image_stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="name">image</property>
<property name="child">
<object class="GtkImage" id="image">
<property name="icon-name">image-missing</property>
<property name="pixel-size">96</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner" id="spinner">
<property name="valign">center</property>
<property name="halign">center</property>
</object>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<property name="selection_mode">none</property>

View file

@ -9,11 +9,17 @@
<property name="orientation">vertical</property>
<property name="vexpand">True</property>
<child>
<object class="GtkLabel" id="name_label">
<property name="halign">start</property>
<style>
<class name="title-2" />
</style>
<object class="GtkBox" id="header">
<property name="orientation">horizontal</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel" id="name_label">
<property name="halign">start</property>
<style>
<class name="title-2" />
</style>
</object>
</child>
</object>
</child>
<child>

View file

@ -60,6 +60,7 @@ sources = files(
'widgets/preferences/password_page.rs',
'widgets/preferences/window.rs',
'widgets/providers/all.rs',
'widgets/providers/image.rs',
'widgets/providers/list.rs',
'widgets/providers/mod.rs',
'widgets/providers/page.rs',

View file

@ -1,4 +1,5 @@
use super::algorithm::{Algorithm, HOTPAlgorithm};
use crate::diesel::ExpressionMethods;
use crate::models::{database, Account, AccountsModel, FaviconError, FaviconScrapper};
use crate::schema::providers;
use anyhow::Result;
@ -301,7 +302,6 @@ impl Provider {
digits: i32,
default_counter: i32,
) -> Result<Self> {
use crate::diesel::ExpressionMethods;
let db = database::connection();
let conn = db.get()?;
@ -454,6 +454,19 @@ impl Provider {
priv_.help_url.borrow().clone()
}
pub fn set_image_uri(&self, uri: &str) -> Result<()> {
let db = database::connection();
let conn = db.get()?;
let target = providers::table.filter(providers::columns::id.eq(self.id()));
diesel::update(target)
.set(providers::columns::image_uri.eq(uri))
.execute(&conn)?;
self.set_property("image-uri", &uri)?;
Ok(())
}
pub fn image_uri(&self) -> Option<String> {
let priv_ = ProviderPriv::from_instance(self);
priv_.image_uri.borrow().clone()

View file

@ -1,20 +1,16 @@
use crate::application::Action;
use crate::helpers::{qrcode, Keyring};
use crate::models::{Account, Algorithm, Provider, ProvidersModel};
use crate::widgets::{ProviderImage, ProviderImageSize};
use anyhow::Result;
use gio::prelude::*;
use gio::{subclass::ObjectSubclass, ActionMapExt};
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use glib::{signal::Inhibit, Receiver, Sender};
use glib::{signal::Inhibit, Sender};
use gtk::{prelude::*, CompositeTemplate};
use libhandy::ActionRowExt;
use once_cell::sync::OnceCell;
use std::cell::RefCell;
pub enum AccountAddAction {
SetIcon(gio::File),
}
mod imp {
use super::*;
@ -24,11 +20,13 @@ mod imp {
#[derive(CompositeTemplate)]
pub struct AccountAddDialog {
pub global_sender: OnceCell<Sender<Action>>,
pub sender: Sender<AccountAddAction>,
pub receiver: RefCell<Option<Receiver<AccountAddAction>>>,
pub model: OnceCell<ProvidersModel>,
pub selected_provider: OnceCell<Provider>,
pub actions: gio::SimpleActionGroup,
pub image: ProviderImage,
#[template_child(id = "main_container")]
pub main_container: TemplateChild<gtk::Box>,
#[template_child(id = "username_entry")]
pub username_entry: TemplateChild<gtk::Entry>,
@ -68,15 +66,6 @@ mod imp {
#[template_child(id = "provider_completion")]
pub provider_completion: TemplateChild<gtk::EntryCompletion>,
#[template_child(id = "image")]
pub image: TemplateChild<gtk::Image>,
#[template_child(id = "spinner")]
pub spinner: TemplateChild<gtk::Spinner>,
#[template_child(id = "image_stack")]
pub image_stack: TemplateChild<gtk::Stack>,
}
impl ObjectSubclass for AccountAddDialog {
@ -89,18 +78,15 @@ mod imp {
glib_object_subclass!();
fn new() -> Self {
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let receiver = RefCell::new(Some(r));
let actions = gio::SimpleActionGroup::new();
Self {
global_sender: OnceCell::new(),
sender,
receiver,
actions,
image: ProviderImage::new(ProviderImageSize::Large),
model: OnceCell::new(),
selected_provider: OnceCell::new(),
main_container: TemplateChild::default(),
token_entry: TemplateChild::default(),
username_entry: TemplateChild::default(),
more_list: TemplateChild::default(),
@ -114,9 +100,6 @@ mod imp {
hmac_algorithm_row: TemplateChild::default(),
counter_row: TemplateChild::default(),
period_row: TemplateChild::default(),
image: TemplateChild::default(),
spinner: TemplateChild::default(),
image_stack: TemplateChild::default(),
}
}
@ -194,8 +177,7 @@ impl AccountAddDialog {
qrcode::screenshot_area(
self.clone().upcast::<gtk::Window>(),
clone!(@weak self as dialog, @weak token_entry, @weak username_entry, @strong self_.model as model,
@strong self_.sender as sender => move |screenshot| {
clone!(@weak self as dialog, @weak token_entry, @weak username_entry, @strong self_.model as model => move |screenshot| {
if let Ok(otpauth) = qrcode::scan(&screenshot) {
token_entry.set_text(&otpauth.token);
if let Some(ref username) = otpauth.account {
@ -232,14 +214,15 @@ impl AccountAddDialog {
fn set_provider(&self, provider: Provider) {
let self_ = imp::AccountAddDialog::from_instance(self);
self_.more_list.get().show();
self_.provider_entry.get().set_text(&provider.name());
self_
.period_label
.get()
.set_text(&provider.period().to_string());
self_.image.set_provider(&provider);
self_
.algorithm_label
.get()
@ -249,6 +232,7 @@ impl AccountAddDialog {
.digits_label
.get()
.set_text(&provider.digits().to_string());
match provider.algorithm() {
Algorithm::TOTP => {
self_.hmac_algorithm_row.get().hide();
@ -269,18 +253,6 @@ impl AccountAddDialog {
if let Some(ref help_url) = provider.help_url() {
self_.provider_help_row.get().set_subtitle(Some(help_url));
}
self_.image_stack.get().set_visible_child_name("loading");
self_.spinner.get().start();
let p = provider.clone();
let sender = self_.sender.clone();
spawn!(async move {
if let Ok(file) = p.favicon().await {
send!(sender, AccountAddAction::SetIcon(file));
}
});
self_.selected_provider.set(provider);
}
@ -316,16 +288,13 @@ impl AccountAddDialog {
fn setup_widgets(&self) {
let self_ = imp::AccountAddDialog::from_instance(self);
let receiver = self_.receiver.borrow_mut().take().unwrap();
receiver.attach(
None,
clone!(@weak self as dialog => move |action| dialog.do_action(action)),
);
self_
.provider_completion
.get()
.set_model(Some(&self_.model.get().unwrap().completion_model()));
self_.main_container.get().prepend(&self_.image);
self_.provider_completion.get().connect_match_selected(
clone!(@weak self as dialog, @strong self_.model as model => move |_, store, iter| {
let provider_id = store.get_value(iter, 0). get_some::<i32>().unwrap();
@ -336,16 +305,4 @@ impl AccountAddDialog {
}),
);
}
fn do_action(&self, action: AccountAddAction) -> glib::Continue {
match action {
AccountAddAction::SetIcon(file) => {
let self_ = imp::AccountAddDialog::from_instance(self);
self_.image.get().set_from_file(file.get_path().unwrap());
self_.spinner.get().stop();
self_.image_stack.get().set_visible_child_name("image");
}
};
glib::Continue(true)
}
}

View file

@ -5,5 +5,5 @@ mod window;
pub use self::accounts::AccountAddDialog;
pub use self::preferences::PreferencesWindow;
pub use self::providers::{ProvidersDialog, ProvidersList};
pub use self::providers::{ProviderImage, ProviderImageSize, ProvidersDialog, ProvidersList};
pub use self::window::{View, Window};

View file

@ -0,0 +1,221 @@
use crate::models::Provider;
use gio::{subclass::ObjectSubclass, FileExt};
use glib::subclass::prelude::*;
use glib::{glib_object_subclass, glib_wrapper};
use glib::{Receiver, Sender};
use gtk::{prelude::*, CompositeTemplate};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
pub enum ProviderImageSize {
Small,
Large,
}
pub enum ImageAction {
Ready(gio::File),
Failed,
}
mod imp {
use super::*;
use glib::subclass;
use gtk::subclass::prelude::*;
use std::cell::RefCell;
static PROPERTIES: [subclass::Property; 1] = [subclass::Property("provider", |name| {
glib::ParamSpec::object(
name,
"provider",
"Provider",
Provider::static_type(),
glib::ParamFlags::READWRITE,
)
})];
#[derive(Debug, CompositeTemplate)]
pub struct ProviderImage {
pub sender: Sender<ImageAction>,
pub receiver: RefCell<Option<Receiver<ImageAction>>>,
#[template_child(id = "stack")]
pub stack: TemplateChild<gtk::Stack>,
#[template_child(id = "image")]
pub image: TemplateChild<gtk::Image>,
#[template_child(id = "spinner")]
pub spinner: TemplateChild<gtk::Spinner>,
pub provider: RefCell<Option<Provider>>,
}
impl ObjectSubclass for ProviderImage {
const NAME: &'static str = "ProviderImage";
type Type = super::ProviderImage;
type ParentType = gtk::Box;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib_object_subclass!();
fn new() -> Self {
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let receiver = RefCell::new(Some(r));
Self {
sender,
receiver,
stack: TemplateChild::default(),
image: TemplateChild::default(),
spinner: TemplateChild::default(),
provider: RefCell::new(None),
}
}
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/provider_image.ui");
Self::bind_template_children(klass);
klass.install_properties(&PROPERTIES);
}
}
impl ObjectImpl for ProviderImage {
fn constructed(&self, obj: &Self::Type) {
obj.init_template();
obj.setup_widgets();
self.parent_constructed(obj);
}
fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value) {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("provider", ..) => {
let provider = value
.get()
.expect("type conformity checked by `Object::set_property`");
self.provider.replace(provider);
}
_ => unimplemented!(),
}
}
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("provider", ..) => self.provider.borrow().to_value(),
_ => unimplemented!(),
}
}
}
impl WidgetImpl for ProviderImage {}
impl BoxImpl for ProviderImage {}
}
glib_wrapper! {
pub struct ProviderImage(ObjectSubclass<imp::ProviderImage>) @extends gtk::Widget, gtk::Box;
}
impl ProviderImage {
pub fn new(image_size: ProviderImageSize) -> Self {
let image = glib::Object::new(Self::static_type(), &[])
.expect("Failed to create ProviderImage")
.downcast::<ProviderImage>()
.expect("Created ProviderImage is of wrong type");
image.set_size(image_size);
image
}
pub fn set_provider(&self, provider: &Provider) {
let self_ = imp::ProviderImage::from_instance(self);
self_.stack.get().set_visible_child_name("loading");
self_.spinner.get().start();
self.set_property("provider", &provider.clone()).unwrap();
match provider.image_uri() {
Some(uri) => {
// Very dirty hack to store that we couldn't find an icon
// to avoid re-hitting the website every time we have to display it
if uri == "invalid" {
self_
.image
.get()
.set_from_icon_name(Some("image-icon-missing"));
self_.stack.get().set_visible_child_name("image");
return;
}
let file = gio::File::new_for_uri(&uri);
if !file.query_exists(gio::NONE_CANCELLABLE) {
self.fetch();
return;
}
self_.image.get().set_from_file(file.get_path().unwrap());
self_.stack.get().set_visible_child_name("image");
}
_ => {
self.fetch();
}
}
}
fn fetch(&self) {
let self_ = imp::ProviderImage::from_instance(self);
let sender = self_.sender.clone();
self_.stack.get().set_visible_child_name("loading");
self_.spinner.get().start();
let p = self.provider();
spawn!(async move {
match p.favicon().await {
Ok(file) => send!(sender, ImageAction::Ready(file)),
Err(_) => send!(sender, ImageAction::Failed),
}
});
}
pub fn set_size(&self, image_size: ProviderImageSize) {
let self_ = imp::ProviderImage::from_instance(self);
match image_size {
ProviderImageSize::Small => {
self_.image.get().set_pixel_size(48);
self.set_halign(gtk::Align::Start);
}
ProviderImageSize::Large => {
self_.image.get().set_pixel_size(96);
self.set_halign(gtk::Align::Center);
}
}
}
fn provider(&self) -> Provider {
let provider = self.get_property("provider").unwrap();
provider.get::<Provider>().unwrap().unwrap()
}
fn setup_widgets(&self) {
let self_ = imp::ProviderImage::from_instance(self);
let receiver = self_.receiver.borrow_mut().take().unwrap();
receiver.attach(
None,
clone!(@weak self as image => move |action| image.do_action(action)),
);
}
fn do_action(&self, action: ImageAction) -> glib::Continue {
let self_ = imp::ProviderImage::from_instance(self);
match action {
ImageAction::Failed => {
self_
.image
.get()
.set_from_icon_name(Some("image-missing-symbolic"));
self.provider().set_image_uri("invalid");
}
ImageAction::Ready(image) => {
self_.image.get().set_from_file(image.get_path().unwrap());
self.provider().set_image_uri(&image.get_uri());
}
}
self_.stack.get().set_visible_child_name("image");
self_.spinner.get().stop();
glib::Continue(true)
}
}

View file

@ -1,8 +1,10 @@
mod all;
mod image;
mod list;
mod page;
mod row;
pub use self::all::ProvidersDialog;
pub use self::image::{ProviderImage, ProviderImageSize};
pub use self::list::ProvidersList;
pub use self::page::{ProviderPage, ProviderPageMode};
pub use self::row::ProviderRow;

View file

@ -1,4 +1,5 @@
use crate::models::{Algorithm, HOTPAlgorithm, Provider};
use crate::widgets::{ProviderImage, ProviderImageSize};
use gio::subclass::ObjectSubclass;
use glib::subclass::prelude::*;
use glib::translate::ToGlib;
@ -18,6 +19,9 @@ mod imp {
#[derive(Debug, CompositeTemplate)]
pub struct ProviderPage {
pub image: ProviderImage,
#[template_child(id = "main_container")]
pub main_container: TemplateChild<gtk::Box>,
#[template_child(id = "name_entry")]
pub name_entry: TemplateChild<gtk::Entry>,
#[template_child(id = "period_spinbutton")]
@ -30,10 +34,6 @@ mod imp {
pub provider_website_entry: TemplateChild<gtk::Entry>,
#[template_child(id = "provider_help_entry")]
pub provider_help_entry: TemplateChild<gtk::Entry>,
#[template_child(id = "image_stack")]
pub image_stack: TemplateChild<gtk::Stack>,
#[template_child(id = "spinner")]
pub spinner: TemplateChild<gtk::Spinner>,
#[template_child(id = "algorithm_comborow")]
pub algorithm_comborow: TemplateChild<libhandy::ComboRow>,
#[template_child(id = "hmac_algorithm_comborow")]
@ -64,14 +64,14 @@ mod imp {
let hmac_algorithms_model = libhandy::EnumListModel::new(HOTPAlgorithm::static_type());
Self {
image: ProviderImage::new(ProviderImageSize::Large),
main_container: TemplateChild::default(),
name_entry: TemplateChild::default(),
period_spinbutton: TemplateChild::default(),
digits_spinbutton: TemplateChild::default(),
default_counter_spinbutton: TemplateChild::default(),
provider_website_entry: TemplateChild::default(),
provider_help_entry: TemplateChild::default(),
image_stack: TemplateChild::default(),
spinner: TemplateChild::default(),
algorithm_comborow: TemplateChild::default(),
hmac_algorithm_comborow: TemplateChild::default(),
period_row: TemplateChild::default(),
@ -127,9 +127,6 @@ impl ProviderPage {
self_.provider_help_entry.get().set_text(website);
}
self_.image_stack.get().set_visible_child_name("loading");
self_.spinner.get().start();
self_.algorithm_comborow.get().set_selected(
self_
.algorithms_model
@ -151,14 +148,7 @@ impl ProviderPage {
.hmac_algorithms_model
.find_position(provider.hmac_algorithm().to_glib()),
);
/*let sender = self.sender.clone();
spawn!(async move {
if let Ok(file) = p.favicon().await {
send!(sender, AddAccountAction::SetIcon(file));
}
});*/
self_.image.set_provider(&provider);
self_
.title
.get()
@ -172,6 +162,8 @@ impl ProviderPage {
.get()
.set_model(Some(&self_.algorithms_model));
self_.main_container.get().prepend(&self_.image);
self_
.algorithm_comborow
.get()
@ -213,8 +205,6 @@ impl ProviderPage {
self_.provider_website_entry.get().set_text("");
self_.provider_help_entry.get().set_text("");
self_.image_stack.get().set_visible_child_name("image");
self_.spinner.get().stop();
self_.algorithm_comborow.get().set_selected(0);
}
ProviderPageMode::Edit => {}

View file

@ -1,5 +1,5 @@
use crate::models::{Account, AccountSorter, Algorithm, Provider};
use crate::widgets::accounts::AccountRow;
use crate::widgets::{accounts::AccountRow, ProviderImage, ProviderImageSize};
use gio::prelude::*;
use gio::subclass::ObjectSubclass;
use glib::subclass::prelude::*;
@ -23,6 +23,7 @@ mod imp {
#[derive(CompositeTemplate)]
pub struct ProviderRow {
pub image: ProviderImage,
pub provider: RefCell<Option<Provider>>,
#[template_child(id = "name_label")]
pub name_label: TemplateChild<gtk::Label>,
@ -30,6 +31,8 @@ mod imp {
pub accounts_list: TemplateChild<gtk::ListBox>,
#[template_child(id = "progress")]
pub progress: TemplateChild<gtk::ProgressBar>,
#[template_child(id = "header")]
pub header: TemplateChild<gtk::Box>,
}
impl ObjectSubclass for ProviderRow {
@ -43,9 +46,11 @@ mod imp {
fn new() -> Self {
Self {
image: ProviderImage::new(ProviderImageSize::Small),
name_label: TemplateChild::default(),
accounts_list: TemplateChild::default(),
progress: TemplateChild::default(),
header: TemplateChild::default(),
provider: RefCell::new(None),
}
}
@ -111,6 +116,9 @@ impl ProviderRow {
fn setup_widgets(&self) {
let self_ = imp::ProviderRow::from_instance(self);
self_.header.get().prepend(&self_.image);
self_.image.set_provider(&self.provider());
let progress_bar = self_.progress.get();
if self.provider().algorithm() == Algorithm::TOTP {
progress_bar.set_fraction(1_f64);