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:
Generated
+7
@@ -4034,9 +4034,14 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
|
"color_quant",
|
||||||
|
"gif",
|
||||||
|
"image-webp",
|
||||||
"moxcms",
|
"moxcms",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
|
"zune-core",
|
||||||
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7013,8 +7018,10 @@ dependencies = [
|
|||||||
"bevy",
|
"bevy",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"image",
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
"kira",
|
"kira",
|
||||||
|
"reqwest",
|
||||||
"resvg",
|
"resvg",
|
||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ ron = "0.12"
|
|||||||
# only `deflate` is needed because the importer rejects other
|
# only `deflate` is needed because the importer rejects other
|
||||||
# compression methods anyway (see Phase 7 spec).
|
# compression methods anyway (see Phase 7 spec).
|
||||||
zip = { version = "8.6", default-features = false, features = ["deflate"] }
|
zip = { version = "8.6", default-features = false, features = ["deflate"] }
|
||||||
|
# Image decoding for avatar bytes received from the server.
|
||||||
|
# Features mirror what Bevy already enables via bevy_image.
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
|
||||||
|
|
||||||
# Importer-only test dependency: tests build zip archives in a
|
# Importer-only test dependency: tests build zip archives in a
|
||||||
# scratch directory so they don't pollute the real user themes path
|
# scratch directory so they don't pollute the real user themes path
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use bevy::winit::WinitWindows;
|
|||||||
use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings};
|
use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings};
|
||||||
use solitaire_engine::{
|
use solitaire_engine::{
|
||||||
register_theme_asset_sources, AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin,
|
register_theme_asset_sources, AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin,
|
||||||
AudioPlugin, AutoCompletePlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
AudioPlugin, AutoCompletePlugin, AvatarPlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
||||||
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin,
|
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin,
|
||||||
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin,
|
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin,
|
||||||
OnboardingPlugin, PausePlugin, PlayBySeedPlugin, ProfilePlugin, ProgressPlugin,
|
OnboardingPlugin, PausePlugin, PlayBySeedPlugin, ProfilePlugin, ProgressPlugin,
|
||||||
@@ -187,6 +187,7 @@ pub fn run() {
|
|||||||
.add_plugins(HudPlugin)
|
.add_plugins(HudPlugin)
|
||||||
.add_plugins(HelpPlugin)
|
.add_plugins(HelpPlugin)
|
||||||
.add_plugins(HomePlugin::default())
|
.add_plugins(HomePlugin::default())
|
||||||
|
.add_plugins(AvatarPlugin)
|
||||||
.add_plugins(ProfilePlugin)
|
.add_plugins(ProfilePlugin)
|
||||||
.add_plugins(PausePlugin)
|
.add_plugins(PausePlugin)
|
||||||
.add_plugins(SettingsPlugin::default())
|
.add_plugins(SettingsPlugin::default())
|
||||||
|
|||||||
@@ -534,6 +534,21 @@ impl SolitaireServerClient {
|
|||||||
Self::extract_me_body(resp).await
|
Self::extract_me_body(resp).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [`fetch_me`] but uses an explicit token instead of reading from the
|
||||||
|
/// OS keychain. Useful immediately after login/register when the token has
|
||||||
|
/// not yet been persisted.
|
||||||
|
pub async fn fetch_me_with_token(&self, token: &str) -> Result<(String, Option<String>), SyncError> {
|
||||||
|
let url = format!("{}/api/me", self.base_url);
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.bearer_auth(token)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| SyncError::Network(e.to_string()))?;
|
||||||
|
Self::extract_me_body(resp).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn extract_me_body(resp: reqwest::Response) -> Result<(String, Option<String>), SyncError> {
|
async fn extract_me_body(resp: reqwest::Response) -> Result<(String, Option<String>), SyncError> {
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ edition.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { workspace = true }
|
bevy = { workspace = true }
|
||||||
|
image = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
kira = { workspace = true }
|
kira = { workspace = true }
|
||||||
solitaire_core = { workspace = true }
|
solitaire_core = { workspace = true }
|
||||||
solitaire_data = { workspace = true }
|
solitaire_data = { workspace = true }
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
//! Downloads and caches the player's server avatar for display in the
|
||||||
|
//! profile modal.
|
||||||
|
//!
|
||||||
|
//! # Flow
|
||||||
|
//!
|
||||||
|
//! 1. After a successful login/register, `sync_setup_plugin` fires
|
||||||
|
//! [`AvatarFetchEvent`] with the server base URL and the relative
|
||||||
|
//! avatar path (e.g. `/avatars/{uuid}.png`).
|
||||||
|
//! 2. [`handle_avatar_fetch`] spawns an async task on the
|
||||||
|
//! [`AsyncComputeTaskPool`] that downloads the image bytes via
|
||||||
|
//! `reqwest` (reusing the same HTTP client pattern as the sync client).
|
||||||
|
//! 3. [`poll_avatar_task`] harvests the result, decodes the bytes into a
|
||||||
|
//! Bevy [`Image`] via `image::load_from_memory`, inserts it into
|
||||||
|
//! [`Assets<Image>`], and stores the [`Handle<Image>`] in
|
||||||
|
//! [`AvatarResource`].
|
||||||
|
//! 4. `profile_plugin` reads [`AvatarResource`] when the profile modal
|
||||||
|
//! opens and renders the avatar image (or an initials fallback when
|
||||||
|
//! `AvatarResource` is `None`).
|
||||||
|
|
||||||
|
use bevy::asset::RenderAssetUsages;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::tasks::{futures_lite::future, AsyncComputeTaskPool, Task};
|
||||||
|
|
||||||
|
/// Stores the loaded avatar [`Handle<Image>`], or `None` when no avatar
|
||||||
|
/// has been fetched yet (new account, no internet, or fetch in progress).
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct AvatarResource(pub Option<Handle<Image>>);
|
||||||
|
|
||||||
|
/// Fired by `sync_setup_plugin` after a successful login or register when
|
||||||
|
/// the server reports that the user has a profile picture set.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AvatarFetchEvent {
|
||||||
|
/// Full HTTP(S) URL to the avatar image (base_url + avatar_url path).
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl bevy::prelude::Message for AvatarFetchEvent {}
|
||||||
|
|
||||||
|
/// In-flight avatar download task. Returns the raw image bytes on success,
|
||||||
|
/// or `None` on any network / decode error.
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct PendingAvatarTask(Option<Task<Option<Vec<u8>>>>);
|
||||||
|
|
||||||
|
pub struct AvatarPlugin;
|
||||||
|
|
||||||
|
impl Plugin for AvatarPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_message::<AvatarFetchEvent>()
|
||||||
|
.init_resource::<AvatarResource>()
|
||||||
|
.init_resource::<PendingAvatarTask>()
|
||||||
|
.add_systems(Update, (handle_avatar_fetch, poll_avatar_task));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_avatar_fetch(
|
||||||
|
mut events: MessageReader<AvatarFetchEvent>,
|
||||||
|
mut pending: ResMut<PendingAvatarTask>,
|
||||||
|
) {
|
||||||
|
for ev in events.read() {
|
||||||
|
// Cancel any in-flight task and restart with the new URL.
|
||||||
|
let url = ev.url.clone();
|
||||||
|
pending.0 = Some(AsyncComputeTaskPool::get().spawn(async move {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.ok()?
|
||||||
|
.block_on(async move {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let bytes = client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.ok()?
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
Some(bytes.to_vec())
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_avatar_task(
|
||||||
|
mut pending: ResMut<PendingAvatarTask>,
|
||||||
|
mut avatar: ResMut<AvatarResource>,
|
||||||
|
mut images: ResMut<Assets<Image>>,
|
||||||
|
) {
|
||||||
|
let Some(task) = pending.0.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(result) = future::block_on(future::poll_once(task)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
pending.0 = None;
|
||||||
|
|
||||||
|
let Some(bytes) = result else { return };
|
||||||
|
|
||||||
|
match image::load_from_memory(&bytes) {
|
||||||
|
Ok(dyn_img) => {
|
||||||
|
let bevy_img = Image::from_dynamic(dyn_img, true, RenderAssetUsages::RENDER_WORLD);
|
||||||
|
let handle = images.add(bevy_img);
|
||||||
|
avatar.0 = Some(handle);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("avatar_plugin: failed to decode avatar image: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ pub mod card_animation;
|
|||||||
pub mod achievement_plugin;
|
pub mod achievement_plugin;
|
||||||
pub mod analytics_plugin;
|
pub mod analytics_plugin;
|
||||||
pub mod animation_plugin;
|
pub mod animation_plugin;
|
||||||
|
pub mod avatar_plugin;
|
||||||
pub mod auto_complete_plugin;
|
pub mod auto_complete_plugin;
|
||||||
pub mod audio_plugin;
|
pub mod audio_plugin;
|
||||||
pub mod card_plugin;
|
pub mod card_plugin;
|
||||||
@@ -123,6 +124,7 @@ pub use leaderboard_plugin::{LeaderboardPlugin, LeaderboardResource, Leaderboard
|
|||||||
pub use input_plugin::InputPlugin;
|
pub use input_plugin::InputPlugin;
|
||||||
pub use onboarding_plugin::{OnboardingPlugin, OnboardingScreen};
|
pub use onboarding_plugin::{OnboardingPlugin, OnboardingScreen};
|
||||||
pub use pause_plugin::{ForfeitConfirmScreen, PausePlugin, PauseScreen, PausedResource};
|
pub use pause_plugin::{ForfeitConfirmScreen, PausePlugin, PauseScreen, PausedResource};
|
||||||
|
pub use avatar_plugin::{AvatarFetchEvent, AvatarPlugin, AvatarResource};
|
||||||
pub use profile_plugin::{ProfilePlugin, ProfileScreen};
|
pub use profile_plugin::{ProfilePlugin, ProfileScreen};
|
||||||
pub use radial_menu::{
|
pub use radial_menu::{
|
||||||
legal_destinations_for_card, radial_anchor_for_index, radial_hovered_index, RadialIcon,
|
legal_destinations_for_card, radial_anchor_for_index, radial_hovered_index, RadialIcon,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use solitaire_core::achievement::{achievement_by_id, ALL_ACHIEVEMENTS};
|
|||||||
use solitaire_data::SyncBackend;
|
use solitaire_data::SyncBackend;
|
||||||
|
|
||||||
use crate::achievement_plugin::AchievementsResource;
|
use crate::achievement_plugin::AchievementsResource;
|
||||||
|
use crate::avatar_plugin::AvatarResource;
|
||||||
use crate::events::ToggleProfileRequestEvent;
|
use crate::events::ToggleProfileRequestEvent;
|
||||||
use crate::font_plugin::FontResource;
|
use crate::font_plugin::FontResource;
|
||||||
use crate::progress_plugin::ProgressResource;
|
use crate::progress_plugin::ProgressResource;
|
||||||
@@ -143,6 +144,7 @@ fn toggle_profile_screen(
|
|||||||
achievements: Option<Res<AchievementsResource>>,
|
achievements: Option<Res<AchievementsResource>>,
|
||||||
stats: Option<Res<StatsResource>>,
|
stats: Option<Res<StatsResource>>,
|
||||||
font_res: Option<Res<FontResource>>,
|
font_res: Option<Res<FontResource>>,
|
||||||
|
avatar: Option<Res<AvatarResource>>,
|
||||||
screens: Query<Entity, With<ProfileScreen>>,
|
screens: Query<Entity, With<ProfileScreen>>,
|
||||||
) {
|
) {
|
||||||
let button_clicked = requests.read().count() > 0;
|
let button_clicked = requests.read().count() > 0;
|
||||||
@@ -170,10 +172,12 @@ fn toggle_profile_screen(
|
|||||||
achievements.as_deref(),
|
achievements.as_deref(),
|
||||||
stats.as_deref(),
|
stats.as_deref(),
|
||||||
font_res.as_deref(),
|
font_res.as_deref(),
|
||||||
|
avatar.as_deref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn spawn_profile_screen(
|
fn spawn_profile_screen(
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
settings: Option<&SettingsResource>,
|
settings: Option<&SettingsResource>,
|
||||||
@@ -182,6 +186,7 @@ fn spawn_profile_screen(
|
|||||||
achievements: Option<&AchievementsResource>,
|
achievements: Option<&AchievementsResource>,
|
||||||
stats: Option<&StatsResource>,
|
stats: Option<&StatsResource>,
|
||||||
font_res: Option<&FontResource>,
|
font_res: Option<&FontResource>,
|
||||||
|
avatar: Option<&AvatarResource>,
|
||||||
) {
|
) {
|
||||||
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
||||||
let font_section = TextFont {
|
let font_section = TextFont {
|
||||||
@@ -245,11 +250,61 @@ fn spawn_profile_screen(
|
|||||||
));
|
));
|
||||||
if let Some(s) = settings {
|
if let Some(s) = settings {
|
||||||
let (backend_name, username) = sync_info(&s.0.sync_backend);
|
let (backend_name, username) = sync_info(&s.0.sync_backend);
|
||||||
body.spawn((
|
|
||||||
Text::new(format!("Account: {username} | Backend: {backend_name}")),
|
// 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(),
|
font_row.clone(),
|
||||||
TextColor(TEXT_PRIMARY),
|
TextColor(TEXT_PRIMARY),
|
||||||
));
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if let Some(ss) = sync_status {
|
if let Some(ss) = sync_status {
|
||||||
let status_text = match &ss.0 {
|
let status_text = match &ss.0 {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ use solitaire_data::{
|
|||||||
SyncError,
|
SyncError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::avatar_plugin::AvatarFetchEvent;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
DeleteAccountRequestEvent, InfoToastEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent,
|
DeleteAccountRequestEvent, InfoToastEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent,
|
||||||
SyncLogoutRequestEvent,
|
SyncLogoutRequestEvent,
|
||||||
@@ -135,9 +136,12 @@ impl SyncFocusedField {
|
|||||||
/// In-flight login/register task. `url` and `username` are preserved so the
|
/// In-flight login/register task. `url` and `username` are preserved so the
|
||||||
/// poll system can update settings and provider on success without re-reading
|
/// poll system can update settings and provider on success without re-reading
|
||||||
/// the (already-despawned or cleared) form fields.
|
/// the (already-despawned or cleared) form fields.
|
||||||
|
/// Return type of the async auth + profile-fetch task.
|
||||||
|
type AuthTaskResult = Result<(String, String, Option<String>), SyncError>;
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
struct PendingAuthTask {
|
struct PendingAuthTask {
|
||||||
task: Option<Task<Result<(String, String), SyncError>>>,
|
task: Option<Task<AuthTaskResult>>,
|
||||||
url: String,
|
url: String,
|
||||||
username: String,
|
username: String,
|
||||||
}
|
}
|
||||||
@@ -366,11 +370,18 @@ fn handle_auth_button(
|
|||||||
.build()
|
.build()
|
||||||
.map_err(|e| SyncError::Network(format!("tokio rt: {e}")))?
|
.map_err(|e| SyncError::Network(format!("tokio rt: {e}")))?
|
||||||
.block_on(async {
|
.block_on(async {
|
||||||
if is_register {
|
let (access_token, refresh_token) = if is_register {
|
||||||
client.register(&pw).await
|
client.register(&pw).await?
|
||||||
} else {
|
} else {
|
||||||
client.login(&pw).await
|
client.login(&pw).await?
|
||||||
}
|
};
|
||||||
|
// Fetch avatar URL immediately while we have the fresh token.
|
||||||
|
let avatar_url = client
|
||||||
|
.fetch_me_with_token(&access_token)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|(_, url)| url);
|
||||||
|
Ok((access_token, refresh_token, avatar_url))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -393,6 +404,7 @@ fn poll_auth_task(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut manual_sync: MessageWriter<ManualSyncRequestEvent>,
|
mut manual_sync: MessageWriter<ManualSyncRequestEvent>,
|
||||||
mut toast: MessageWriter<InfoToastEvent>,
|
mut toast: MessageWriter<InfoToastEvent>,
|
||||||
|
mut avatar_fetch: MessageWriter<AvatarFetchEvent>,
|
||||||
) {
|
) {
|
||||||
let Some(task) = pending.task.as_mut() else {
|
let Some(task) = pending.task.as_mut() else {
|
||||||
return;
|
return;
|
||||||
@@ -407,7 +419,7 @@ fn poll_auth_task(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok((access_token, refresh_token)) => {
|
Ok((access_token, refresh_token, fetched_avatar_url)) => {
|
||||||
let url = pending.url.clone();
|
let url = pending.url.clone();
|
||||||
let username = pending.username.clone();
|
let username = pending.username.clone();
|
||||||
|
|
||||||
@@ -424,7 +436,7 @@ fn poll_auth_task(
|
|||||||
settings.0.sync_backend = SyncBackend::SolitaireServer {
|
settings.0.sync_backend = SyncBackend::SolitaireServer {
|
||||||
url: url.clone(),
|
url: url.clone(),
|
||||||
username: username.clone(),
|
username: username.clone(),
|
||||||
avatar_url: None,
|
avatar_url: fetched_avatar_url.clone(),
|
||||||
};
|
};
|
||||||
if let Some(path) = &settings_path.0
|
if let Some(path) = &settings_path.0
|
||||||
&& let Err(e) = save_settings_to(path, &settings.0)
|
&& let Err(e) = save_settings_to(path, &settings.0)
|
||||||
@@ -438,6 +450,14 @@ fn poll_auth_task(
|
|||||||
// Kick off an immediate pull with the new provider.
|
// Kick off an immediate pull with the new provider.
|
||||||
manual_sync.write(ManualSyncRequestEvent);
|
manual_sync.write(ManualSyncRequestEvent);
|
||||||
|
|
||||||
|
// Trigger avatar download if the server reported a profile picture.
|
||||||
|
if let Some(ref rel_url) = fetched_avatar_url {
|
||||||
|
let base = pending.url.trim_end_matches('/').to_string();
|
||||||
|
avatar_fetch.write(AvatarFetchEvent {
|
||||||
|
url: format!("{base}{rel_url}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Close both the setup modal and the settings panel.
|
// Close both the setup modal and the settings panel.
|
||||||
for entity in &screen {
|
for entity in &screen {
|
||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
|
|||||||
Reference in New Issue
Block a user