324 lines
13 KiB
C#
324 lines
13 KiB
C#
using System.ComponentModel;
|
||
using System.Diagnostics;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Windows.Input;
|
||
using CORP_MAUI.Models;
|
||
using CORP_MAUI.Pages;
|
||
using CORP_MAUI.Services;
|
||
|
||
namespace CORP_MAUI.ViewModels;
|
||
|
||
public class ProfileViewModel : INotifyPropertyChanged
|
||
{
|
||
private User? _user;
|
||
private bool _isLoading;
|
||
private string? _localImagePath;
|
||
|
||
public ProfileViewModel()
|
||
{
|
||
LoadProfileCommand = new Command(async () => await LoadProfileAsync());
|
||
LogoutCommand = new Command(async () => await LogoutAsync());
|
||
}
|
||
|
||
public User? User
|
||
{
|
||
get => _user;
|
||
set
|
||
{
|
||
_user = value;
|
||
OnPropertyChanged();
|
||
OnPropertyChanged(nameof(FullName));
|
||
OnPropertyChanged(nameof(Username));
|
||
OnPropertyChanged(nameof(Email));
|
||
OnPropertyChanged(nameof(Position));
|
||
OnPropertyChanged(nameof(Department));
|
||
OnPropertyChanged(nameof(StatusText));
|
||
OnPropertyChanged(nameof(StatusColor));
|
||
OnPropertyChanged(nameof(Skills));
|
||
OnPropertyChanged(nameof(HasSkills));
|
||
OnPropertyChanged(nameof(DisplayImageSource)); // Обновляем изображение при изменении пользователя
|
||
}
|
||
}
|
||
|
||
public bool IsLoading
|
||
{
|
||
get => _isLoading;
|
||
set
|
||
{
|
||
_isLoading = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
// Wrappers for UI binding
|
||
public string FullName => _user?.FullName ?? "Загрузка...";
|
||
public string Username => _user != null ? $"@{_user.Username}" : "";
|
||
public string Email => _user?.Email ?? "-";
|
||
public string Position => _user?.PositionName ?? "-";
|
||
public string Department => _user?.DepartmentName ?? "-";
|
||
public bool IsOnline => _user?.IsOnline ?? false;
|
||
public string StatusText => IsOnline ? "В сети" : "Не в сети";
|
||
public Color StatusColor => IsOnline ? Colors.LimeGreen : Colors.Gray;
|
||
public List<Skill> Skills => _user?.Skills ?? new List<Skill>();
|
||
public bool HasSkills => Skills.Any();
|
||
|
||
// Image Logic
|
||
public ImageSource? DisplayImageSource
|
||
{
|
||
get
|
||
{
|
||
// Сначала проверяем, есть ли Base64 изображение в User.Avatar
|
||
if (_user != null && !string.IsNullOrEmpty(_user.Avatar))
|
||
{
|
||
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 { }
|
||
}
|
||
|
||
// Default placeholder
|
||
return ImageSource.FromFile("dotnet_bot.png");
|
||
}
|
||
}
|
||
|
||
public ICommand LoadProfileCommand { get; }
|
||
public ICommand LogoutCommand { get; }
|
||
|
||
public async Task LoadProfileAsync()
|
||
{
|
||
if (IsLoading) return;
|
||
IsLoading = true;
|
||
|
||
try
|
||
{
|
||
Trace.WriteLine("[ProfileViewModel] >>> Начало загрузки профиля");
|
||
User? loadedUser = await ServiceProvider.ApiService.GetCurrentUserAsync();
|
||
|
||
if (loadedUser != null)
|
||
{
|
||
Trace.WriteLine($"[ProfileViewModel] ✅ Пользователь загружен: {loadedUser.FullName}");
|
||
User = loadedUser;
|
||
|
||
try
|
||
{
|
||
Trace.WriteLine("[ProfileViewModel] >>> Загрузка аватара");
|
||
await LoadAvatarAsync();
|
||
Trace.WriteLine("[ProfileViewModel] ✅ Аватар загружен успешно");
|
||
}
|
||
catch (Exception avatarEx)
|
||
{
|
||
Trace.WriteLine($"[ProfileViewModel] ⚠️ Ошибка загрузки аватара (не критично): {avatarEx.Message}");
|
||
// Ошибка загрузки аватара не критична - продолжаем работу
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine("[ProfileViewModel] ❌ loadedUser == null");
|
||
Page? mainPage = Application.Current?.Windows.FirstOrDefault()?.Page;
|
||
if (mainPage != null)
|
||
await mainPage.DisplayAlert("Ошибка", "Не удалось загрузить профиль", "OK");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ProfileViewModel] ❌ Исключение при загрузке профиля:");
|
||
Trace.WriteLine($" Type: {ex.GetType().Name}");
|
||
Trace.WriteLine($" Message: {ex.Message}");
|
||
Trace.WriteLine($" StackTrace: {ex.StackTrace}");
|
||
|
||
Page? mainPage = Application.Current?.Windows.FirstOrDefault()?.Page;
|
||
if (mainPage != null)
|
||
await mainPage.DisplayAlert("Ошибка", $"Не удалось загрузить профиль: {ex.Message}", "OK");
|
||
}
|
||
finally
|
||
{
|
||
IsLoading = false;
|
||
Trace.WriteLine("[ProfileViewModel] <<< Завершение загрузки профиля");
|
||
}
|
||
}
|
||
|
||
private async Task LoadAvatarAsync()
|
||
{
|
||
// Если аватар уже в виде Base64 в User.Avatar, ничего не делаем
|
||
if (_user != null && !string.IsNullOrEmpty(_user.Avatar) && _user.Avatar.StartsWith("data:image"))
|
||
{
|
||
Trace.WriteLine("[ProfileViewModel] ✅ Аватар уже загружен как Base64, пропускаем загрузку");
|
||
return;
|
||
}
|
||
|
||
if (_user == null || string.IsNullOrEmpty(_user.DisplayAvatarUrl))
|
||
{
|
||
Trace.WriteLine("[ProfileViewModel] Нет URL аватара для загрузки");
|
||
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] ⚠️ Не удалось загрузить данные аватара (пустой ответ)");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ProfileViewModel] ❌ Ошибка загрузки аватара:");
|
||
Trace.WriteLine($" Type: {ex.GetType().Name}");
|
||
Trace.WriteLine($" Message: {ex.Message}");
|
||
Trace.WriteLine($" StackTrace: {ex.StackTrace}");
|
||
// Не пробрасываем исключение дальше - ошибка загрузки аватара не критична
|
||
}
|
||
}
|
||
|
||
private async Task LogoutAsync()
|
||
{
|
||
Page? mainPage = Application.Current?.Windows.FirstOrDefault()?.Page;
|
||
if (mainPage == null) return;
|
||
|
||
bool result = await mainPage.DisplayAlert("Выход", "Вы уверены, что хотите выйти?", "Да", "Нет");
|
||
if (result)
|
||
{
|
||
try
|
||
{
|
||
await ServiceProvider.ApiService.UpdateStatusAsync(false);
|
||
await ServiceProvider.WebSocketService.DisconnectAsync();
|
||
ServiceProvider.ApiService.Logout();
|
||
SecureStorage.Remove("user_id");
|
||
SecureStorage.Remove("username");
|
||
|
||
if (Application.Current?.Windows.Count > 0)
|
||
{
|
||
Application.Current.Windows[0].Page = new NavigationPage(new LoginPage());
|
||
}
|
||
}
|
||
catch(Exception ex)
|
||
{
|
||
await mainPage.DisplayAlert("Ошибка", $"Ошибка при выходе: {ex.Message}", "OK");
|
||
}
|
||
}
|
||
}
|
||
|
||
public event PropertyChangedEventHandler? PropertyChanged;
|
||
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
|
||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||
}
|
||
|