using System.Text.Json; using System.Text.Json.Serialization; using System.Diagnostics; using System.ComponentModel; using System.Runtime.CompilerServices; using Microsoft.Maui.Controls; namespace CORP_MAUI.Models; public class User : INotifyPropertyChanged { public int Id { get; set; } public string Username { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; [JsonPropertyName("phone_number")] public string? PhoneNumber { get; set; } [JsonPropertyName("first_name")] public string FirstName { get; set; } = string.Empty; [JsonPropertyName("last_name")] public string LastName { get; set; } = string.Empty; [JsonPropertyName("departments")] public List? Departments { get; set; } // Может быть либо объектом Department, либо строкой (для обратной совместимости) public object? Department { get; set; } // Может быть либо объектом Position, либо строкой (для обратной совместимости) public object? Position { get; set; } public List? Skills { get; set; } [JsonPropertyName("is_online")] public bool IsOnline { get; set; } [JsonPropertyName("avatar_url")] public string? AvatarUrl { get; set; } [JsonPropertyName("avatar")] public string? Avatar { get; set; } // INotifyPropertyChanged implementation public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // Вспомогательные свойства для удобства [JsonIgnore] public string FullName => $"{FirstName} {LastName}".Trim(); /// /// Проверяет, является ли аватар Base64 строкой /// [JsonIgnore] public bool IsAvatarBase64 => !string.IsNullOrEmpty(Avatar) && Avatar.StartsWith("data:image"); /// /// Получить байты изображения из Base64 строки /// public byte[]? GetAvatarBytesFromBase64() { if (!IsAvatarBase64) return null; try { // Формат: data:image/jpeg;base64, int commaIndex = Avatar!.IndexOf(','); if (commaIndex < 0) return null; string base64Data = Avatar.Substring(commaIndex + 1); return Convert.FromBase64String(base64Data); } catch (Exception ex) { Trace.WriteLine($"[User] Ошибка декодирования Base64 аватара: {ex.Message}"); return null; } } [JsonIgnore] public string? DisplayAvatarUrl { get { // Сначала проверяем 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; // Handle protocol-relative URLs if (AvatarUrl.StartsWith("//")) return $"https:{AvatarUrl}"; if (AvatarUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return AvatarUrl; string cleanPath = AvatarUrl.TrimStart('/'); return $"https://corp.robotail.pro/{cleanPath}"; } } /// /// Кэшированный ImageSource аватара (не создаётся каждый раз) /// [JsonIgnore] private ImageSource? _cachedImageSource; /// /// Путь к файлу кэшированного аватара /// [JsonIgnore] public string? CachedAvatarFilePath { get; set; } /// /// Флаг, что аватар был успешно загружен /// [JsonIgnore] public bool IsAvatarLoaded { get; private set; } /// /// Установить путь к файлу кэшированного аватара /// public void SetCachedAvatarFilePath(string filePath) { Trace.WriteLine($"[User.SetCachedAvatarFilePath] {FullName}: Setting path to {filePath}"); CachedAvatarFilePath = filePath; _cachedImageSource = ImageSource.FromFile(filePath); IsAvatarLoaded = true; Trace.WriteLine($"[User.SetCachedAvatarFilePath] {FullName}: ImageSource created, notifying UI..."); // Уведомляем UI об изменении аватара MainThread.BeginInvokeOnMainThread(() => { Trace.WriteLine($"[User.SetCachedAvatarFilePath] {FullName}: OnPropertyChanged(DisplayAvatarSource)"); OnPropertyChanged(nameof(DisplayAvatarSource)); }); } /// /// Сбросить кэш ImageSource /// public void ResetImageSourceCache() { _cachedImageSource = null; IsAvatarLoaded = false; CachedAvatarFilePath = null; OnPropertyChanged(nameof(DisplayAvatarSource)); } /// /// Универсальное свойство для отображения аватара /// [JsonIgnore] public ImageSource? DisplayAvatarSource { get { Trace.WriteLine($"[User.DisplayAvatarSource] {FullName}: IsAvatarLoaded={IsAvatarLoaded}, CachedPath={CachedAvatarFilePath}"); // Если аватар уже загружен - возвращаем кэшированный if (IsAvatarLoaded && _cachedImageSource != null) { Trace.WriteLine($"[User.DisplayAvatarSource] {FullName}: Returning cached ImageSource"); return _cachedImageSource; } // Проверяем путь к файлу if (!string.IsNullOrEmpty(CachedAvatarFilePath) && File.Exists(CachedAvatarFilePath)) { Trace.WriteLine($"[User.DisplayAvatarSource] {FullName}: Loading from file: {CachedAvatarFilePath}"); _cachedImageSource = ImageSource.FromFile(CachedAvatarFilePath); IsAvatarLoaded = true; return _cachedImageSource; } try { // Проверяем глобальный файловый кэш string? url = DisplayAvatarUrl; if (!string.IsNullOrEmpty(url)) { string? cachedPath = Services.ImageCacheService.Instance.GetCachedFilePath(url); if (!string.IsNullOrEmpty(cachedPath) && File.Exists(cachedPath)) { Trace.WriteLine($"[User.DisplayAvatarSource] {FullName}: Found in global cache: {cachedPath}"); CachedAvatarFilePath = cachedPath; _cachedImageSource = ImageSource.FromFile(cachedPath); IsAvatarLoaded = true; return _cachedImageSource; } // Если нет в кэше - возвращаем placeholder // Аватар будет загружен асинхронно через LoadUserAvatarAsync Trace.WriteLine($"[User.DisplayAvatarSource] {FullName}: Not in cache, returning placeholder"); } } catch (Exception ex) { Trace.WriteLine($"[User] ⚠️ Ошибка загрузки аватара для {FullName}: {ex.Message}"); } // Возвращаем placeholder return ImageSource.FromFile("dotnet_bot.png"); } } [JsonIgnore] public string DepartmentName { get { if (Departments != null && Departments.Count > 0) return Departments[0].Name; if (Department is Department dept) return dept.Name; if (Department is string str) return str; if (Department is JsonElement element) { if (element.ValueKind == JsonValueKind.String) return element.GetString() ?? string.Empty; if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty("name", out JsonElement nameProp)) return nameProp.GetString() ?? string.Empty; } return string.Empty; } } [JsonIgnore] public string PositionName { get { if (Position is Position pos) return pos.Name; if (Position is string str) return str; if (Position is JsonElement element) { if (element.ValueKind == JsonValueKind.String) return element.GetString() ?? string.Empty; if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty("name", out JsonElement nameProp)) return nameProp.GetString() ?? string.Empty; } return string.Empty; } } }