Add user avatar preloading functionality and image caching service

This commit is contained in:
Данил Ярош 2026-01-16 15:08:49 +03:00
parent e50b799992
commit d7c2f74cc9
12 changed files with 528 additions and 168 deletions

View File

@ -1,5 +1,6 @@
using CORP_MAUI.Pages;
using CORP_MAUI.Pages;
using CORP_MAUI.Models;
using CORP_MAUI.Services;
using System.Diagnostics;
namespace CORP_MAUI;
@ -20,6 +21,9 @@ public partial class App : Application
InitializeComponent();
// Очищаем устаревший кэш изображений
ImageCacheService.Instance.CleanExpiredCache();
Trace.WriteLine("[App] ✅ Application initialized");
}

View File

@ -49,7 +49,24 @@ public class User
{
get
{
if (!string.IsNullOrEmpty(Avatar)) return Avatar;
// Сначала проверяем Avatar (может содержать полный URL или data:image)
if (!string.IsNullOrEmpty(Avatar))
{
// Если это data:image - не возвращаем как URL
if (Avatar.StartsWith("data:image"))
return null;
// Если это полный URL
if (Avatar.StartsWith("http", StringComparison.OrdinalIgnoreCase))
return Avatar;
// Если это относительный путь
if (!Avatar.StartsWith("data:"))
{
string cleanUrl = Avatar.TrimStart('/');
return $"https://corp.robotail.pro/{cleanUrl}";
}
}
if (string.IsNullOrEmpty(AvatarUrl)) return null;
@ -60,40 +77,93 @@ public class User
if (AvatarUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
return AvatarUrl;
string cleanUrl = AvatarUrl.TrimStart('/');
return $"https://corp.robotail.pro/{cleanUrl}";
string cleanPath = AvatarUrl.TrimStart('/');
return $"https://corp.robotail.pro/{cleanPath}";
}
}
/// <summary>
/// Универсальное свойство для отображения аватара (поддерживает Base64 и URL)
/// Кэшированный ImageSource аватара (не создаётся каждый раз)
/// </summary>
[JsonIgnore]
private ImageSource? _cachedImageSource;
/// <summary>
/// Путь к файлу кэшированного аватара
/// </summary>
[JsonIgnore]
public string? CachedAvatarFilePath { get; set; }
/// <summary>
/// Флаг, что аватар был успешно загружен
/// </summary>
[JsonIgnore]
public bool IsAvatarLoaded { get; private set; }
/// <summary>
/// Установить путь к файлу кэшированного аватара
/// </summary>
public void SetCachedAvatarFilePath(string filePath)
{
CachedAvatarFilePath = filePath;
_cachedImageSource = ImageSource.FromFile(filePath);
IsAvatarLoaded = true;
}
/// <summary>
/// Сбросить кэш ImageSource
/// </summary>
public void ResetImageSourceCache()
{
_cachedImageSource = null;
IsAvatarLoaded = false;
CachedAvatarFilePath = null;
}
/// <summary>
/// Универсальное свойство для отображения аватара
/// </summary>
[JsonIgnore]
public ImageSource? DisplayAvatarSource
{
get
{
// Если аватар уже загружен - возвращаем кэшированный
if (IsAvatarLoaded && _cachedImageSource != null)
{
return _cachedImageSource;
}
// Проверяем путь к файлу
if (!string.IsNullOrEmpty(CachedAvatarFilePath) && File.Exists(CachedAvatarFilePath))
{
_cachedImageSource = ImageSource.FromFile(CachedAvatarFilePath);
IsAvatarLoaded = true;
return _cachedImageSource;
}
try
{
// 1. Проверяем, есть ли Base64 изображение в Avatar
if (!string.IsNullOrEmpty(Avatar) && Avatar.StartsWith("data:image"))
{
// Извлекаем Base64 данные из строки "data:image/jpeg;base64,..."
string base64Data = Avatar;
int commaIndex = base64Data.IndexOf(',');
if (commaIndex > 0)
{
base64Data = base64Data.Substring(commaIndex + 1);
byte[] imageBytes = Convert.FromBase64String(base64Data);
return ImageSource.FromStream(() => new MemoryStream(imageBytes));
}
}
// 2. Если есть URL аватара
// Проверяем глобальный файловый кэш
string? url = DisplayAvatarUrl;
if (!string.IsNullOrEmpty(url))
{
return ImageSource.FromUri(new Uri(url));
string? cachedPath = Services.ImageCacheService.Instance.GetCachedFilePath(url);
if (!string.IsNullOrEmpty(cachedPath) && File.Exists(cachedPath))
{
CachedAvatarFilePath = cachedPath;
_cachedImageSource = ImageSource.FromFile(cachedPath);
IsAvatarLoaded = true;
return _cachedImageSource;
}
// Если нет в кэше - используем UriImageSource
return new UriImageSource
{
Uri = new Uri(url),
CachingEnabled = true,
CacheValidity = TimeSpan.FromDays(7)
};
}
}
catch (Exception ex)
@ -101,7 +171,7 @@ public class User
Trace.WriteLine($"[User] ⚠️ Ошибка загрузки аватара для {FullName}: {ex.Message}");
}
// 3. Возвращаем placeholder
// Возвращаем placeholder
return ImageSource.FromFile("dotnet_bot.png");
}
}

View File

@ -36,6 +36,13 @@ public partial class ChatPage : ContentPage
{
try
{
// Загружаем аватар получателя если еще не загружен
if (!_recipient.IsAvatarLoaded)
{
await ServiceProvider.ApiService.LoadUserAvatarAsync(_recipient);
RecipientAvatar.Source = _recipient.DisplayAvatarSource;
}
// Получаем текущего пользователя
User? user = await ServiceProvider.ApiService.GetCurrentUserAsync();
if (user != null)

View File

@ -1,4 +1,4 @@
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Diagnostics;
using CORP_MAUI.Models;
namespace CORP_MAUI.Pages;
@ -73,6 +73,9 @@ public partial class DepartmentDetailsPage : ContentPage
HeadSection.IsVisible = true;
HeadNameLabel.Text = _department.Head.FullName;
HeadPositionLabel.Text = _department.Head.PositionName ?? "Должность не указана";
// Загружаем аватар руководителя
await ServiceProvider.ApiService.LoadUserAvatarAsync(_department.Head);
HeadAvatarImage.Source = _department.Head.DisplayAvatarSource;
}
Trace.WriteLine($"[DepartmentDetailsPage] >>> Вызов GetDepartmentEmployeesAsync({_departmentId})");
@ -96,6 +99,9 @@ public partial class DepartmentDetailsPage : ContentPage
}
}
Trace.WriteLine($"[DepartmentDetailsPage] ✅ Данные загружены успешно. Команда: {_teamMembers.Count} человек");
// Предзагружаем аватары в фоне
_ = PreloadAvatarsAsync();
}
catch (Exception ex)
{
@ -153,4 +159,39 @@ public partial class DepartmentDetailsPage : ContentPage
}
}
}
private async Task PreloadAvatarsAsync()
{
try
{
Trace.WriteLine("[DepartmentDetailsPage] >>> Начинаем предзагрузку аватаров...");
// Загружаем аватар руководителя
if (_department?.Head != null)
{
await ServiceProvider.ApiService.LoadUserAvatarAsync(_department.Head);
}
// Загружаем аватары команды
await ServiceProvider.ApiService.PreloadUserAvatarsAsync(_teamMembers);
// Обновляем UI
await MainThread.InvokeOnMainThreadAsync(() =>
{
TeamCollectionView.ItemsSource = null;
TeamCollectionView.ItemsSource = _teamMembers;
if (_department?.Head != null)
{
HeadAvatarImage.Source = _department.Head.DisplayAvatarSource;
}
});
Trace.WriteLine("[DepartmentDetailsPage] ✅ Аватары предзагружены");
}
catch (Exception ex)
{
Trace.WriteLine($"[DepartmentDetailsPage] ⚠️ Ошибка предзагрузки аватаров: {ex.Message}");
}
}
}

View File

@ -81,6 +81,9 @@ public partial class EmployeeProfilePage : ContentPage
Trace.WriteLine($"[EmployeeProfilePage] ✅ Профиль загружен: {_user.FullName}");
// Предзагружаем аватар пользователя
await ServiceProvider.ApiService.LoadUserAvatarAsync(_user);
// Заполняем основную информацию
FullNameLabel.Text = _user.FullName;
PositionLabel.Text = _user.PositionName ?? "Должность не указана";

View File

@ -77,6 +77,9 @@ public partial class EmployeesPage : ContentPage
}
Trace.WriteLine($"[EmployeesPage] ✅ Сотрудники загружены успешно");
// Предзагружаем аватары в фоне
_ = PreloadAvatarsAsync();
}
catch (Exception ex)
{
@ -198,5 +201,28 @@ public partial class EmployeesPage : ContentPage
base.OnDisappearing();
ServiceProvider.WebSocketService.UserStatusChanged -= OnUserStatusChanged;
}
private async Task PreloadAvatarsAsync()
{
try
{
Trace.WriteLine("[EmployeesPage] >>> Начинаем предзагрузку аватаров...");
await ServiceProvider.ApiService.PreloadUserAvatarsAsync(_allEmployees);
// Обновляем UI - уведомляем об изменении коллекции
await MainThread.InvokeOnMainThreadAsync(() =>
{
// CollectionView автоматически обновится, т.к. ImageSource уже установлен в объектах
EmployeesCollectionView.ItemsSource = null;
EmployeesCollectionView.ItemsSource = _employees;
});
Trace.WriteLine("[EmployeesPage] ✅ Аватары предзагружены");
}
catch (Exception ex)
{
Trace.WriteLine($"[EmployeesPage] ⚠️ Ошибка предзагрузки аватаров: {ex.Message}");
}
}
}

View File

@ -13,6 +13,9 @@ public partial class PostDetailsPage : ContentPage
InitializeComponent();
_viewModel = new PostDetailsViewModel(postViewModel);
BindingContext = _viewModel;
// Предзагружаем аватар автора поста
_ = PreloadPostAuthorAvatarAsync();
}
protected override async void OnAppearing()
@ -38,6 +41,9 @@ public partial class PostDetailsPage : ContentPage
{
_viewModel.Comments.Add(comment);
}
// Предзагружаем аватары авторов комментариев
_ = PreloadCommentAuthorsAvatarsAsync();
}
}
catch (Exception ex)
@ -49,6 +55,50 @@ public partial class PostDetailsPage : ContentPage
_viewModel.IsLoadingComments = false;
}
}
private async Task PreloadPostAuthorAvatarAsync()
{
try
{
var author = _viewModel.PostViewModel.Post.Author;
if (author != null)
{
await ServiceProvider.ApiService.LoadUserAvatarAsync(author);
}
}
catch (Exception ex)
{
Trace.WriteLine($"[PostDetailsPage] Ошибка предзагрузки аватара автора: {ex.Message}");
}
}
private async Task PreloadCommentAuthorsAvatarsAsync()
{
try
{
var authors = _viewModel.Comments
.Where(c => c.Author != null)
.Select(c => c.Author!)
.Distinct()
.ToList();
if (authors.Count > 0)
{
await ServiceProvider.ApiService.PreloadUserAvatarsAsync(authors);
// Обновляем UI - переустанавливаем ItemsSource
await MainThread.InvokeOnMainThreadAsync(() =>
{
CommentsCollectionView.ItemsSource = null;
CommentsCollectionView.ItemsSource = _viewModel.Comments;
});
}
}
catch (Exception ex)
{
Trace.WriteLine($"[PostDetailsPage] Ошибка предзагрузки аватаров комментаторов: {ex.Message}");
}
}
private async void OnCloseClicked(object sender, EventArgs e)
{

View File

@ -40,8 +40,9 @@ public partial class PostsPage
try
{
var user = await ServiceProvider.ApiService.GetCurrentUserAsync();
if (user != null && !string.IsNullOrEmpty(user.Avatar))
if (user != null)
{
await ServiceProvider.ApiService.LoadUserAvatarAsync(user);
UserAvatarImage.Source = user.DisplayAvatarSource;
}
}
@ -217,6 +218,9 @@ public partial class PostsPage
// Применяем фильтр поиска
ApplyFilter();
// Предзагружаем аватары авторов постов в фоне
_ = PreloadPostAuthorsAvatarsAsync(viewModels);
}
}
catch (Exception ex)
@ -232,6 +236,34 @@ public partial class PostsPage
PostsRefreshView.IsRefreshing = false;
}
}
private async Task PreloadPostAuthorsAvatarsAsync(IEnumerable<PostViewModel> posts)
{
try
{
var authors = posts
.Where(p => p.Post.Author != null)
.Select(p => p.Post.Author!)
.Distinct()
.ToList();
if (authors.Count > 0)
{
await ServiceProvider.ApiService.PreloadUserAvatarsAsync(authors);
// Обновляем UI - переустанавливаем ItemsSource
await MainThread.InvokeOnMainThreadAsync(() =>
{
PostsCollectionView.ItemsSource = null;
PostsCollectionView.ItemsSource = _posts;
});
}
}
catch (Exception ex)
{
Trace.WriteLine($"[PostsPage] Ошибка предзагрузки аватаров авторов: {ex.Message}");
}
}
private async void OnRefreshing(object sender, EventArgs e)
{

View File

@ -1393,6 +1393,77 @@ public class ApiService : IDisposable
}
}
/// <summary>
/// Предзагрузить аватары для списка пользователей
/// </summary>
public async Task PreloadUserAvatarsAsync(IEnumerable<User> users)
{
var userList = users.ToList();
var tasks = new List<Task>();
foreach (var user in userList)
{
// Пропускаем, если аватар уже загружен
if (user.IsAvatarLoaded)
{
continue;
}
string? url = user.DisplayAvatarUrl;
if (string.IsNullOrEmpty(url))
{
continue;
}
// Проверяем файловый кэш
string? cachedPath = ImageCacheService.Instance.GetCachedFilePath(url);
if (!string.IsNullOrEmpty(cachedPath) && File.Exists(cachedPath))
{
// Устанавливаем путь к файлу
user.SetCachedAvatarFilePath(cachedPath);
}
else
{
// Добавляем в очередь загрузки
tasks.Add(LoadUserAvatarAsync(user));
}
}
if (tasks.Count > 0)
{
Trace.WriteLine($"[ApiService] Загрузка {tasks.Count} аватаров...");
await Task.WhenAll(tasks);
Trace.WriteLine($"[ApiService] ✅ Аватары загружены");
}
}
/// <summary>
/// Загрузить аватар для одного пользователя (файловый кэш)
/// </summary>
public async Task LoadUserAvatarAsync(User user)
{
try
{
string? url = user.DisplayAvatarUrl;
if (string.IsNullOrEmpty(url))
{
return;
}
// Загружаем через файловый кэш
string? filePath = await ImageCacheService.Instance.GetOrLoadImageFileAsync(url, GetImageBytesAsync);
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
{
user.SetCachedAvatarFilePath(filePath);
}
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] Ошибка предзагрузки аватара для {user.FullName}: {ex.Message}");
}
}
#endregion
#region IDisposable

View File

@ -0,0 +1,185 @@
using System.Collections.Concurrent;
using System.Diagnostics;
namespace CORP_MAUI.Services;
/// <summary>
/// Сервис для кэширования изображений на диске (файловый кэш)
/// </summary>
public class ImageCacheService
{
private static ImageCacheService? _instance;
public static ImageCacheService Instance => _instance ??= new ImageCacheService();
// Кэш путей к файлам (URL -> FilePath)
private readonly ConcurrentDictionary<string, string> _pathCache = new();
// Путь к папке кэша на диске
private readonly string _diskCachePath;
// Максимальное время жизни кэша
private readonly TimeSpan _cacheExpiry = TimeSpan.FromDays(7);
private ImageCacheService()
{
_diskCachePath = Path.Combine(FileSystem.CacheDirectory, "avatars");
// Создаём папку кэша, если не существует
if (!Directory.Exists(_diskCachePath))
{
Directory.CreateDirectory(_diskCachePath);
}
Trace.WriteLine($"[ImageCacheService] Initialized. Cache path: {_diskCachePath}");
}
/// <summary>
/// Получить путь к файлу из кэша или загрузить изображение
/// </summary>
public async Task<string?> GetOrLoadImageFileAsync(string? url, Func<string, Task<byte[]?>>? loader = null)
{
if (string.IsNullOrEmpty(url)) return null;
// 1. Проверяем кэш путей
if (_pathCache.TryGetValue(url, out string? cachedPath) && File.Exists(cachedPath))
{
return cachedPath;
}
// 2. Проверяем файл на диске
string filePath = GetCacheFilePath(url);
if (File.Exists(filePath))
{
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > 0 && fileInfo.LastWriteTime > DateTime.Now - _cacheExpiry)
{
_pathCache[url] = filePath;
return filePath;
}
try { File.Delete(filePath); } catch { }
}
// 3. Загружаем изображение
if (loader != null)
{
try
{
byte[]? imageBytes = await loader(url);
if (imageBytes != null && imageBytes.Length > 0)
{
await File.WriteAllBytesAsync(filePath, imageBytes);
_pathCache[url] = filePath;
return filePath;
}
}
catch (Exception ex)
{
Trace.WriteLine($"[ImageCacheService] Ошибка загрузки {url}: {ex.Message}");
}
}
return null;
}
/// <summary>
/// Получить путь к кэшированному файлу (если существует)
/// </summary>
public string? GetCachedFilePath(string? url)
{
if (string.IsNullOrEmpty(url)) return null;
if (_pathCache.TryGetValue(url, out string? cachedPath) && File.Exists(cachedPath))
{
return cachedPath;
}
string filePath = GetCacheFilePath(url);
if (File.Exists(filePath))
{
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > 0)
{
_pathCache[url] = filePath;
return filePath;
}
}
return null;
}
/// <summary>
/// Проверить, есть ли изображение в кэше
/// </summary>
public bool IsCached(string? url)
{
if (string.IsNullOrEmpty(url)) return false;
if (_pathCache.TryGetValue(url, out string? path) && File.Exists(path))
return true;
string filePath = GetCacheFilePath(url);
return File.Exists(filePath);
}
/// <summary>
/// Получить путь к файлу кэша для URL
/// </summary>
private string GetCacheFilePath(string url)
{
using var md5 = System.Security.Cryptography.MD5.Create();
byte[] hashBytes = md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(url));
string hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
return Path.Combine(_diskCachePath, $"{hash}.jpg");
}
// Для обратной совместимости
public string? GetFromCache(string? url) => GetCachedFilePath(url);
/// <summary>
/// Очистить устаревший кэш
/// </summary>
public void CleanExpiredCache()
{
try
{
if (!Directory.Exists(_diskCachePath)) return;
var expiredFiles = Directory.GetFiles(_diskCachePath)
.Where(f => File.GetLastWriteTime(f) < DateTime.Now - _cacheExpiry)
.ToList();
foreach (var file in expiredFiles)
{
try { File.Delete(file); } catch { }
}
if (expiredFiles.Count > 0)
{
Trace.WriteLine($"[ImageCacheService] Удалено {expiredFiles.Count} устаревших файлов");
}
}
catch (Exception ex)
{
Trace.WriteLine($"[ImageCacheService] Ошибка очистки: {ex.Message}");
}
}
/// <summary>
/// Очистить весь кэш
/// </summary>
public void ClearCache()
{
_pathCache.Clear();
try
{
if (Directory.Exists(_diskCachePath))
{
foreach (var file in Directory.GetFiles(_diskCachePath))
{
try { File.Delete(file); } catch { }
}
}
}
catch { }
}
}

View File

@ -62,6 +62,9 @@ public class WebSocketService
_webSocket = new ClientWebSocket();
// Настройка для Android - увеличиваем таймаут и буферы
_webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
// 🔑 Устанавливаем Authorization заголовок
Trace.WriteLine($"[WebSocket] 🔑 Setting Authorization header...");
Trace.WriteLine($"[WebSocket] Token (first 30 chars): {authToken.Substring(0, Math.Min(30, authToken.Length))}...");
@ -70,6 +73,7 @@ public class WebSocketService
Trace.WriteLine($"[WebSocket] ✅ Authorization header set: Bearer [token]");
_cancellationTokenSource = new CancellationTokenSource();
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(15)); // Таймаут подключения
Uri uri = new Uri(url);
Trace.WriteLine($"[WebSocket] Connecting to: {uri}");

View File

@ -12,7 +12,6 @@ public class ProfileViewModel : INotifyPropertyChanged
{
private User? _user;
private bool _isLoading;
private string? _localImagePath;
public ProfileViewModel()
{
@ -67,53 +66,10 @@ public class ProfileViewModel : INotifyPropertyChanged
{
get
{
// Сначала проверяем, есть ли Base64 изображение в User.Avatar
if (_user != null && !string.IsNullOrEmpty(_user.Avatar))
// Используем DisplayAvatarSource из модели User (работает с файловым кэшем)
if (_user != null)
{
try
{
// Проверяем, это Base64 строка?
if (_user.Avatar.StartsWith("data:image"))
{
// Извлекаем Base64 данные из строки "data:image/jpeg;base64,..."
string base64Data = _user.Avatar;
int commaIndex = base64Data.IndexOf(',');
if (commaIndex > 0)
{
base64Data = base64Data.Substring(commaIndex + 1);
byte[] imageBytes = Convert.FromBase64String(base64Data);
return ImageSource.FromStream(() => new MemoryStream(imageBytes));
}
}
}
catch (Exception ex)
{
Trace.WriteLine($"[ProfileViewModel] ⚠️ Ошибка декодирования Base64 аватара: {ex.Message}");
}
}
// Если есть локальный файл кеша
if (!string.IsNullOrEmpty(_localImagePath) && File.Exists(_localImagePath))
{
try
{
FileInfo fileInfo = new FileInfo(_localImagePath);
if (fileInfo.Length > 0)
{
return ImageSource.FromStream(() =>
{
try
{
return new FileStream(_localImagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
catch
{
return null;
}
});
}
}
catch { }
return _user.DisplayAvatarSource;
}
// Default placeholder
@ -179,112 +135,23 @@ public class ProfileViewModel : INotifyPropertyChanged
private async Task LoadAvatarAsync()
{
// Если аватар уже в виде Base64 в User.Avatar, ничего не делаем
if (_user != null && !string.IsNullOrEmpty(_user.Avatar) && _user.Avatar.StartsWith("data:image"))
if (_user == null)
{
Trace.WriteLine("[ProfileViewModel] ✅ Аватар уже загружен как Base64, пропускаем загрузку");
return;
}
if (_user == null || string.IsNullOrEmpty(_user.DisplayAvatarUrl))
{
Trace.WriteLine("[ProfileViewModel] Нет URL аватара для загрузки");
Trace.WriteLine("[ProfileViewModel] Нет пользователя для загрузки аватара");
return;
}
try
{
string url = _user.DisplayAvatarUrl;
Trace.WriteLine($"[ProfileViewModel] Загрузка аватара с URL: {url}");
// Generate cache filename - безопасное создание Uri
Uri? uri = null;
try
{
uri = new Uri(url);
}
catch (UriFormatException uriEx)
{
Trace.WriteLine($"[ProfileViewModel] ⚠️ Некорректный URL аватара: {uriEx.Message}");
return;
}
string extension = Path.GetExtension(uri.LocalPath);
if (string.IsNullOrEmpty(extension)) extension = ".jpg";
string fileName = Path.GetFileNameWithoutExtension(uri.LocalPath);
if (string.IsNullOrEmpty(fileName)) fileName = $"avatar_{_user.Id}";
// Заменяем все недопустимые символы
fileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c, '_'));
string cacheFileName = $"user_{_user.Id}_{fileName}{extension}";
string cacheFilePath = Path.Combine(FileSystem.CacheDirectory, cacheFileName);
Trace.WriteLine($"[ProfileViewModel] Путь кеша: {cacheFilePath}");
// Check cache
if (File.Exists(cacheFilePath))
{
FileInfo fileInfo = new FileInfo(cacheFilePath);
if (fileInfo.Length > 0)
{
Trace.WriteLine($"[ProfileViewModel] ✅ Аватар найден в кеше ({fileInfo.Length} bytes)");
_localImagePath = cacheFilePath;
OnPropertyChanged(nameof(DisplayImageSource));
return;
}
else
{
Trace.WriteLine($"[ProfileViewModel] ⚠️ Файл кеша пуст, удаляем");
try { File.Delete(cacheFilePath); } catch { }
}
}
// Download
Trace.WriteLine($"[ProfileViewModel] Загрузка аватара с сервера...");
byte[]? bytes = await ServiceProvider.ApiService.GetImageBytesAsync(url);
if (bytes is { Length: > 0 })
{
Trace.WriteLine($"[ProfileViewModel] Получено {bytes.Length} bytes");
// Simple signature check
bool isImage = false;
if (bytes.Length > 2)
{
if (bytes[0] == 0xFF && bytes[1] == 0xD8) isImage = true; // JPEG
else if (bytes[0] == 0x89 && bytes[1] == 0x50) isImage = true; // PNG
else isImage = true; // Relaxed check
if (bytes[0] == '<') isImage = false; // HTML check
}
if (isImage)
{
Trace.WriteLine($"[ProfileViewModel] ✅ Данные похожи на изображение, сохраняем в кеш");
await File.WriteAllBytesAsync(cacheFilePath, bytes);
_localImagePath = cacheFilePath;
OnPropertyChanged(nameof(DisplayImageSource));
Trace.WriteLine($"[ProfileViewModel] ✅ Аватар сохранён и отображён");
}
else
{
Trace.WriteLine($"[ProfileViewModel] ⚠️ Данные не похожи на изображение");
}
}
else
{
Trace.WriteLine($"[ProfileViewModel] ⚠️ Не удалось загрузить данные аватара (пустой ответ)");
}
// Используем централизованный метод загрузки аватара
await ServiceProvider.ApiService.LoadUserAvatarAsync(_user);
OnPropertyChanged(nameof(DisplayImageSource));
Trace.WriteLine("[ProfileViewModel] ✅ Аватар загружен");
}
catch (Exception ex)
{
Trace.WriteLine($"[ProfileViewModel] ❌ Ошибка загрузки аватара:");
Trace.WriteLine($" Type: {ex.GetType().Name}");
Trace.WriteLine($" Message: {ex.Message}");
Trace.WriteLine($" StackTrace: {ex.StackTrace}");
// Не пробрасываем исключение дальше - ошибка загрузки аватара не критична
Trace.WriteLine($"[ProfileViewModel] ❌ Ошибка загрузки аватара: {ex.Message}");
// Ошибка загрузки аватара не критична
}
}