use crate::common::image_utils::ImageUtils;
use crate::host::cache_level::CacheLevel;
use crate::host::error::HostApiError;
use crate::services::icon::icon_cache::IconCacheService;
use crate::services::icon_request::IconRequest;
use async_trait::async_trait;
#[async_trait]
pub trait IconExtractor: Send + Sync {
async fn extract_from_path(&self, path: &str) -> Result<Vec<u8>, HostApiError>;
async fn extract_from_url(&self, url: &str) -> Result<Vec<u8>, HostApiError>;
async fn extract_from_extension(&self, ext: &str) -> Result<Vec<u8>, HostApiError>;
fn default_app_icon_path(&self) -> &str;
fn default_web_icon_path(&self) -> &str;
fn is_network_available(&self) -> bool;
async fn extract(&self, request: &IconRequest) -> Result<Vec<u8>, HostApiError> {
match request {
IconRequest::Path(p) => self.extract_from_path(p).await,
IconRequest::Url(u) => self.extract_from_url(u).await,
IconRequest::Extension(e) => self.extract_from_extension(e).await,
IconRequest::Data(data) => decode_data_url(data),
}
}
async fn extract_and_process(&self, request: &IconRequest) -> Result<Vec<u8>, HostApiError> {
const MAX_ICON_SIZE: u32 = 128;
let data = self.extract(request).await?;
let trimmed = ImageUtils::trim_transparent_white_border(data.clone()).unwrap_or(data);
let resized = ImageUtils::resize_image(trimmed.clone(), MAX_ICON_SIZE, MAX_ICON_SIZE)
.await
.unwrap_or(trimmed);
Ok(ImageUtils::to_webp(resized.clone()).unwrap_or(resized))
}
async fn load_default_icon(&self, request: &IconRequest) -> Vec<u8> {
let default_path = match request {
IconRequest::Url(_) => self.default_web_icon_path(),
_ => self.default_app_icon_path(),
};
let png = tokio::fs::read(default_path).await.unwrap_or_default();
if png.is_empty() {
return png;
}
ImageUtils::to_webp(png.clone()).unwrap_or(png)
}
async fn get_icon(
&self,
cache: &IconCacheService,
request: &IconRequest,
level: CacheLevel,
) -> Result<Vec<u8>, HostApiError> {
let hash_key = request.get_hash_string() + ".webp";
if level != CacheLevel::SkipAll {
if level == CacheLevel::Full {
if let Some(data) = cache.get_l1(&hash_key) {
return Ok(data);
}
}
if cache.contains_l2(&hash_key) {
if let Some(data) = cache.get_l2(&hash_key).await {
if level == CacheLevel::Full {
cache.set_l1(&hash_key, data.clone());
}
return Ok(data);
}
}
}
let data = match self.extract_and_process(request).await {
Ok(d) if !d.is_empty() => d,
_ => {
return Ok(self.load_default_icon(request).await);
}
};
write_back_cache(cache, &hash_key, &data, level).await;
Ok(data)
}
async fn get_icon_and_update_cache(
&self,
cache: &IconCacheService,
request: &IconRequest,
level: CacheLevel,
) -> Result<Vec<u8>, HostApiError> {
let hash_key = request.get_hash_string() + ".webp";
let data = self.extract_and_process(request).await?;
write_back_cache(cache, &hash_key, &data, level).await;
Ok(data)
}
}
async fn write_back_cache(
cache: &IconCacheService,
hash_key: &str,
icon_data: &[u8],
level: CacheLevel,
) {
if level == CacheLevel::Full {
cache.set_l1(hash_key, icon_data.to_vec());
}
if level == CacheLevel::Full || level == CacheLevel::SkipMemory {
cache.set_l2(hash_key, icon_data.to_vec()).await;
}
}
fn decode_data_url(data: &str) -> Result<Vec<u8>, HostApiError> {
let payload = data.rsplit_once(";base64,").map(|(_, p)| p).unwrap_or(data);
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, payload).map_err(|e| {
HostApiError::IconExtractionFailed {
request: "data".to_string(),
reason: format!("data URL 解码失败: {}", e),
}
})
}