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 @@ start - 24 + 32 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 };