use bevy::prelude::*;
use bevy::render::render_asset::RenderAssetUsages;
use bevy::render::texture::Image;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
const AVATAR_SIZE: u32 = 64;
#[derive(Resource, Default)]
pub struct AvatarLoader {
loading: HashMap<String, ()>,
completed: Arc<Mutex<Vec<CompletedAvatar>>>,
failed: Arc<Mutex<Vec<String>>>,
pub cache: HashMap<String, Handle<Image>>,
pub failed_urls: HashMap<String, ()>,
}
struct CompletedAvatar {
url: String,
image_data: Vec<u8>,
width: u32,
height: u32,
}
#[derive(Component)]
pub struct AvatarImage {
pub url: String,
pub loaded: bool,
pub failed: bool,
}
#[derive(Component)]
pub struct AvatarFallback {
pub url: String,
}
impl AvatarLoader {
pub fn request(&mut self, url: &str) {
if self.loading.contains_key(url)
|| self.cache.contains_key(url)
|| self.failed_urls.contains_key(url)
{
return;
}
if url.is_empty() {
return;
}
self.loading.insert(url.to_string(), ());
let url_clone = url.to_string();
let completed = Arc::clone(&self.completed);
let failed = Arc::clone(&self.failed);
thread::spawn(move || {
if let Some(avatar) = load_avatar_from_url(&url_clone) {
let mut completed_lock = completed.lock().unwrap();
completed_lock.push(avatar);
} else {
let mut failed_lock = failed.lock().unwrap();
failed_lock.push(url_clone);
}
});
}
pub fn get(&self, url: &str) -> Option<Handle<Image>> {
self.cache.get(url).cloned()
}
pub fn is_loading(&self, url: &str) -> bool {
self.loading.contains_key(url)
}
pub fn has_failed(&self, url: &str) -> bool {
self.failed_urls.contains_key(url)
}
}
fn load_avatar_from_url(url: &str) -> Option<CompletedAvatar> {
let response = reqwest::blocking::Client::new()
.get(url)
.timeout(std::time::Duration::from_secs(10))
.send()
.ok()?;
if !response.status().is_success() {
return None;
}
let bytes = response.bytes().ok()?;
let img = image::load_from_memory(&bytes).ok()?;
let resized = img.resize_exact(
AVATAR_SIZE,
AVATAR_SIZE,
image::imageops::FilterType::Lanczos3,
);
let rgba = resized.to_rgba8();
let (width, height) = rgba.dimensions();
Some(CompletedAvatar {
url: url.to_string(),
image_data: rgba.into_raw(),
width,
height,
})
}
pub fn process_avatar_loads(
mut avatar_loader: ResMut<AvatarLoader>,
mut images: ResMut<Assets<Image>>,
) {
let completed: Vec<CompletedAvatar> = {
let mut lock = avatar_loader.completed.lock().unwrap();
std::mem::take(&mut *lock)
};
let failed: Vec<String> = {
let mut lock = avatar_loader.failed.lock().unwrap();
std::mem::take(&mut *lock)
};
for avatar in completed {
avatar_loader.loading.remove(&avatar.url);
let image = Image::new(
bevy::render::render_resource::Extent3d {
width: avatar.width,
height: avatar.height,
depth_or_array_layers: 1,
},
bevy::render::render_resource::TextureDimension::D2,
avatar.image_data,
bevy::render::render_resource::TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD,
);
let handle = images.add(image);
avatar_loader.cache.insert(avatar.url, handle);
}
for url in failed {
avatar_loader.loading.remove(&url);
avatar_loader.failed_urls.insert(url, ());
}
}
pub fn update_avatar_images(
avatar_loader: Res<AvatarLoader>,
mut query: Query<(&mut UiImage, &mut AvatarImage)>,
) {
for (mut ui_image, mut avatar) in query.iter_mut() {
if avatar.loaded || avatar.failed {
continue;
}
if let Some(handle) = avatar_loader.get(&avatar.url) {
ui_image.texture = handle;
avatar.loaded = true;
} else if avatar_loader.has_failed(&avatar.url) {
avatar.failed = true;
}
}
}
pub fn show_avatar_fallbacks(
avatar_loader: Res<AvatarLoader>,
mut fallback_query: Query<(&mut Visibility, &AvatarFallback)>,
) {
for (mut visibility, fallback) in fallback_query.iter_mut() {
if avatar_loader.has_failed(&fallback.url) {
*visibility = Visibility::Visible;
}
}
}
pub fn request_avatars(
mut avatar_loader: ResMut<AvatarLoader>,
query: Query<&AvatarImage, Added<AvatarImage>>,
) {
for avatar in query.iter() {
if !avatar.url.is_empty() {
avatar_loader.request(&avatar.url);
}
}
}