feat(engine): wire avatar download and display into profile modal
Build and Deploy / build-and-push (push) Successful in 4m15s
Build and Deploy / build-and-push (push) Successful in 4m15s
- Add avatar_plugin: AvatarPlugin, AvatarResource, AvatarFetchEvent
- After AvatarFetchEvent fires, spawns an async reqwest download task
- On completion, decodes image bytes via image::load_from_memory →
Image::from_dynamic and inserts into Assets<Image>
- Expand auth task to also call fetch_me_with_token immediately after
login/register so avatar_url is available without a second round-trip
- poll_auth_task fires AvatarFetchEvent when avatar_url is Some, building
the full URL from base_url + relative avatar path
- Profile modal shows 48px circular avatar ImageNode when AvatarResource
is populated, or an initials disc (first letter of username) as fallback
- Add image = "0.25" and reqwest to solitaire_engine deps
- Add fetch_me_with_token helper to SolitaireServerClient for use when
the access token hasn't been persisted to keychain yet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use solitaire_core::achievement::{achievement_by_id, ALL_ACHIEVEMENTS};
|
||||
use solitaire_data::SyncBackend;
|
||||
|
||||
use crate::achievement_plugin::AchievementsResource;
|
||||
use crate::avatar_plugin::AvatarResource;
|
||||
use crate::events::ToggleProfileRequestEvent;
|
||||
use crate::font_plugin::FontResource;
|
||||
use crate::progress_plugin::ProgressResource;
|
||||
@@ -143,6 +144,7 @@ fn toggle_profile_screen(
|
||||
achievements: Option<Res<AchievementsResource>>,
|
||||
stats: Option<Res<StatsResource>>,
|
||||
font_res: Option<Res<FontResource>>,
|
||||
avatar: Option<Res<AvatarResource>>,
|
||||
screens: Query<Entity, With<ProfileScreen>>,
|
||||
) {
|
||||
let button_clicked = requests.read().count() > 0;
|
||||
@@ -170,10 +172,12 @@ fn toggle_profile_screen(
|
||||
achievements.as_deref(),
|
||||
stats.as_deref(),
|
||||
font_res.as_deref(),
|
||||
avatar.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn spawn_profile_screen(
|
||||
commands: &mut Commands,
|
||||
settings: Option<&SettingsResource>,
|
||||
@@ -182,6 +186,7 @@ fn spawn_profile_screen(
|
||||
achievements: Option<&AchievementsResource>,
|
||||
stats: Option<&StatsResource>,
|
||||
font_res: Option<&FontResource>,
|
||||
avatar: Option<&AvatarResource>,
|
||||
) {
|
||||
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
||||
let font_section = TextFont {
|
||||
@@ -245,11 +250,61 @@ fn spawn_profile_screen(
|
||||
));
|
||||
if let Some(s) = settings {
|
||||
let (backend_name, username) = sync_info(&s.0.sync_backend);
|
||||
body.spawn((
|
||||
Text::new(format!("Account: {username} | Backend: {backend_name}")),
|
||||
font_row.clone(),
|
||||
TextColor(TEXT_PRIMARY),
|
||||
));
|
||||
|
||||
// Avatar row: image (if downloaded) or filled initials circle.
|
||||
let avatar_handle = avatar.and_then(|a| a.0.clone());
|
||||
body.spawn(Node {
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
column_gap: Val::Px(10.0),
|
||||
margin: UiRect { bottom: Val::Px(4.0), ..default() },
|
||||
..default()
|
||||
})
|
||||
.with_children(|row| {
|
||||
const SIZE: f32 = 48.0;
|
||||
const RADIUS: f32 = 24.0;
|
||||
if let Some(handle) = avatar_handle {
|
||||
row.spawn((
|
||||
ImageNode::new(handle),
|
||||
Node {
|
||||
width: Val::Px(SIZE),
|
||||
height: Val::Px(SIZE),
|
||||
border_radius: BorderRadius::all(Val::Px(RADIUS)),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
} else {
|
||||
// Initials fallback: coloured disc with the first letter.
|
||||
let initial = username.chars().next().unwrap_or('?').to_uppercase().next().unwrap_or('?');
|
||||
row.spawn((
|
||||
Node {
|
||||
width: Val::Px(SIZE),
|
||||
height: Val::Px(SIZE),
|
||||
border_radius: BorderRadius::all(Val::Px(RADIUS)),
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BG_ELEVATED),
|
||||
))
|
||||
.with_children(|disc| {
|
||||
disc.spawn((
|
||||
Text::new(initial.to_string()),
|
||||
TextFont {
|
||||
font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
|
||||
font_size: 22.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_SECONDARY),
|
||||
));
|
||||
});
|
||||
}
|
||||
row.spawn((
|
||||
Text::new(format!("{username} | {backend_name}")),
|
||||
font_row.clone(),
|
||||
TextColor(TEXT_PRIMARY),
|
||||
));
|
||||
});
|
||||
}
|
||||
if let Some(ss) = sync_status {
|
||||
let status_text = match &ss.0 {
|
||||
|
||||
Reference in New Issue
Block a user