diff --git a/data/resources/ui/provider_row.ui b/data/resources/ui/provider_row.ui
index 24a0e44..c6b1a96 100644
--- a/data/resources/ui/provider_row.ui
+++ b/data/resources/ui/provider_row.ui
@@ -19,7 +19,7 @@
diff --git a/src/models/favicon.rs b/src/models/favicon.rs
index 726cbc1..b3e8c8c 100644
--- a/src/models/favicon.rs
+++ b/src/models/favicon.rs
@@ -111,9 +111,8 @@ pub enum Favicon {
impl fmt::Debug for Favicon {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- Self::Data(bytes, metadata) => f
+ Self::Data(_, metadata) => f
.debug_struct("Favicon")
- .field("data", bytes)
.field("type", &metadata.type_())
.field("size", &metadata.size())
.finish(),
@@ -153,7 +152,7 @@ impl Favicon {
}
}
- pub async fn cache(&self, icon_name: &str) -> anyhow::Result {
+ pub async fn cache(&self, icon_name: &str) -> anyhow::Result<()> {
let body = match self {
Self::Data(bytes, _) => bytes.to_owned(),
Self::Url(url, _) => {
@@ -161,30 +160,22 @@ impl Favicon {
res.bytes().await?.to_vec()
}
};
+ let cache_path = FAVICONS_PATH.join(icon_name);
+ let mut dest = tokio::fs::File::create(cache_path.clone()).await?;
if self.metadata().type_() == &Type::Ico {
log::debug!("Found a .ico favicon, converting to PNG");
-
- let cache_path = FAVICONS_PATH.join(format!("{}.png", icon_name));
- let mut dest = tokio::fs::File::create(cache_path.clone()).await?;
-
if let Ok(ico) = image::load_from_memory_with_format(&body, image::ImageFormat::Ico) {
let mut cursor = std::io::Cursor::new(vec![]);
ico.write_to(&mut cursor, image::ImageOutputFormat::Png)?;
dest.write_all(cursor.get_ref()).await?;
+ return Ok(());
} else {
log::debug!("It seems to not be a .ICO favicon, fallback to PNG");
- dest.write_all(&body).await?;
};
-
- Ok(cache_path)
- } else {
- let cache_path = FAVICONS_PATH.join(icon_name);
- let mut dest = tokio::fs::File::create(cache_path.clone()).await?;
- dest.write_all(&body).await?;
-
- Ok(cache_path)
}
+ dest.write_all(&body).await?;
+ Ok(())
}
async fn size(&self) -> Option<(u32, u32)> {
@@ -267,7 +258,6 @@ impl std::fmt::Display for FaviconError {
}
}
-#[derive(Debug)]
pub struct FaviconScrapper(Vec);
impl FaviconScrapper {
@@ -315,6 +305,20 @@ impl FaviconScrapper {
self.0.is_empty()
}
+ pub async fn find_size(&self, size: u32) -> Option<&Favicon> {
+ let mut best = None;
+ for favicon in self.0.iter() {
+ if let Some(current_size) = favicon.size().await {
+ // Only store the width & assumes it has the same height here to simplify things
+ if current_size.0 == size {
+ best = Some(favicon);
+ break;
+ }
+ }
+ }
+ best
+ }
+
pub async fn find_best(&self) -> Option<&Favicon> {
let mut largest_size = 0;
let mut best = None;
@@ -502,6 +506,12 @@ impl FaviconScrapper {
}
}
+impl fmt::Debug for FaviconScrapper {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_list().entries(&self.0).finish()
+ }
+}
+
impl std::ops::Index for FaviconScrapper {
type Output = Favicon;
diff --git a/src/models/provider.rs b/src/models/provider.rs
index a02b543..4483b9f 100644
--- a/src/models/provider.rs
+++ b/src/models/provider.rs
@@ -1,13 +1,15 @@
use super::algorithm::{Algorithm, OTPMethod};
use crate::{
- models::{database, otp, Account, AccountsModel, FaviconError, FaviconScrapper},
+ models::{
+ database, otp, Account, AccountsModel, FaviconError, FaviconScrapper, Type, FAVICONS_PATH,
+ },
schema::providers,
};
use anyhow::Result;
use core::cmp::Ordering;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use glib::{Cast, StaticType, ToValue};
-use gtk::{gio, glib, prelude::*, subclass::prelude::*};
+use gtk::{gdk_pixbuf, gio, glib, prelude::*, subclass::prelude::*};
use std::{
cell::{Cell, RefCell},
str::FromStr,
@@ -351,19 +353,77 @@ impl Provider {
website: String,
name: String,
id: u32,
- ) -> Result> {
+ ) -> Result> {
let website_url = Url::parse(&website)?;
let favicon = FaviconScrapper::from_url(website_url).await?;
log::debug!("Found the following icons {:#?} for {}", favicon, name);
let icon_name = format!("{}_{}", id, name.replace(' ', "_"));
let icon_name = glib::base64_encode(icon_name.as_bytes());
+ let small_icon_name = format!("{icon_name}_32x32");
+ let large_icon_name = format!("{icon_name}_96x96");
+ // We need two sizes:
+ // - 32x32 for the accounts lists
+ // - 96x96 elsewhere
+ // either we find them in the list of available icons
+ // or we scale down the "best" one we find.
+ let mut found_small = false;
+ let mut found_large = false;
+
+ match (favicon.find_size(32).await, favicon.find_size(96).await) {
+ (Some(small), Some(large)) => {
+ if small.cache(&small_icon_name).await.is_ok()
+ && large.cache(&large_icon_name).await.is_ok()
+ {
+ return Ok(icon_name.to_string());
+ }
+ }
+ (Some(small), _) => {
+ log::debug!("Found a 32x32 variant with no 96x96 variant");
+ found_small = true;
+ small.cache(&small_icon_name).await?;
+ }
+ (_, Some(large)) => {
+ log::debug!("Found a 96x96 variant with no 32x32 variant");
+ found_large = true;
+ large.cache(&large_icon_name).await?;
+ }
+ // We found none, we fallback to getting the best icon
+ _ => (),
+ };
- log::debug!("Trying to find the highest resolution favicon");
if let Some(best_favicon) = favicon.find_best().await {
- log::debug!("Best favicon found is {:#?}", best_favicon);
- let cache_path = best_favicon.cache(&icon_name).await?;
- Ok(gio::File::for_path(cache_path))
+ log::debug!("Largest favicon found is {:#?}", best_favicon);
+ best_favicon.cache(&icon_name).await?;
+ let cache_path = FAVICONS_PATH.join(&*icon_name);
+ // Don't try to scale down svg variants
+ if best_favicon.metadata().type_() != &Type::Svg {
+ log::debug!("Creating scaled down variants for {:#?}", cache_path);
+ {
+ let pixbuf = gdk_pixbuf::Pixbuf::from_file(cache_path.clone())?;
+ if !found_small {
+ log::debug!("Creating a 32x32 variant of the favicon");
+ let small_pixbuf = pixbuf
+ .scale_simple(32, 32, gdk_pixbuf::InterpType::Bilinear)
+ .unwrap();
+
+ let mut small_cache = cache_path.clone();
+ small_cache.set_file_name(small_icon_name);
+ small_pixbuf.savev(small_cache.clone(), "png", &[])?;
+ }
+ if !found_large {
+ log::debug!("Creating a 96x96 variant of the favicon");
+ let large_pixbuf = pixbuf
+ .scale_simple(96, 96, gdk_pixbuf::InterpType::Bilinear)
+ .unwrap();
+ let mut large_cache = cache_path.clone();
+ large_cache.set_file_name(large_icon_name);
+ large_pixbuf.savev(large_cache.clone(), "png", &[])?;
+ }
+ };
+ tokio::fs::remove_file(cache_path).await?;
+ }
+ Ok(icon_name.to_string())
} else {
Err(Box::new(FaviconError::NoResults))
}
diff --git a/src/widgets/providers/image.rs b/src/widgets/providers/image.rs
index 340b806..6bf8f53 100644
--- a/src/widgets/providers/image.rs
+++ b/src/widgets/providers/image.rs
@@ -1,11 +1,11 @@
-use crate::models::Provider;
use crate::models::RUNTIME;
+use crate::models::{Provider, FAVICONS_PATH};
use glib::{clone, Receiver, Sender};
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk_macros::send;
pub enum ImageAction {
- Ready(gio::File),
+ Ready(String),
Failed,
}
@@ -79,7 +79,7 @@ mod imp {
"size",
"size",
"Image size",
- 24,
+ 32,
96,
48,
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT,
@@ -154,13 +154,19 @@ impl ProviderImage {
return;
}
- let file = gio::File::for_uri(&uri);
- if !file.query_exists(gio::Cancellable::NONE) {
+ let small_file = gio::File::for_path(&FAVICONS_PATH.join(format!("{uri}_32x32")));
+ let large_file = gio::File::for_path(&FAVICONS_PATH.join(format!("{uri}_96x96")));
+ if !small_file.query_exists(gio::Cancellable::NONE)
+ || !large_file.query_exists(gio::Cancellable::NONE)
+ {
self.fetch();
return;
}
-
- imp.image.set_from_file(file.path());
+ if imp.size.get() == 32 {
+ imp.image.set_from_file(small_file.path());
+ } else {
+ imp.image.set_from_file(large_file.path());
+ }
imp.stack.set_visible_child_name("image");
}
_ => {
@@ -181,8 +187,8 @@ impl ProviderImage {
let (sender, receiver) = futures::channel::oneshot::channel();
RUNTIME.spawn(async move {
match Provider::favicon(website, name, id).await {
- Ok(file) => {
- sender.send(Some(file)).unwrap();
+ Ok(cache_name) => {
+ sender.send(Some(cache_name)).unwrap();
}
Err(err) => {
log::error!("Failed to load favicon {}", err);
@@ -193,8 +199,8 @@ impl ProviderImage {
glib::MainContext::default().spawn_local(clone!(@weak self as this => async move {
let imp = this.imp();
let response = receiver.await.unwrap();
- if let Some(file) = response {
- send!(imp.sender.clone(), ImageAction::Ready(file));
+ if let Some(cache_name) = response {
+ send!(imp.sender.clone(), ImageAction::Ready(cache_name));
} else {
send!(imp.sender.clone(), ImageAction::Failed);
}
@@ -241,10 +247,15 @@ impl ProviderImage {
imp.image.set_from_icon_name(Some("provider-fallback"));
"invalid".to_string()
}
- ImageAction::Ready(image) => {
- imp.image.set_from_file(image.path());
- let image_uri = image.uri();
- image_uri.to_string()
+ ImageAction::Ready(cache_name) => {
+ if imp.size.get() == 32 {
+ imp.image
+ .set_from_file(Some(&FAVICONS_PATH.join(format!("{cache_name}_32x32"))));
+ } else {
+ imp.image
+ .set_from_file(Some(&FAVICONS_PATH.join(format!("{cache_name}_96x96"))));
+ }
+ cache_name
}
};
if let Some(provider) = self.provider() {
diff --git a/src/widgets/providers/page.rs b/src/widgets/providers/page.rs
index 7b95917..e6ce395 100644
--- a/src/widgets/providers/page.rs
+++ b/src/widgets/providers/page.rs
@@ -5,7 +5,7 @@ use crate::{
use adw::prelude::*;
use gettextrs::gettext;
use glib::{clone, translate::IntoGlib};
-use gtk::{gio, glib, subclass::prelude::*, CompositeTemplate};
+use gtk::{gio, gdk_pixbuf, glib, subclass::prelude::*, CompositeTemplate};
mod imp {
use super::*;
@@ -254,36 +254,26 @@ impl ProviderPage {
let image_uri = if let Some(file) = imp.selected_image.borrow().clone() {
let basename = file.basename().unwrap();
- let extension = basename
- .to_str()
- .unwrap()
- .split('.')
- .last()
- .unwrap_or("png");
+ let icon_name = glib::base64_encode(basename.to_str().unwrap().as_bytes());
+ let small_icon_name = format!("{icon_name}_32x32");
+ let large_icon_name = format!("{icon_name}_96x96");
- let icon_name = format!(
- "{}.{}",
- glib::base64_encode(basename.to_str().unwrap().as_bytes()),
- extension
- );
+ // Create a 96x96 & 32x32 variants
+ let stream = file.read(gio::Cancellable::NONE)?;
+ let pixbuf = gdk_pixbuf::Pixbuf::from_stream(&stream, gio::Cancellable::NONE)?;
+ log::debug!("Creating a 32x32 variant of the selected favicon");
+ let small_pixbuf = pixbuf
+ .scale_simple(32, 32, gdk_pixbuf::InterpType::Bilinear)
+ .unwrap();
+ small_pixbuf.savev(FAVICONS_PATH.join(small_icon_name), "png", &[])?;
- let image_dest = FAVICONS_PATH.join(icon_name.as_str());
+ log::debug!("Creating a 96x96 variant of the selected favicon");
+ let large_pixbuf = pixbuf
+ .scale_simple(96, 96, gdk_pixbuf::InterpType::Bilinear)
+ .unwrap();
+ large_pixbuf.savev(FAVICONS_PATH.join(large_icon_name), "png", &[])?;
- let dest_file = gio::File::for_path(image_dest);
- dest_file
- .create(
- gio::FileCreateFlags::REPLACE_DESTINATION,
- gio::Cancellable::NONE,
- )
- .ok();
- file.copy(
- &dest_file,
- gio::FileCopyFlags::OVERWRITE,
- gio::Cancellable::NONE,
- None,
- )?;
-
- Some(dest_file.uri().to_string())
+ Some(icon_name.to_string())
} else {
None
};