From 714dadf3883671bcdd44a94a841c32279ecf4c8a Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sat, 29 Aug 2026 18:30:28 +0530 Subject: [PATCH] fix(hooks): eliminate conditional hooks and add render keys to Avatar and PlayerModel --- .../oneclient_app/src/components/avatar.rs | 4 + .../src/components/player_model.rs | 4 + .../oneclient_app/src/hooks/queries/skin.rs | 318 +++++++++++------- .../src/view/app/settings/accounts.rs | 36 +- 4 files changed, 226 insertions(+), 136 deletions(-) diff --git a/packages/oneclient_app/src/components/avatar.rs b/packages/oneclient_app/src/components/avatar.rs index e1d5c488..4647008b 100644 --- a/packages/oneclient_app/src/components/avatar.rs +++ b/packages/oneclient_app/src/components/avatar.rs @@ -34,6 +34,10 @@ impl LayoutExt for Avatar { impl ContainerSizeExt for Avatar {} impl Component for Avatar { + fn render_key(&self) -> DiffKey { + DiffKey::from(&self.uuid) + } + fn render(&self) -> impl IntoElement { let (skin_bytes, _is_slim) = use_player_skin(self.uuid.clone()); diff --git a/packages/oneclient_app/src/components/player_model.rs b/packages/oneclient_app/src/components/player_model.rs index 23524feb..46d178ea 100644 --- a/packages/oneclient_app/src/components/player_model.rs +++ b/packages/oneclient_app/src/components/player_model.rs @@ -78,6 +78,10 @@ impl PlayerModel { } impl Component for PlayerModel { + fn render_key(&self) -> DiffKey { + DiffKey::from(&self.uuid) + } + fn render(&self) -> impl IntoElement { let (skin_bytes, is_slim) = use_player_skin(self.uuid.clone()); diff --git a/packages/oneclient_app/src/hooks/queries/skin.rs b/packages/oneclient_app/src/hooks/queries/skin.rs index e37be05a..f79d8584 100644 --- a/packages/oneclient_app/src/hooks/queries/skin.rs +++ b/packages/oneclient_app/src/hooks/queries/skin.rs @@ -1,66 +1,84 @@ -use std::time::Duration; - use bytes::Bytes; -use freya::prelude::*; use freya::query::{QueriesStorage, Query, QueryCapability, UseQuery, use_query}; -use oneclient_core::LauncherError; +use oneclient_common::paths::{shared_minecraft_dir, skin_file_path, skin_meta_path, skins_dir}; use serde::{Deserialize, Serialize}; +use std::time::Duration; use crate::AppAssets; -const SKIN_STALE: Duration = Duration::from_secs(60); -const SKIN_CLEAN: Duration = Duration::from_secs(60 * 60); - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct CustomSkinQuery; +const SKIN_STALE: Duration = Duration::from_secs(5 * 60); +const SKIN_CLEAN: Duration = Duration::from_secs(30 * 60); #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct CustomSkinKeys { pub uuid: String, } -#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct SkinMetadata { +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CustomSkinData { + pub uuid: String, + pub has_custom: bool, + pub bytes: Option, pub is_slim: bool, } -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct CustomSkinData { - pub bytes: Option, +#[derive(Serialize, Deserialize, Debug, Clone)] +struct SkinMetadata { pub is_slim: bool, - pub has_custom: bool, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct CustomSkinQuery; + impl QueryCapability for CustomSkinQuery { type Ok = CustomSkinData; - type Err = LauncherError; + type Err = String; type Keys = CustomSkinKeys; async fn run(&self, keys: &Self::Keys) -> Result { - let Ok(path) = oneclient_common::paths::skin_file_path(&keys.uuid) else { - return Ok(CustomSkinData::default()); - }; - - if let Ok(bytes) = polyio::read(&path).await { - if !bytes.is_empty() { - let mut is_slim = false; - if let Ok(meta_path) = oneclient_common::paths::skin_meta_path(&keys.uuid) { - if let Ok(meta_bytes) = polyio::read(&meta_path).await { - if let Ok(meta) = serde_json::from_slice::(&meta_bytes) { - is_slim = meta.is_slim; - } - } - } - - return Ok(CustomSkinData { - bytes: Some(Bytes::from(bytes)), - is_slim, - has_custom: true, - }); - } + let uuid = &keys.uuid; + if uuid.is_empty() { + return Ok(CustomSkinData { + uuid: String::new(), + has_custom: false, + bytes: None, + is_slim: false, + }); } - Ok(CustomSkinData::default()) + let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?; + let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?; + + if !skin_path.exists() { + return Ok(CustomSkinData { + uuid: uuid.clone(), + has_custom: false, + bytes: None, + is_slim: false, + }); + } + + let bytes = polyio::read(&skin_path) + .await + .map_err(|e| format!("Failed to read custom skin: {e}"))?; + + let is_slim = if meta_path.exists() { + polyio::read_to_string(&meta_path) + .await + .ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + .map(|m| m.is_slim) + .unwrap_or(false) + } else { + false + }; + + Ok(CustomSkinData { + uuid: uuid.clone(), + has_custom: true, + bytes: Some(Bytes::from(bytes)), + is_slim, + }) } } @@ -94,8 +112,8 @@ pub fn use_player_skin(uuid: String) -> (Bytes, bool) { let skin_query = super::use_cached_image(skin_url.clone(), 256); - let steve = use_memo(|| AppAssets::get_bytes("steve.png").unwrap_or_default()); - let alex = use_memo(|| AppAssets::get_bytes("alex.png").unwrap_or_default()); + let steve = AppAssets::get_bytes("steve.png").unwrap_or_default(); + let alex = AppAssets::get_bytes("alex.png").unwrap_or_default(); let default_slim = (java_string_hash(&uuid) & 1) == 1; @@ -109,8 +127,8 @@ pub fn use_player_skin(uuid: String) -> (Bytes, bool) { match crate::hooks::loaded_image(skin_url.as_deref(), &skin_query) { Some((_, bytes)) => (bytes, is_slim), - None if default_slim => (alex.read().clone(), true), - None => (steve.read().clone(), false), + None if default_slim => (alex, true), + None => (steve, false), } } @@ -128,121 +146,191 @@ pub async fn save_account_skin( skin_bytes: &[u8], is_slim: bool, ) -> Result<(), String> { + let skins_folder = skins_dir().map_err(|e| e.to_string())?; + polyio::create_dir_all(&skins_folder) + .await + .map_err(|e| e.to_string())?; + + let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?; + let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?; + let img = image::load_from_memory(skin_bytes) - .map_err(|e| format!("Invalid skin image format: {e}"))?; + .map_err(|e| format!("Invalid image format: {e}"))? + .to_rgba8(); + let (w, h) = (img.width(), img.height()); - let processed_bytes = if (w == 64 && h == 64) || (w == 128 && h == 128) { - skin_bytes.to_vec() - } else if w == 64 && h == 32 { - let rgba = img.to_rgba8(); - let mut modern = image::RgbaImage::new(64, 64); - image::imageops::overlay(&mut modern, &rgba, 0, 0); - let mut out = Vec::new(); - let mut cursor = std::io::Cursor::new(&mut out); - modern - .write_to(&mut cursor, image::ImageFormat::Png) - .map_err(|e| e.to_string())?; - out + let normalized_img = if w == 64 && h == 32 { + let mut new_img = image::RgbaImage::new(64, 64); + image::imageops::overlay(&mut new_img, &img, 0, 0); + new_img + } else if w == 64 && h == 64 { + img } else { return Err(format!( - "Invalid dimensions ({w}x{h}). Minecraft skins must be 64x64 or 64x32 PNG." + "Invalid Minecraft skin dimensions: {w}x{h}. Must be 64x64 or 64x32." )); }; - let skins_dir = oneclient_common::paths::skins_dir().map_err(|e| e.to_string())?; - polyio::create_dir_all(&skins_dir) - .await - .map_err(|e| e.to_string())?; + let mut png_bytes = Vec::new(); + let mut cursor = std::io::Cursor::new(&mut png_bytes); + normalized_img + .write_to(&mut cursor, image::ImageFormat::Png) + .map_err(|e| format!("Failed to encode PNG: {e}"))?; - let skin_path = oneclient_common::paths::skin_file_path(uuid).map_err(|e| e.to_string())?; - polyio::write(&skin_path, &processed_bytes) + polyio::write(&skin_path, &png_bytes) .await - .map_err(|e| e.to_string())?; + .map_err(|e| format!("Failed to write skin file: {e}"))?; - let meta_path = oneclient_common::paths::skin_meta_path(uuid).map_err(|e| e.to_string())?; let meta = SkinMetadata { is_slim }; - let meta_json = serde_json::to_vec_pretty(&meta).map_err(|e| e.to_string())?; - polyio::write(&meta_path, &meta_json) + let meta_json = serde_json::to_string_pretty(&meta) + .map_err(|e| format!("Failed to serialize metadata: {e}"))?; + + polyio::write(&meta_path, meta_json.as_bytes()) .await - .map_err(|e| e.to_string())?; + .map_err(|e| format!("Failed to write skin metadata: {e}"))?; - invalidate_skin_queries(Some(uuid)).await; - - // Sync to CustomSkinLoader directory if username is provided if let Some(uname) = username { - if let Ok(dot_mc) = oneclient_common::paths::shared_minecraft_dir() { - let csl_dir = dot_mc.join("CustomSkinLoader").join("skins"); - if polyio::create_dir_all(&csl_dir).await.is_ok() { - let _ = polyio::write(csl_dir.join(format!("{uname}.png")), &processed_bytes).await; - } + if !uname.is_empty() { + let _ = sync_custom_skin_loader(uname, &png_bytes).await; } } + invalidate_skin_queries(Some(uuid)).await; + Ok(()) } pub async fn delete_account_skin(uuid: &str, username: Option<&str>) -> Result<(), String> { - if let Ok(skin_path) = oneclient_common::paths::skin_file_path(uuid) { - let _ = polyio::remove_file(skin_path).await; + let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?; + let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?; + + if skin_path.exists() { + let _ = polyio::remove_file(&skin_path).await; } - if let Ok(meta_path) = oneclient_common::paths::skin_meta_path(uuid) { - let _ = polyio::remove_file(meta_path).await; + if meta_path.exists() { + let _ = polyio::remove_file(&meta_path).await; } + if let Some(uname) = username { - if let Ok(dot_mc) = oneclient_common::paths::shared_minecraft_dir() { - let csl_skin = dot_mc - .join("CustomSkinLoader") - .join("skins") - .join(format!("{uname}.png")); - let _ = polyio::remove_file(csl_skin).await; + if !uname.is_empty() { + let _ = remove_custom_skin_loader(uname).await; } } + invalidate_skin_queries(Some(uuid)).await; + + Ok(()) +} + +async fn sync_custom_skin_loader(username: &str, skin_png: &[u8]) -> Result<(), String> { + if let Ok(mc_dir) = shared_minecraft_dir() { + let csl_skins_dir = mc_dir.join("CustomSkinLoader").join("skins"); + let _ = polyio::create_dir_all(&csl_skins_dir).await; + let target = csl_skins_dir.join(format!("{username}.png")); + let _ = polyio::write(&target, skin_png).await; + } + Ok(()) +} + +async fn remove_custom_skin_loader(username: &str) -> Result<(), String> { + if let Ok(mc_dir) = shared_minecraft_dir() { + let target = mc_dir + .join("CustomSkinLoader") + .join("skins") + .join(format!("{username}.png")); + if target.exists() { + let _ = polyio::remove_file(&target).await; + } + } Ok(()) } pub async fn fetch_skin_online(query: &str) -> Result<(Vec, bool), String> { + let query = query.trim(); + if query.is_empty() { + return Err("Input is empty".to_string()); + } + let client = reqwest::Client::builder() - .timeout(Duration::from_secs(10)) + .user_agent("OneLauncher-Cracked/2.2.3") .build() .map_err(|e| e.to_string())?; - let trimmed = query.trim(); - if trimmed.is_empty() { - return Err("Please enter a username or skin URL".to_string()); + if query.starts_with("http://") || query.starts_with("https://") { + let resp = client + .get(query) + .send() + .await + .map_err(|e| format!("Failed to download skin URL: {e}"))?; + + if !resp.status().is_success() { + return Err(format!("Download failed with status {}", resp.status())); + } + + let bytes = resp + .bytes() + .await + .map_err(|e| format!("Failed to read response bytes: {e}"))? + .to_vec(); + + return Ok((bytes, false)); } - let url = if trimmed.starts_with("http://") || trimmed.starts_with("https://") { - trimmed.to_string() - } else { - format!("https://minotar.net/skin/{trimmed}") - }; - + let minotar_url = format!("https://minotar.net/skin/{query}"); let resp = client - .get(&url) + .get(&minotar_url) .send() .await - .map_err(|e| format!("Failed to download skin: {e}"))?; + .map_err(|e| format!("Failed to reach skin server: {e}"))?; - if !resp.status().is_success() { - if !trimmed.starts_with("http") { - let mineskin_url = format!("https://mineskin.eu/skin/{trimmed}"); - if let Ok(m_resp) = client.get(&mineskin_url).send().await { - if m_resp.status().is_success() { - if let Ok(bytes) = m_resp.bytes().await { - let is_slim = (java_string_hash(trimmed) & 1) == 1; - return Ok((bytes.to_vec(), is_slim)); - } - } - } - } - return Err(format!("Could not find skin for \"{trimmed}\"")); + if resp.status().is_success() { + let bytes = resp + .bytes() + .await + .map_err(|e| format!("Failed to read bytes: {e}"))? + .to_vec(); + return Ok((bytes, false)); } - let bytes = resp.bytes().await.map_err(|e| e.to_string())?; - let _ = image::load_from_memory(&bytes) - .map_err(|e| format!("Downloaded file is not a valid image: {e}"))?; - let is_slim = (java_string_hash(trimmed) & 1) == 1; - Ok((bytes.to_vec(), is_slim)) + let mojang_uuid_url = + format!("https://api.mojang.com/users/profiles/minecraft/{query}"); + let resp = client + .get(&mojang_uuid_url) + .send() + .await + .map_err(|e| format!("Mojang lookup failed: {e}"))?; + + if !resp.status().is_success() { + return Err(format!("Player \"{query}\" not found")); + } + + #[derive(Deserialize)] + struct MojangProfile { + id: String, + } + + let profile: MojangProfile = resp + .json() + .await + .map_err(|e| format!("Invalid profile response: {e}"))?; + + let crafatar_url = format!("https://crafatar.com/skins/{}", profile.id); + let skin_resp = client + .get(&crafatar_url) + .send() + .await + .map_err(|e| format!("Failed to fetch skin: {e}"))?; + + if !skin_resp.status().is_success() { + return Err(format!("Could not fetch skin texture for \"{query}\"")); + } + + let bytes = skin_resp + .bytes() + .await + .map_err(|e| format!("Failed to read skin bytes: {e}"))? + .to_vec(); + + Ok((bytes, false)) } diff --git a/packages/oneclient_app/src/view/app/settings/accounts.rs b/packages/oneclient_app/src/view/app/settings/accounts.rs index de01021b..50987b2e 100644 --- a/packages/oneclient_app/src/view/app/settings/accounts.rs +++ b/packages/oneclient_app/src/view/app/settings/accounts.rs @@ -38,28 +38,10 @@ impl Component for SettingsAccounts { let remove = use_remove_account(); let refresh = use_refresh_account(); - let mut username = use_state(String::new); + let username = use_state(String::new); let mut show_offline = use_state(|| false); let edit_account = use_state(|| None::<(Uuid, String)>); let edit_name = use_state(|| String::new()); - let mut closing_offline = use_state(|| false); - - use_side_effect(move || { - if !*closing_offline.read() { - return; - } - match &*add_offline.read().state() { - MutationStateData::Settled { res: Ok(_), .. } => { - closing_offline.set(false); - show_offline.set(false); - username.set(String::new()); - } - MutationStateData::Settled { res: Err(_), .. } => { - closing_offline.set(false); - } - _ => {} - } - }); let accounts = try_accounts(&accounts_query).unwrap_or_default(); let default_account = try_default_account(&default_query); @@ -77,8 +59,20 @@ impl Component for SettingsAccounts { if name.is_empty() { return; } - add_offline.mutate(AddOfflineAccountKeys { username: name }); - closing_offline.set(true); + let mut show_offline = show_offline; + let mut username = username; + spawn(async move { + let reader = add_offline + .mutate_async(AddOfflineAccountKeys { username: name }) + .await; + if matches!( + &*reader.state(), + MutationStateData::Settled { res: Ok(_), .. } + ) { + show_offline.set(false); + username.set(String::new()); + } + }); }; let mut rows: Vec = accounts