diff --git a/CORP-MAUI/App.xaml.cs b/CORP-MAUI/App.xaml.cs
index aa83c80..6179d15 100644
--- a/CORP-MAUI/App.xaml.cs
+++ b/CORP-MAUI/App.xaml.cs
@@ -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");
}
diff --git a/CORP-MAUI/Models/User.cs b/CORP-MAUI/Models/User.cs
index 8bbb1f4..2077725 100644
--- a/CORP-MAUI/Models/User.cs
+++ b/CORP-MAUI/Models/User.cs
@@ -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}";
}
}
///
- /// Универсальное свойство для отображения аватара (поддерживает Base64 и URL)
+ /// Кэшированный ImageSource аватара (не создаётся каждый раз)
+ ///
+ [JsonIgnore]
+ private ImageSource? _cachedImageSource;
+
+ ///
+ /// Путь к файлу кэшированного аватара
+ ///
+ [JsonIgnore]
+ public string? CachedAvatarFilePath { get; set; }
+
+ ///
+ /// Флаг, что аватар был успешно загружен
+ ///
+ [JsonIgnore]
+ public bool IsAvatarLoaded { get; private set; }
+
+ ///
+ /// Установить путь к файлу кэшированного аватара
+ ///
+ public void SetCachedAvatarFilePath(string filePath)
+ {
+ CachedAvatarFilePath = filePath;
+ _cachedImageSource = ImageSource.FromFile(filePath);
+ IsAvatarLoaded = true;
+ }
+
+ ///
+ /// Сбросить кэш ImageSource
+ ///
+ public void ResetImageSourceCache()
+ {
+ _cachedImageSource = null;
+ IsAvatarLoaded = false;
+ CachedAvatarFilePath = null;
+ }
+
+ ///
+ /// Универсальное свойство для отображения аватара
///
[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");
}
}
diff --git a/CORP-MAUI/Pages/ChatPage.xaml.cs b/CORP-MAUI/Pages/ChatPage.xaml.cs
index a7b8c04..b125492 100644
--- a/CORP-MAUI/Pages/ChatPage.xaml.cs
+++ b/CORP-MAUI/Pages/ChatPage.xaml.cs
@@ -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)
diff --git a/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs b/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs
index 4e00c35..3b6e4a5 100644
--- a/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs
+++ b/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs
@@ -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}");
+ }
+ }
}
diff --git a/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs b/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
index 4c70003..548ebc4 100644
--- a/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
+++ b/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
@@ -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 ?? "Должность не указана";
diff --git a/CORP-MAUI/Pages/EmployeesPage.xaml.cs b/CORP-MAUI/Pages/EmployeesPage.xaml.cs
index 309ea2c..291a924 100644
--- a/CORP-MAUI/Pages/EmployeesPage.xaml.cs
+++ b/CORP-MAUI/Pages/EmployeesPage.xaml.cs
@@ -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}");
+ }
+ }
}
diff --git a/CORP-MAUI/Pages/PostDetailsPage.xaml.cs b/CORP-MAUI/Pages/PostDetailsPage.xaml.cs
index d06ed46..c9b0c06 100644
--- a/CORP-MAUI/Pages/PostDetailsPage.xaml.cs
+++ b/CORP-MAUI/Pages/PostDetailsPage.xaml.cs
@@ -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)
{
diff --git a/CORP-MAUI/Pages/PostsPage.xaml.cs b/CORP-MAUI/Pages/PostsPage.xaml.cs
index 51a1256..115aaa4 100644
--- a/CORP-MAUI/Pages/PostsPage.xaml.cs
+++ b/CORP-MAUI/Pages/PostsPage.xaml.cs
@@ -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 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)
{
diff --git a/CORP-MAUI/Services/ApiService.cs b/CORP-MAUI/Services/ApiService.cs
index f221e64..24dbb92 100644
--- a/CORP-MAUI/Services/ApiService.cs
+++ b/CORP-MAUI/Services/ApiService.cs
@@ -1393,6 +1393,77 @@ public class ApiService : IDisposable
}
}
+ ///
+ /// Предзагрузить аватары для списка пользователей
+ ///
+ public async Task PreloadUserAvatarsAsync(IEnumerable users)
+ {
+ var userList = users.ToList();
+ var tasks = new List();
+
+ 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] ✅ Аватары загружены");
+ }
+ }
+
+ ///
+ /// Загрузить аватар для одного пользователя (файловый кэш)
+ ///
+ 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
diff --git a/CORP-MAUI/Services/ImageCacheService.cs b/CORP-MAUI/Services/ImageCacheService.cs
new file mode 100644
index 0000000..d0d8436
--- /dev/null
+++ b/CORP-MAUI/Services/ImageCacheService.cs
@@ -0,0 +1,185 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+
+namespace CORP_MAUI.Services;
+
+///
+/// Сервис для кэширования изображений на диске (файловый кэш)
+///
+public class ImageCacheService
+{
+ private static ImageCacheService? _instance;
+ public static ImageCacheService Instance => _instance ??= new ImageCacheService();
+
+ // Кэш путей к файлам (URL -> FilePath)
+ private readonly ConcurrentDictionary _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}");
+ }
+
+ ///
+ /// Получить путь к файлу из кэша или загрузить изображение
+ ///
+ public async Task GetOrLoadImageFileAsync(string? url, Func>? 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;
+ }
+
+ ///
+ /// Получить путь к кэшированному файлу (если существует)
+ ///
+ 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;
+ }
+
+ ///
+ /// Проверить, есть ли изображение в кэше
+ ///
+ 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);
+ }
+
+ ///
+ /// Получить путь к файлу кэша для URL
+ ///
+ 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);
+
+ ///
+ /// Очистить устаревший кэш
+ ///
+ 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}");
+ }
+ }
+
+ ///
+ /// Очистить весь кэш
+ ///
+ public void ClearCache()
+ {
+ _pathCache.Clear();
+ try
+ {
+ if (Directory.Exists(_diskCachePath))
+ {
+ foreach (var file in Directory.GetFiles(_diskCachePath))
+ {
+ try { File.Delete(file); } catch { }
+ }
+ }
+ }
+ catch { }
+ }
+}
diff --git a/CORP-MAUI/Services/WebSocketService.cs b/CORP-MAUI/Services/WebSocketService.cs
index 9bbfc18..fe6b710 100644
--- a/CORP-MAUI/Services/WebSocketService.cs
+++ b/CORP-MAUI/Services/WebSocketService.cs
@@ -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}");
diff --git a/CORP-MAUI/ViewModels/ProfileViewModel.cs b/CORP-MAUI/ViewModels/ProfileViewModel.cs
index 44f3a6c..f767c19 100644
--- a/CORP-MAUI/ViewModels/ProfileViewModel.cs
+++ b/CORP-MAUI/ViewModels/ProfileViewModel.cs
@@ -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}");
+ // Ошибка загрузки аватара не критична
}
}