diff --git a/maxima-ui/src/bridge/game_images.rs b/maxima-ui/src/bridge/game_images.rs deleted file mode 100644 index 6b9120b..0000000 --- a/maxima-ui/src/bridge/game_images.rs +++ /dev/null @@ -1,154 +0,0 @@ -use anyhow::{Ok, Result}; -use egui::Context; -use log::{debug, error}; -use maxima::{ - core::{ - service_layer::{ServiceGame, ServiceGameImagesRequestBuilder, SERVICE_REQUEST_GAMEIMAGES}, - LockedMaxima, - }, - util::native::maxima_dir, -}; -use std::{ - fs, - sync::mpsc::Sender, -}; - -use crate::{ - bridge_thread::{InteractThreadGameUIImagesResponse, MaximaLibResponse}, - ui_image::{GameImageType, UIImage}, - GameUIImages, -}; - -async fn get_preferred_hero_image(images: &Option) -> Option { - if images.is_none() { - return None; - } - - let key_art = images.as_ref().unwrap().key_art(); - if key_art.is_none() { - return None; - } - - let key_art = key_art.as_ref().unwrap(); - - debug!("{:?}", key_art); - if let Some(img) = key_art.aspect_10x3_image() { - return Some(img.path().clone()); - } - - if let Some(img) = key_art.aspect_2x1_image() { - return Some(img.path().clone()); - } - - if let Some(img) = key_art.aspect_16x9_image() { - return Some(img.path().clone()); - } - - None -} - -async fn get_logo_image(images: &Option) -> Option { - if images.is_none() { - return None; - } - - let logo_set = images.as_ref().unwrap().primary_logo(); - if logo_set.is_none() { - return None; - } - - let largest_logo = logo_set.as_ref().unwrap().largest_image(); - if largest_logo.is_none() { - return None; - } - - Some(largest_logo.as_ref().unwrap().path().to_string()) -} - -pub async fn game_images_request( - maxima_arc: LockedMaxima, - slug: String, - channel: Sender, - ctx: &Context, -) -> Result<()> { - debug!("got request to load game images for {:?}", slug); - let game_hero = maxima_dir() - .unwrap() - .join("cache/ui/images/") - .join(&slug) - .join("hero.jpg"); - let game_logo = maxima_dir() - .unwrap() - .join("cache/ui/images/") - .join(&slug) - .join("logo.png"); - let has_hero = fs::metadata(&game_hero).is_ok(); - let has_logo = fs::metadata(&game_logo).is_ok(); - let images: Option = // TODO: make it a result - if !has_hero || !has_logo { //game hasn't been cached yet - let maxima = maxima_arc.lock().await; - maxima.service_layer() - .request(SERVICE_REQUEST_GAMEIMAGES, ServiceGameImagesRequestBuilder::default() - .should_fetch_context_image(!has_logo) - .should_fetch_backdrop_images(!has_hero) - .game_slug(slug.clone()) - .locale(maxima.locale().short_str().to_owned()) - .build()?).await? - } else { None }; - let hero_url: Option = if has_hero { - debug!("Using cached hero image for {:?}", slug); - None - } else { - get_preferred_hero_image(&images).await - }; - let logo_url: Option = if has_logo { - debug!("Using cached logo for {:?}", slug); - None - } else { - get_logo_image(&images).await - }; - - let ctx = ctx.clone(); - let is_logo = logo_url.is_some() || has_logo; - tokio::task::spawn(async move { - let hero = UIImage::load( - slug.clone(), - GameImageType::Hero, - if has_hero { None } else { hero_url }, - ctx.clone(), - ); - - let logo = UIImage::load( - slug.clone(), - GameImageType::Logo, - if has_logo { None } else { logo_url }, - ctx.clone(), - ); - - let hero = hero.await; - let logo = logo.await; - - if hero.is_ok() { - let res = MaximaLibResponse::GameUIImagesResponse(InteractThreadGameUIImagesResponse { - slug: slug.clone(), - response: Ok(GameUIImages { - logo: if is_logo { - Some(logo.expect("no logo").into()) - } else { - None - }, - hero: hero.expect("no hero").into(), - }), - }); - debug!("sending {}'s GameUIImages back to UI", &slug); - let _ = channel.send(res); - egui::Context::request_repaint(&ctx); - } else { - if !hero.is_ok() { - error!("hero image not ok"); - } - } - }); - tokio::task::yield_now().await; // LMAO - Ok(()) -} diff --git a/maxima-ui/src/bridge/get_friends.rs b/maxima-ui/src/bridge/get_friends.rs index 39af503..eb33b10 100644 --- a/maxima-ui/src/bridge/get_friends.rs +++ b/maxima-ui/src/bridge/get_friends.rs @@ -5,13 +5,13 @@ use maxima::{core::LockedMaxima, rtm::client::BasicPresence}; use std::sync::mpsc::Sender; use crate::{ - bridge_thread::{MaximaLibResponse, InteractThreadFriendListResponse}, - views::friends_view::{UIFriend, UIFriendImageWrapper}, + bridge_thread::{InteractThreadFriendListResponse, MaximaLibResponse}, ui_image::UIImageCacheLoaderCommand, views::friends_view::UIFriend }; pub async fn get_friends_request( maxima_arc: LockedMaxima, channel: Sender, + remote_provider_channel: Sender, ctx: &Context, ) -> Result<()> { debug!("recieved request to load friends"); @@ -23,14 +23,13 @@ pub async fn get_friends_request( let friends = maxima.friends(0).await?; for bitchass in friends { - + remote_provider_channel.send(UIImageCacheLoaderCommand::ProvideRemote(crate::ui_image::UIImageType::Avatar(bitchass.id().to_string()), bitchass.avatar().as_ref().unwrap().medium().path().to_string())).unwrap(); let friend_info = UIFriend { name: bitchass.display_name().to_string(), id: bitchass.id().to_string(), online: BasicPresence::Offline, game: None, game_presence: None, - avatar: UIFriendImageWrapper::Unloaded(bitchass.avatar().as_ref().unwrap().medium().path().to_string()), }; let res = MaximaLibResponse::FriendInfoResponse(InteractThreadFriendListResponse { diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 0a92358..fa73d93 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -1,43 +1,122 @@ use anyhow::{Ok, Result, bail}; use egui::Context; -use log::debug; -use maxima::core::LockedMaxima; -use std::sync::mpsc::Sender; +use log::{debug, info}; +use maxima::{core::{service_layer::{ServiceGame, ServiceGameImagesRequestBuilder, ServiceLayerClient, SERVICE_REQUEST_GAMEIMAGES}, LockedMaxima}, util::native::maxima_dir}; +use std::{fs, sync::mpsc::Sender}; use crate::{ - bridge_thread::{InteractThreadGameListResponse, MaximaLibResponse}, - GameDetailsWrapper, GameInfo, GameUIImagesWrapper, + bridge_thread::{InteractThreadGameListResponse, MaximaLibResponse}, ui_image::UIImageCacheLoaderCommand, GameDetailsWrapper, GameInfo }; +async fn get_preferred_hero_image(images: &Option) -> Option { + if images.is_none() { + return None; + } + + let key_art = images.as_ref().unwrap().key_art(); + if key_art.is_none() { + return None; + } + + let key_art = key_art.as_ref().unwrap(); + + debug!("{:?}", key_art); + if let Some(img) = key_art.aspect_10x3_image() { + return Some(img.path().clone()); + } + + if let Some(img) = key_art.aspect_2x1_image() { + return Some(img.path().clone()); + } + + if let Some(img) = key_art.aspect_16x9_image() { + return Some(img.path().clone()); + } + + None +} + +fn get_logo_image(images: &Option) -> Option { + if images.is_none() { + return None; + } + + let logo_set = images.as_ref().unwrap().primary_logo(); + if logo_set.is_none() { + return None; + } + + let largest_logo = logo_set.as_ref().unwrap().largest_image(); + if largest_logo.is_none() { + return None; + } + + Some(largest_logo.as_ref().unwrap().path().to_string()) +} + +async fn handle_images(slug: String, locale: String, has_hero: bool, has_logo: bool, channel: Sender, service_layer: ServiceLayerClient) -> Result<()> { + debug!("handling image downloads for {}", &slug); + let images = service_layer + .request(SERVICE_REQUEST_GAMEIMAGES, ServiceGameImagesRequestBuilder::default() + .should_fetch_context_image(!has_logo) + .should_fetch_backdrop_images(!has_hero) + .game_slug(slug.clone()) + .locale(locale.clone()) + .build()?).await?; + + if !has_hero { + if let Some(hero) = get_preferred_hero_image(&images).await { + channel.send(UIImageCacheLoaderCommand::ProvideRemote(crate::ui_image::UIImageType::Hero(slug.clone()), hero)).unwrap() + } + } + + if !has_logo { + if let Some(logo) = get_logo_image(&images) { + channel.send(UIImageCacheLoaderCommand::ProvideRemote(crate::ui_image::UIImageType::Logo(slug), logo))? + } else { + channel.send(UIImageCacheLoaderCommand::Stub(crate::ui_image::UIImageType::Logo(slug)))? + } + } + + Ok(()) +} + + pub async fn get_games_request( maxima_arc: LockedMaxima, channel: Sender, + channel1: Sender, ctx: &Context, ) -> Result<()> { debug!("recieved request to load games"); let mut maxima = maxima_arc.lock().await; + let service_layer = maxima.service_layer().clone(); + let locale = maxima.locale().short_str().to_owned(); let logged_in = maxima.auth_storage().lock().await.current().is_some(); if !logged_in { bail!("Ignoring request to load games, not logged in."); } + let owned_games = maxima.mut_library().games().await; + if owned_games.len() <= 0 { return Ok(()); } for game in owned_games { + info!("processing {}", &game.base_offer().slug()); let game_info = GameInfo { slug: game.base_offer().slug().to_string(), offer: game.base_offer().offer().offer_id().to_string(), name: game.name(), - images: GameUIImagesWrapper::Unloaded, details: GameDetailsWrapper::Unloaded, dlc: game.extra_offers().clone(), installed: game.base_offer().installed().await, has_cloud_saves: game.base_offer().offer().has_cloud_save(), }; + let slug = game_info.slug.clone(); let settings = crate::GameSettings { //TODO: eventually support cloud saves, the option is here for that but for now, keep it disabled in ui! cloud_saves: true, @@ -49,6 +128,30 @@ pub async fn get_games_request( settings }); channel.send(res)?; + + let game_hero = maxima_dir() + .unwrap() + .join("cache/ui/images/") + .join(&slug) + .join("hero.jpg"); + let game_logo = maxima_dir() + .unwrap() + .join("cache/ui/images/") + .join(&slug) + .join("logo.png"); + let has_hero = fs::metadata(&game_hero).is_ok(); + let has_logo = fs::metadata(&game_logo).is_ok(); + + if !has_hero || !has_logo { + //we're like 20 tasks deep i swear but this shit's gonna be real fast, trust + let slug_send = slug.clone(); + let locale_send = locale.clone(); + let channel_send = channel1.clone(); + let service_layer_send = service_layer.clone(); + tokio::task::spawn(async move { handle_images(slug_send, locale_send, has_hero, has_logo, channel_send, service_layer_send).await }); + tokio::task::yield_now().await; + + } egui::Context::request_repaint(&ctx); } diff --git a/maxima-ui/src/bridge/get_user_avatar.rs b/maxima-ui/src/bridge/get_user_avatar.rs deleted file mode 100644 index 02f4ecd..0000000 --- a/maxima-ui/src/bridge/get_user_avatar.rs +++ /dev/null @@ -1,27 +0,0 @@ -use anyhow::{Ok, Result}; -use egui::Context; -use std::sync::mpsc::Sender; - -use crate::{ - bridge_thread::{MaximaLibResponse, InteractThreadUserAvatarResponse}, - ui_image::UIImage -}; - -pub async fn get_user_avatar_request( - channel: Sender, - id: String, - url: String, - ctx: &Context, -) -> Result<()> { - let image = UIImage::load_friend(id.clone(), url, ctx.clone()).await; - let _ = channel.send(MaximaLibResponse::UserAvatarResponse(InteractThreadUserAvatarResponse { - id, - response: if image.is_err() { - Err(image.err().unwrap()) - } else { - Ok(image?.into()) - }, - })); - - Ok(()) -} \ No newline at end of file diff --git a/maxima-ui/src/bridge/mod.rs b/maxima-ui/src/bridge/mod.rs index f9ba330..629ccd6 100644 --- a/maxima-ui/src/bridge/mod.rs +++ b/maxima-ui/src/bridge/mod.rs @@ -1,7 +1,5 @@ pub mod game_details; -pub mod game_images; pub mod get_friends; pub mod get_games; -pub mod get_user_avatar; pub mod login_oauth; pub mod start_game; diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 3efe6ae..f24acff 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -1,7 +1,7 @@ use log::{debug, error, info, warn}; use std::sync::mpsc::TryRecvError; use crate::{ - bridge_thread, views::{downloads_view::QueuedDownload, friends_view::UIFriendImageWrapper}, BackendStallState, GameDetails, GameDetailsWrapper, GameUIImages, GameUIImagesWrapper, MaximaEguiApp + bridge_thread, views::downloads_view::QueuedDownload, BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp }; pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { @@ -70,49 +70,6 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { bridge_thread::MaximaLibResponse::FriendInfoResponse(res) => { app.friends.push(res.friend); } - bridge_thread::MaximaLibResponse::GameUIImagesResponse(res) => { - debug!("Got UIImages back from the interact thread"); - if res.response.is_err() { - continue; - } - - let response = res.response.unwrap(); - - for (slug, game) in &mut app.games { - if !slug.eq(&res.slug) { - continue; - } - - debug!("setting images for {:?}", game.slug); - game.images = GameUIImagesWrapper::Available(GameUIImages { - hero: response.hero.to_owned(), - logo: response.logo.to_owned(), - }); - } - } - bridge_thread::MaximaLibResponse::UserAvatarResponse(res) => { - - if res.response.is_err() { - error!("{}", res.response.err().expect("").to_string()); - continue; - } - - let response = res.response.unwrap(); - - if app.user_id.eq(&res.id) { - app.local_user_pfp = UIFriendImageWrapper::Available(response.clone()); - debug!("your own pfp"); - continue; - } - - for user in &mut app.friends { - if !user.id.eq(&res.id) { - continue; - } - debug!("Got {}'s Avatar back from the interact thread", &user.name); - user.avatar = UIFriendImageWrapper::Available(response.clone()); - } - } bridge_thread::MaximaLibResponse::InteractionThreadDiedResponse => { error!("interact thread died"); app.critical_bg_thread_crashed = true; diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index b63112a..95108dc 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -1,22 +1,19 @@ use anyhow::{Ok, Result}; use egui::Context; -use log::{info, warn}; +use log::{error, info, warn}; use std::{ - panic, path::PathBuf, sync::{ - mpsc::{Receiver, Sender}, - Arc, - }, time::{Duration, SystemTime} + panic, path::PathBuf, sync::mpsc::{Receiver, Sender}, time::{Duration, SystemTime} }; use maxima::{content::manager::{ContentManager, QueuedGameBuilder}, core::{dip::{DiPManifest, DIP_RELATIVE_PATH}, service_layer::ServicePlayer, LockedMaxima, Maxima, MaximaOptionsBuilder}, util::registry::{check_registry_validity, set_up_registry}}; use crate::{ bridge::{ game_details::game_details_request, - game_images::game_images_request, get_friends::get_friends_request, - get_games::get_games_request, get_user_avatar::get_user_avatar_request, + get_friends::get_friends_request, + get_games::get_games_request, login_oauth::login_oauth, start_game::start_game_request, - }, event_thread::{EventThread, MaximaEventRequest, MaximaEventResponse}, ui_image::UIImage, views::friends_view::UIFriend, GameDetails, GameInfo, GameSettings, GameUIImages + }, event_thread::{EventThread, MaximaEventRequest, MaximaEventResponse}, ui_image::UIImageCacheLoaderCommand, views::friends_view::UIFriend, GameDetails, GameInfo, GameSettings }; pub struct InteractThreadLoginResponse { @@ -32,21 +29,11 @@ pub struct InteractThreadFriendListResponse { pub friend: UIFriend, } -pub struct InteractThreadUserAvatarResponse { - pub id: String, - pub response: Result>, -} - pub struct InteractThreadGameDetailsResponse { pub slug: String, pub response: Result, } -pub struct InteractThreadGameUIImagesResponse { - pub slug: String, - pub response: Result, -} - pub struct InteractThreadLocateGameFailure { pub reason: anyhow::Error, pub xml_path: String, @@ -67,8 +54,6 @@ pub enum MaximaLibRequest { LoginRequestOauth, GetGamesRequest, GetFriendsRequest, - GetUserAvatarRequest(String, String), - GetGameImagesRequest(String), GetGameDetailsRequest(String), StartGameRequest(GameInfo, Option), InstallGameRequest(String, PathBuf), @@ -83,9 +68,7 @@ pub enum MaximaLibResponse { ServiceStarted, GameInfoResponse(InteractThreadGameListResponse), FriendInfoResponse(InteractThreadFriendListResponse), - UserAvatarResponse(InteractThreadUserAvatarResponse), GameDetailsResponse(InteractThreadGameDetailsResponse), - GameUIImagesResponse(InteractThreadGameUIImagesResponse), LocateGameResponse(InteractThreadLocateGameResponse), // Alerts, rather than responses: @@ -120,7 +103,8 @@ impl BridgeThread { backend_responder.send(MaximaLibResponse::DownloadQueueUpdate(current, queue)).unwrap(); } - pub fn new(ctx: &Context) -> Self { + pub fn new(ctx: &Context, remote_provider_channel: Sender) -> Self { + puffin::profile_function!(); let (backend_commander, backend_cmd_listener) = std::sync::mpsc::channel(); let (backend_responder, backend_listener) = std::sync::mpsc::channel(); @@ -131,7 +115,7 @@ impl BridgeThread { tokio::task::spawn(async move { let die_fallback_transmittter = backend_responder.clone(); //panic::set_hook(Box::new( |_| {})); - let result = BridgeThread::run(backend_cmd_listener, backend_responder, rtm_cmd_listener, rtm_responder, &context).await; + let result = BridgeThread::run(backend_cmd_listener, backend_responder, rtm_cmd_listener, rtm_responder, remote_provider_channel, &context).await; if let Err(result) = result { die_fallback_transmittter .send(MaximaLibResponse::InteractionThreadDiedResponse) @@ -150,6 +134,7 @@ impl BridgeThread { backend_responder: Sender, rtm_cmd_listener: Receiver, rtm_responder: Sender, + remote_provider_channel: Sender, ctx: &Context, ) -> Result<()> { // first things first check registry @@ -260,9 +245,10 @@ impl BridgeThread { )); backend_responder.send(lmessage)?; } - - - get_user_avatar_request(backend_responder.clone(), user.id().to_string(), user.player().as_ref().unwrap().avatar().as_ref().unwrap().medium().path().to_string(), &ctx).await?; + let res = remote_provider_channel.send(UIImageCacheLoaderCommand::ProvideRemote(crate::ui_image::UIImageType::Avatar(user.id().to_string()), user.player().as_ref().unwrap().avatar().as_ref().unwrap().medium().path().to_string())); + if let Err(err) = res { + error!("failed to send user pfp to loader: {:?}", err); + } ctx.request_repaint(); } @@ -319,40 +305,28 @@ impl BridgeThread { } match request? { - MaximaLibRequest::LoginRequestOauth | MaximaLibRequest::StartService => { info!("bro tried to log in twice") } + MaximaLibRequest::LoginRequestOauth | MaximaLibRequest::StartService => { error!("bro tried to log in twice") } MaximaLibRequest::GetGamesRequest => { let channel = backend_responder.clone(); + let channel1 = remote_provider_channel.clone(); let maxima = maxima_arc.clone(); let context = ctx.clone(); - async move { get_games_request(maxima, channel, &context).await }.await?; + tokio::task::spawn(async move { get_games_request(maxima, channel, channel1, &context).await }); + tokio::task::yield_now().await; } MaximaLibRequest::GetFriendsRequest => { let channel = backend_responder.clone(); + let channel1 = remote_provider_channel.clone(); let maxima = maxima_arc.clone(); let context = ctx.clone(); - async move { get_friends_request(maxima, channel, &context).await }.await?; - } - MaximaLibRequest::GetGameImagesRequest(slug) => { - let channel = backend_responder.clone(); - let maxima = maxima_arc.clone(); - let context = ctx.clone(); - async move { game_images_request(maxima, slug, channel, &context).await } - .await?; - } - MaximaLibRequest::GetUserAvatarRequest(id, url) => { - let channel = backend_responder.clone(); - let context = ctx.clone(); - async move { get_user_avatar_request(channel, id, url, &context).await } - .await?; + tokio::task::spawn(async move { get_friends_request(maxima, channel, channel1, &context).await }); + tokio::task::yield_now().await; } MaximaLibRequest::GetGameDetailsRequest(slug) => { let channel = backend_responder.clone(); let maxima = maxima_arc.clone(); let context = ctx.clone(); - async move { - game_details_request(maxima, slug.clone(), channel, &context).await - } - .await?; + async move { game_details_request(maxima, slug.clone(), channel, &context).await }.await?; } MaximaLibRequest::LocateGameRequest(_, path) => { #[cfg(unix)] @@ -393,6 +367,7 @@ impl BridgeThread { } MaximaLibRequest::ShutdownRequest => break 'outer Ok(()), //TODO: kill the bridge thread } + puffin::GlobalProfiler::lock().new_frame(); } } } diff --git a/maxima-ui/src/fs/image_loader.rs b/maxima-ui/src/fs/image_loader.rs deleted file mode 100644 index 58d1424..0000000 --- a/maxima-ui/src/fs/image_loader.rs +++ /dev/null @@ -1,80 +0,0 @@ -use anyhow::{bail, Result}; -use egui::ColorImage; -use egui_extras::RetainedImage; -use image::{io::Reader as ImageReader, DynamicImage}; -use log::{debug, error}; -use std::io::Read; - -pub struct ImageLoader {} - -impl ImageLoader { - pub fn load_from_fs(path: &str) -> Result { - debug!("Loading image {:?}", path); - let img = ImageReader::open(path); - if img.is_err() { - error!("Failed to open \"{}\"!", path); - // TODO: fix this - return Self::load_from_fs("./res/placeholder.png"); // probably a really shitty idea but i don't want to embed the png, or make a system to return pointers to the texture, suffer. - } - - let img_decoded = img?.with_guessed_format(); - if img_decoded.is_err() { - error!("Failed to decode \"{}\"! Trying as SVG...", path); - // this is incredibly fucking stupid - // i should've never done this, i should've found a proper method to detect things - // but here we are. if it works, it works, and i sure as hell don't want to fix it. - let mut f = std::fs::File::open(path)?; - let mut buffer = String::new(); - f.read_to_string(&mut buffer)?; - let yeah = RetainedImage::from_svg_str(format!("{:?}_Retained_Decoded", path), &buffer); - if yeah.is_err() { - bail!("Failed to read SVG from \"{}\"!", path); - } - - return Ok(yeah.unwrap()); - } - - let img_decoded = img_decoded?.decode(); - if img_decoded.is_err() { - error!("Failed to decode \"{}\" actually frfr this time", path); - } let img_decoded = img_decoded?; - - match img_decoded.color().channel_count() { - 2 => { - let img_a = DynamicImage::ImageRgba8(img_decoded.into_rgba8()); - let ci = ColorImage::from_rgba_unmultiplied( - [img_a.width() as usize, img_a.height() as usize], - img_a.as_bytes(), - ); - - Ok(RetainedImage::from_color_image( - format!("{:?}_Retained_Decoded", path), - ci, - )) - } - 3 => { - let ci = ColorImage::from_rgb( - [img_decoded.width() as usize, img_decoded.height() as usize], - img_decoded.as_bytes(), - ); - - Ok(RetainedImage::from_color_image( - format!("{:?}_Retained_Decoded", path), - ci, - )) - } - 4 => { - let ci = ColorImage::from_rgba_unmultiplied( - [img_decoded.width() as usize, img_decoded.height() as usize], - img_decoded.as_bytes(), - ); - - Ok(RetainedImage::from_color_image( - format!("{:?}_Retained_Decoded", path), - ci, - )) - } - _ => bail!("unsupported amount of channels"), - } - } -} diff --git a/maxima-ui/src/fs/mod.rs b/maxima-ui/src/fs/mod.rs deleted file mode 100644 index c54b8e5..0000000 --- a/maxima-ui/src/fs/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod image_loader; \ No newline at end of file diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index 2222a4e..6e4e8da 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -14,8 +14,8 @@ use std::collections::HashMap; use std::default::Default; use std::path::PathBuf; use std::{ops::RangeInclusive, rc::Rc, sync::Arc}; -use ui_image::UIImage; -use views::friends_view::{UIFriend, UIFriendImageWrapper}; +use ui_image::UIImageCache; +use views::friends_view::UIFriend; use eframe::egui_glow; use egui::{ @@ -23,13 +23,12 @@ use egui::{ vec2, Color32, FontData, FontDefinitions, FontFamily, Margin, Rect, Response, Rounding, Stroke, TextureId, Ui, Vec2, Visuals, }; -use egui_extras::{RetainedImage, Size, StripBuilder}; +use egui_extras::{Size, StripBuilder}; use egui_glow::glow; use bridge_thread::{BridgeThread, InteractThreadLocateGameResponse}; use app_bg_renderer::AppBgRenderer; -use fs::image_loader::ImageLoader; use game_view_bg_renderer::GameViewBgRenderer; use renderers::app_bg_renderer; use renderers::game_view_bg_renderer; @@ -48,7 +47,6 @@ use views::{ }; pub mod bridge; -mod fs; pub mod util; mod views; pub mod widgets; @@ -171,21 +169,6 @@ pub enum GameInfoTab { /// TBD pub struct GameInstalledModsInfo {} -#[derive(Clone)] -pub struct GameUIImages { - /// YOOOOO - hero: Arc, - /// The stylized logo of the game, some games don't have this! - logo: Option>, -} - -#[derive(Clone)] -pub enum GameUIImagesWrapper { - Unloaded, - Loading, - Available(GameUIImages), -} - #[derive(PartialEq, Clone)] pub struct GameDetails { /// Time (in hours/10) you have logged in the game @@ -234,8 +217,6 @@ pub struct GameInfo { offer: String, /// Display name of the game name: String, - /// Art Assets - images: GameUIImagesWrapper, /// Game info details: GameDetailsWrapper, dlc: Vec, @@ -285,12 +266,6 @@ pub struct MaximaEguiApp { user_name: String, /// Logged in user's ID user_id: String, - /// CEO OF EPIC GAMES (TOTALLY NOT THE SINGLE BIGGEST DRAG ON THE GAMES INDUSTRY) - tim_sweeney: Rc, - /// actual renderable for the user's profile picture //TODO - user_pfp_renderable: TextureId, - /// Your profile picture - local_user_pfp: UIFriendImageWrapper, /// games games: HashMap, /// selected game @@ -309,6 +284,8 @@ pub struct MaximaEguiApp { game_view_bg_renderer: Option, /// Renderer for the app's background app_bg_renderer: Option, + /// Image cache + img_cache: UIImageCache, /// Translations locale: TranslationManager, /// If a core thread has crashed and made the UI unstable @@ -448,9 +425,8 @@ impl MaximaEguiApp { let settings: FrontendSettings = if let Some(storage) = cc.storage { eframe::get_value(storage, "settings").unwrap_or(FrontendSettings::new()) } else { FrontendSettings::new() }; - - let tim_sweeney = - Rc::new(RetainedImage::from_image_bytes("Timothy Dean Sweeney", include_bytes!("../res/usericon_tmp.png")).expect("yeah")); + + let (img_cache, remote_provider_channel) = UIImageCache::new(cc.egui_ctx.clone()); Self { args, @@ -467,9 +443,6 @@ impl MaximaEguiApp { search_buffer: String::new(), friend_sel : String::new(), }, - user_pfp_renderable: (&tim_sweeney).texture_id(&cc.egui_ctx), - tim_sweeney, - local_user_pfp: UIFriendImageWrapper::Loading, user_name: "User".to_owned(), user_id: String::new(), games: HashMap::new(), @@ -482,9 +455,10 @@ impl MaximaEguiApp { modal: None, game_view_bg_renderer: GameViewBgRenderer::new(cc), app_bg_renderer: AppBgRenderer::new(cc), + img_cache, locale: TranslationManager::new(&settings.language), critical_bg_thread_crashed: false, - backend: BridgeThread::new(&cc.egui_ctx), //please don't fucking break + backend: BridgeThread::new(&cc.egui_ctx, remote_provider_channel), //please don't fucking break backend_state: BackendStallState::Starting, playing_game: None, installing_now: None, @@ -664,13 +638,10 @@ impl eframe::App for MaximaEguiApp { self.game_sel = key.clone() } } - match &self.games[&self.game_sel].images { - GameUIImagesWrapper::Unloaded | GameUIImagesWrapper::Loading => { - render.draw(ui, fullrect, fullrect.size(), TextureId::Managed(1), 0.0); - } - GameUIImagesWrapper::Available(images) => { - render.draw(ui, fullrect, images.hero.size, images.hero.renderable, how_game); - } + //TODO: background + match &self.img_cache.get(ui_image::UIImageType::Hero(self.games[&self.game_sel].slug.clone())) { + Some(tex) => render.draw(ui, fullrect, tex.size_vec2(), tex.id(), how_game), + None => { render.draw(ui, fullrect, fullrect.size(), TextureId::Managed(1), 0.0); }, } } else { render.draw(ui, fullrect, fullrect.size(), TextureId::Managed(1), 0.0); @@ -800,19 +771,12 @@ impl eframe::App for MaximaEguiApp { rtl.style_mut().spacing.item_spacing.x = 0.0; rtl.allocate_space(vec2(2.0, 2.0)); rtl.style_mut().spacing.item_spacing.x = APP_MARGIN.x; - - let avatar: TextureId = match &self.local_user_pfp { - UIFriendImageWrapper::DoNotLoad | - UIFriendImageWrapper::Unloaded(_) | - UIFriendImageWrapper::Loading => { - self.user_pfp_renderable - }, - UIFriendImageWrapper::Available(img) => { - img.renderable - }, + + let img_response = if let Some(av) = self.img_cache.get(ui_image::UIImageType::Avatar(self.user_id.clone())) { + rtl.image((av.id(), vec2(36.0, 36.0))) + } else { + rtl.image((self.img_cache.placeholder_avatar.id(), vec2(36.0, 36.0))) }; - - let img_response = rtl.image((avatar, vec2(36.0, 36.0))); let stroke = Stroke::new(2.0, { if self.playing_game.is_some() { FRIEND_INGAME_COLOR diff --git a/maxima-ui/src/ui_image.rs b/maxima-ui/src/ui_image.rs index c9b909d..8356381 100644 --- a/maxima-ui/src/ui_image.rs +++ b/maxima-ui/src/ui_image.rs @@ -1,140 +1,221 @@ -use egui::{Context, TextureId, Vec2}; -use egui_extras::RetainedImage; -use std::{fs, path::PathBuf, sync::Arc}; +use egui::{load::ImageLoader, vec2, ColorImage, Image, ImageData, TextureHandle, TextureId, TextureOptions, Vec2}; +use image::{DynamicImage, ImageResult}; +use std::{collections::HashMap, fs, path::PathBuf, sync::{mpsc::{Receiver, Sender}, Arc, Mutex}}; use tokio::fs::File; use tokio::io; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use core::slice::SlicePattern; use log::{debug, error, info}; use std::result::Result::Ok; -use crate::ImageLoader; +use image::{io::Reader as ImageReader}; use maxima::util::native::maxima_dir; -#[derive(Clone)] -pub struct UIImage { - /// Holds the actual texture data - _retained: Arc, - /// Pass to egui to render - pub renderable: TextureId, - /// width and height of the image, in pixels - pub size: Vec2, +#[derive(Clone, PartialEq, Eq, Hash, std::fmt::Debug)] +pub enum UIImageType { + Hero(String), + Logo(String), + Background(String), + Avatar(String), } -#[derive(Clone, PartialEq)] -pub enum GameImageType { - Hero, - Logo, +pub struct UIImageCache { + cache: Arc>>>, // none represents loading, lack of represents untouched + commander: Sender, + pub placeholder_avatar: TextureHandle, } -pub async fn download_image(url: String, file_name: &PathBuf) -> Result<()> { - info!("Downloading image at {:?}", &url); - let result = reqwest::get(&url).await; - if result.is_err() { - bail!("Failed to download {}! Reason: {:?}", &url, &result); - } - - let body = result?.bytes().await?; - let file = File::create(&file_name).await; - if file.is_err() { - bail!("Failed to create {:?}! Reason: {:?}", &file_name, &file); - } - - if let Err(err) = io::copy(&mut body.as_slice(), &mut file?).await { - error!("Failed to copy file! Reason: {:?}", err) - } +pub enum UIImageCacheLoaderCommand { + ProvideRemote(UIImageType, String), + Load(UIImageType), + /// Force an image to never load (like for games that don't have logos) + Stub(UIImageType), +} - debug!("Copied file!"); - Ok(()) +pub fn load_image_bytes(image_bytes: &[u8]) -> Result { + let image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?; + let size = [image.width() as _, image.height() as _]; + let image_buffer = image.to_rgba8(); + let pixels = image_buffer.as_flat_samples(); + Ok(egui::ColorImage::from_rgba_unmultiplied( + size, + pixels.as_slice(), + )) } -impl UIImage { - pub async fn load( - slug: String, - diff: GameImageType, - url: Option, - ctx: Context, - ) -> Result { - let cache_folder = maxima_dir().unwrap().join("cache/ui/images").join(&slug); - let file_name = match diff { - GameImageType::Hero => cache_folder.join("hero.jpg"), - GameImageType::Logo => cache_folder.join("logo.png"), - }; - - if !fs::metadata(&cache_folder).is_ok() { - // folder is missing - let res = fs::create_dir_all(&cache_folder); - if res.is_err() { - error!("Failed to create directory {:?}", &cache_folder); - } - } +impl UIImageCache { + pub fn new(context: egui::Context) -> (Self, Sender) { + let cache = Arc::new(Mutex::new(HashMap::new())); + let (tx, rx) = std::sync::mpsc::channel::(); + let tx1 = tx.clone(); - if fs::metadata(&file_name).is_err() { - //image hasn't been cached yet - if url.is_none() { - bail!( - "file does not exist on disk, and a URL was not provided to retrieve it from!" - ); - } - download_image(url.unwrap(), &file_name).await?; - } + let yeah = load_image_bytes(include_bytes!("../res/usericon_tmp.png")).unwrap(); + let placeholder_avatar = context.load_texture("Placeholder Avatar", yeah, TextureOptions::NEAREST); + let send_cache = cache.clone(); + tokio::task::spawn(async move { + UIImageCache::run(context, rx, send_cache).await; + }); - let fs_load = ImageLoader::load_from_fs(&file_name.to_str().unwrap()); - if fs_load.is_ok() { - let img = fs_load?; - Ok(UIImage { - renderable: img.texture_id(&ctx), - size: img.size_vec2(), - _retained: img.into(), - }) - } else { - bail!("could not load from FS") - } + (Self { cache, commander: tx, placeholder_avatar }, tx1) } - pub async fn load_friend(id: String, url: String, ctx: Context) -> Result { - let avatar_cache_folder = maxima_dir().unwrap().join("cache/avatars"); - - if !fs::metadata(&avatar_cache_folder).is_ok() { - // ideally lib should always make this but we're playing it safe if he doesn't - let res = fs::create_dir_all(&avatar_cache_folder); - if res.is_err() { - error!("Failed to create directory {:?}", &avatar_cache_folder); - } - } - let png_cache = avatar_cache_folder.join(id.clone() + "_208x208.png"); - let jpeg_cache = avatar_cache_folder.join(id.clone() + "_208x208.jpg"); + fn get_path_for_image(variant: &UIImageType) -> PathBuf { + let image_cache_root = maxima_dir().unwrap().join("cache/ui/images"); + // lib should probably create the pfp path but we'll check both just in case we're first + let avatar_cache_root = maxima_dir().unwrap().join("cache/avatars"); + match variant { + UIImageType::Hero(slug) => image_cache_root.join(&slug).join("hero.jpg"), + UIImageType::Logo(slug) => image_cache_root.join(&slug).join("logo.png"), + UIImageType::Background(slug) => image_cache_root.join(&slug).join("background.jpg"), + UIImageType::Avatar(uid) => { // avatars can be both jpeg and png + let png_cache = avatar_cache_root.join(uid.clone() + "_208x208.png"); + let jpeg_cache = avatar_cache_root.join(uid.clone() + "_208x208.jpg"); + if fs::metadata(&jpeg_cache).is_ok() { + jpeg_cache + } else { + png_cache + } + }, + } + } - if fs::metadata(&png_cache).is_err() && fs::metadata(&jpeg_cache).is_err() { - download_image( - url.clone(), - if url.ends_with("JPEG") { - &jpeg_cache + async fn load( + needle: UIImageType, + cache: Arc>>>, + remotes: HashMap, + context: egui::Context) -> Result<()> { + let path = UIImageCache::get_path_for_image(&needle); + + if !path.exists() { + debug!("{:?} ({:?}) doesn't exist, downloading", &needle, &path); + if let Some(parent) = &path.parent() { + // not sure why it *wouldn't* have a parent but i'm just being safe + // i don't have a way to catch-all the slugs atm, so this is the better solution, it's infrequent and a non-ui thread anyway + if !fs::metadata(&parent).is_ok() { + let res = fs::create_dir_all(&parent); + res.context(format!("Failed to create directory {:?}", &parent))?; + } + } + if let Some(remote) = remotes.get(&needle) { + let result = reqwest::get(remote).await; + let result = result.context(format!("Failed to download {:?}!", &remote))?; + + let file = File::create(&path).await; + let mut file = file.context(format!("Failed to create {:?}", &path))?; + + let body = result.bytes().await.unwrap(); + io::copy(&mut body.as_slice(), &mut file).await?; + + if let Ok(ci) = load_image_bytes(&body) { + cache.lock().unwrap().insert(needle, Some(context.load_texture(path.to_str().unwrap().to_string(), ci, TextureOptions::NEAREST))); + } } else { - &png_cache + debug!("has no remote or local file"); + return Ok(()); + } + } else { + let img = ImageReader::open(&path); + let img = img.context(format!("Failed to open {:?}", path))?; + + let img_with_format = img.with_guessed_format(); + let img_with_format = img_with_format.context(format!("Failed to guess format of {:?}!", path))?; + + let img_decoded = img_with_format.decode(); + let img_decoded = img_decoded.context(format!("Failed to decode {:?}!", &path))?; + + let color_image = match img_decoded.color().channel_count() { + 2 => { + let img_a = DynamicImage::ImageRgba8(img_decoded.into_rgba8()); + ColorImage::from_rgba_unmultiplied( + [img_a.width() as usize, img_a.height() as usize], + img_a.as_bytes(), + ) + } + 3 => { + ColorImage::from_rgb( + [img_decoded.width() as usize, img_decoded.height() as usize], + img_decoded.as_bytes(), + ) + } + 4 => { + ColorImage::from_rgba_unmultiplied( + [img_decoded.width() as usize, img_decoded.height() as usize], + img_decoded.as_bytes(), + ) + } + _ => { + bail!("unsupported amount of channels in {:?}!", path); + }, + }; + + cache.lock().unwrap().insert(needle, Some(context.load_texture(path.to_str().unwrap().to_string(), color_image, TextureOptions::NEAREST))); + } + context.request_repaint(); + Ok(()) + } + + async fn run(context: egui::Context, + commander: Receiver, + cache: Arc>>>) { + + let mut remotes: HashMap = HashMap::new(); + + 'outer: loop { + match commander.try_recv() { + Err(error) => if error == std::sync::mpsc::TryRecvError::Disconnected { break 'outer }, + Ok(request) => match request { + UIImageCacheLoaderCommand::ProvideRemote(needle, target) => { + debug!("remote provided for {:?}", &needle); + // undoes a race condition, ui's fast so it can get to the images before the backend can + let mut cache = cache.lock().unwrap(); + if let Some(test_none) = cache.get(&needle) { + if test_none.is_none() { cache.remove(&needle); } + } + remotes.insert(needle, target); + }, + UIImageCacheLoaderCommand::Load(needle) => { // this might cause some slowdown, whoops! + let ctx_send = context.clone(); + let remotes_send = remotes.clone(); + let cache_send = cache.clone(); + + tokio::task::spawn(async move { + match UIImageCache::load(needle.clone(), cache_send, remotes_send, ctx_send).await { + Ok(_) => { + debug!("finished async load of {:?}", &needle); + }, + Err(err) => { + error!("async load of {:?} failed: {:?}", &needle, err); + }, + } + }); + tokio::task::yield_now().await; + }, + UIImageCacheLoaderCommand::Stub(needle) => { + cache.lock().unwrap().insert(needle, None); + }, }, - ) - .await? + } } + info!("Shutting down image loader thread"); + } - let file_name = if fs::metadata(&jpeg_cache).is_ok() { - jpeg_cache - } else { - png_cache - }; - - let fs_load = ImageLoader::load_from_fs(&file_name.to_str().unwrap()); - if fs_load.is_ok() { - let img = fs_load?; - return Ok(UIImage { - renderable: img.texture_id(&ctx), - size: img.size_vec2(), - _retained: img.into(), - }); + pub fn provide_remote(&self, needle: UIImageType, target: String) { + self.commander.send(UIImageCacheLoaderCommand::ProvideRemote(needle, target)).unwrap(); + } + + pub fn get(&self, needle: UIImageType) -> Option { + // i'm hardly building this in a performant way but it's robust and solid unlike the previous mess + let mut cache = self.cache.lock().unwrap(); + if let Some(loading_or_loaded) = cache.get(&needle) { + if let Some(loaded) = loading_or_loaded { + Some(loaded.clone()) + } else { None } } else { - bail!("could not load from FS") + cache.insert(needle.clone(), None); + self.commander.send(UIImageCacheLoaderCommand::Load(needle)).unwrap(); + None } } -} +} \ No newline at end of file diff --git a/maxima-ui/src/views/downloads_view.rs b/maxima-ui/src/views/downloads_view.rs index 5885322..27bef47 100644 --- a/maxima-ui/src/views/downloads_view.rs +++ b/maxima-ui/src/views/downloads_view.rs @@ -1,7 +1,7 @@ use egui::{pos2, vec2, Align2, Color32, FontId, Mesh, Rect, Rounding, Shape, Stroke, Ui, Widget}; use humansize::DECIMAL; -use crate::{bridge_thread, GameUIImages, GameUIImagesWrapper, MaximaEguiApp, APP_MARGIN}; +use crate::{bridge_thread, MaximaEguiApp, APP_MARGIN}; #[derive(Clone)] pub struct QueuedDownload { @@ -19,18 +19,12 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is ui.allocate_ui(size, |ui| { let game_dl = game; let game = app.games.get_mut(&game.slug).unwrap(); - let game_images: Option<&GameUIImages> = match &game.images { // TODO: just replace this entire system with the one i made in a newer project - GameUIImagesWrapper::Unloaded => { - app.backend.backend_commander.send(bridge_thread::MaximaLibRequest::GetGameImagesRequest(game.slug.clone())).unwrap(); - game.images = GameUIImagesWrapper::Loading; - None - }, - GameUIImagesWrapper::Loading => { - None - }, - GameUIImagesWrapper::Available(images) => { - Some(images) }, - }; + let (hero, logo) = { + ( + app.img_cache.get(crate::ui_image::UIImageType::Hero(game.slug.clone())), + app.img_cache.get(crate::ui_image::UIImageType::Logo(game.slug.clone())) + ) + }; let container_rect = Rect { min: ui.cursor().min, max: ui.cursor().min + size @@ -40,9 +34,9 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is ui.painter().rect_filled(container_rect, Rounding::same(corner_radius), Color32::BLACK); - if let Some(img) = game_images { + if let Some(img) = hero { let img_rounding = Rounding { nw: corner_radius, ne: 0.0, sw: corner_radius, se: 0.0 }; - let img_response = ui.add(egui::Image::new((img.hero.renderable, img.hero.size)).rounding(img_rounding).max_size(size)); + let img_response = ui.add(egui::Image::new((img.id(), size)).rounding(img_rounding).max_size(size)); let top_left = pos2(img_response.rect.max.x - 80.0, img_response.rect.min.y); let top_right = pos2(img_response.rect.max.x - 00.0, img_response.rect.min.y); @@ -65,9 +59,9 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is mesh.add_triangle(3, 4, 2); ui.painter().add(Shape::mesh(mesh)); - if let Some(logo) = &img.logo { + if let Some(handle) = &logo { let logo_rect = img_response.rect.clone().shrink(26.0); - ui.put(logo_rect, egui::Image::new((logo.renderable, logo.size)).maintain_aspect_ratio(true).fit_to_exact_size(logo_rect.size())); + ui.put(logo_rect, egui::Image::new(handle).maintain_aspect_ratio(true).fit_to_exact_size(logo_rect.size())); //ui.painter().image(logo.renderable, logo_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); } diff --git a/maxima-ui/src/views/friends_view.rs b/maxima-ui/src/views/friends_view.rs index a440bcf..5f1ebf3 100644 --- a/maxima-ui/src/views/friends_view.rs +++ b/maxima-ui/src/views/friends_view.rs @@ -3,7 +3,7 @@ use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use egui::{pos2, vec2, Align2, Color32, FontId, Id, Rect, Rounding, Stroke, Ui, Vec2}; use maxima::rtm::client::BasicPresence; -use crate::{bridge_thread, main, translation_manager::positional_replace, ui_image::UIImage, widgets::enum_dropdown::enum_dropdown, MaximaEguiApp, FRIEND_INGAME_COLOR}; +use crate::{bridge_thread, main, translation_manager::positional_replace, widgets::enum_dropdown::enum_dropdown, MaximaEguiApp, FRIEND_INGAME_COLOR}; use strum_macros::EnumIter; @@ -33,16 +33,6 @@ pub struct FriendsViewBar { pub friend_sel : String, } -pub enum UIFriendImageWrapper { - /// The user doesn't have an avatar or otherwise the app doesn't want it - DoNotLoad, - /// Avatar exists but is not loaded - Unloaded(String), - /// Avatar is being loaded - Loading, - /// Avatar can be rendered - Available(Arc) -} pub struct UIFriend { pub name : String, @@ -50,14 +40,8 @@ pub struct UIFriend { pub online : BasicPresence, pub game : Option, pub game_presence : Option, - pub avatar: UIFriendImageWrapper, } -impl UIFriend { - pub fn set_avatar_loading_flag(&mut self) { - self.avatar = UIFriendImageWrapper::Loading; - } -} const F9B233: Color32 = Color32::from_rgb(249, 178, 51); const DARK_GREY: Color32 = Color32::from_rgb(64, 64, 64); @@ -201,22 +185,6 @@ pub fn friends_view(app : &mut MaximaEguiApp, ui: &mut Ui) { let buttons = app.friends_view_bar.friend_sel.eq(&friend.id) && friend_rect_hovered; if buttons { app.force_friends = true; } let how_buttons = context.animate_bool_with_easing(Id::new("friendlistbuttons_".to_owned()+&friend.id), buttons, ease_out_cubic); - let avatar: Option<&Arc> = match &friend.avatar { - UIFriendImageWrapper::DoNotLoad => { - None - }, - UIFriendImageWrapper::Unloaded(url) => { - let _ = app.backend.backend_commander.send(bridge_thread::MaximaLibRequest::GetUserAvatarRequest(friend.id.clone(), url.to_string())); - friend.set_avatar_loading_flag(); - None - }, - UIFriendImageWrapper::Loading => { - None - }, - UIFriendImageWrapper::Available(img) => { - Some(img) - }, - }; let (friend_status, friend_color) = match friend.online { BasicPresence::Unknown => (&app.locale.localization.friends_view.status.unknown as &String, Color32::DARK_RED), @@ -287,7 +255,7 @@ pub fn friends_view(app : &mut MaximaEguiApp, ui: &mut Ui) { } }); } - + let pfp_rect = Rect { min: main_res.rect.min + vec2(2.0, 2.0), max: main_res.rect.min + vec2(2.0, 2.0) + vec2(PFP_SIZE, PFP_SIZE) @@ -298,10 +266,10 @@ pub fn friends_view(app : &mut MaximaEguiApp, ui: &mut Ui) { max: pfp_rect.max + vec2(1.0, 1.0) }; - if let Some(pfp) = avatar { - main_painter.image(pfp.renderable, pfp_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); + if let Some(pfp) = app.img_cache.get(crate::ui_image::UIImageType::Avatar(friend.id.clone())) { + main_painter.image(pfp.id(), pfp_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); } else { - main_painter.image(app.user_pfp_renderable, pfp_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); + main_painter.image(app.img_cache.placeholder_avatar.id(), pfp_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); } main_painter.rect(outline_rect, Rounding::same(4.0), Color32::TRANSPARENT, Stroke::new(2.0, friend_color)); diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index a9f6012..e5f5e74 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -2,7 +2,7 @@ use eframe::glow::OBJECT_TYPE; use egui::{pos2, vec2, Color32, Margin, Mesh, Pos2, Rect, RichText, Rounding, ScrollArea, Shape, Stroke, Ui}; use log::debug; use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; -use crate::{bridge_thread, set_app_modal, widgets::enum_dropdown::enum_dropdown, GameDetails, GameDetailsWrapper, GameInfo, GameUIImages, GameUIImagesWrapper, InstallModalState, MaximaEguiApp, PageType, PopupModal}; +use crate::{bridge_thread, set_app_modal, widgets::enum_dropdown::enum_dropdown, GameDetails, GameDetailsWrapper, GameInfo, InstallModalState, MaximaEguiApp, PageType, PopupModal}; use strum_macros::EnumIter; @@ -54,18 +54,11 @@ pub fn game_view_details_panel(app : &mut MaximaEguiApp, ui: &mut Ui) { puffin::profile_function!(); if app.games.len() < 1 { return } let game: &mut GameInfo = if let Some(game) = app.games.get_mut(&app.game_sel) { game } else { return }; - let game_images: Option<&GameUIImages> = match &game.images { - GameUIImagesWrapper::Unloaded => { - debug!("Loading images for {:?}", game.name); - app.backend.backend_commander.send(bridge_thread::MaximaLibRequest::GetGameImagesRequest(game.slug.clone())).unwrap(); - game.images = GameUIImagesWrapper::Loading; - None - }, - GameUIImagesWrapper::Loading => { - None - }, - GameUIImagesWrapper::Available(images) => { - Some(images) }, + let (hero, logo) = { + ( + app.img_cache.get(crate::ui_image::UIImageType::Hero(game.slug.clone())), + app.img_cache.get(crate::ui_image::UIImageType::Logo(game.slug.clone())) + ) }; let game_details: Option<&GameDetails> = match &game.details { @@ -86,8 +79,8 @@ pub fn game_view_details_panel(app : &mut MaximaEguiApp, ui: &mut Ui) { let mut hero_rect = Rect::clone(&ui.available_rect_before_wrap()); let aspect_ratio: f32 = - if let Some(images) = game_images { - images.hero.size.x / images.hero.size.y + if let Some(handle) = &hero { + handle.aspect_ratio() } else { 16.0 / 9.0 }; @@ -123,9 +116,9 @@ pub fn game_view_details_panel(app : &mut MaximaEguiApp, ui: &mut Ui) { logo_transition_frac = bezier_ease(hero_vis_frac); { puffin::profile_scope!("hero image"); - if let Some(images) = game_images { + if let Some(handle) = hero { if let Some(gvbg) = &app.game_view_bg_renderer { - gvbg.draw(ui, hero_rect, images.hero.size, images.hero.renderable, hero_vis_frac); + gvbg.draw(ui, hero_rect, handle.size_vec2(), handle.id(), hero_vis_frac); //TODO: negative allocation fix //ui.allocate_space(hero_rect.size().max(vec2(0.0, 0.0))); } @@ -135,7 +128,7 @@ pub fn game_view_details_panel(app : &mut MaximaEguiApp, ui: &mut Ui) { } } - if hero_vis_frac < 1.0 && game_images.is_some(){ + if hero_vis_frac < 1.0 /* && hero.is_some() */ { // TODO: find a better solution let mut mesh = Mesh::default(); @@ -376,12 +369,11 @@ pub fn game_view_details_panel(app : &mut MaximaEguiApp, ui: &mut Ui) { }); }); // ScrollArea - if let Some(images) = game_images { - if let Some(logo) = &images.logo { - let logo_size_pre = if logo.size.x >= logo.size.y { + if let Some(handle) = logo { + let logo_size_pre = if handle.size_vec2().x >= handle.size_vec2().y { // wider than it is tall, scale based on X as max - let mult_frac = 320.0 / logo.size.x; - logo.size.y * mult_frac + let mult_frac = 320.0 / handle.size_vec2().x; + handle.size_vec2().y * mult_frac } else { // taller than it is wide, scale based on Y // fringe edge case, here in case EA decides they want to pull something really fucking stupid @@ -393,11 +385,8 @@ pub fn game_view_details_panel(app : &mut MaximaEguiApp, ui: &mut Ui) { Pos2 { x: (egui::lerp(hero_rect.min.x..=hero_rect.max.x-180.0, frac2)), y: (hero_rect.min.y) }, Pos2 { x: (egui::lerp(hero_rect.max.x..=hero_rect.max.x-20.0, frac2)), y: (egui::lerp(hero_rect.max.y..=hero_rect.min.y+80.0, frac2)) } ); - ui.put(logo_rect, egui::Image::new((logo.renderable, logo_size))); - } - } else { - //ui.put(hero_rect, egui::Label::new("NO LOGO")); - } + ui.put(logo_rect, egui::Image::new((handle.id(), logo_size))); + } }); // Vertical }