diff --git a/CORP-MAUI/App.xaml.cs b/CORP-MAUI/App.xaml.cs
index 6179d15..2646859 100644
--- a/CORP-MAUI/App.xaml.cs
+++ b/CORP-MAUI/App.xaml.cs
@@ -1,4 +1,4 @@
-using CORP_MAUI.Pages;
+using CORP_MAUI.Pages;
using CORP_MAUI.Models;
using CORP_MAUI.Services;
using System.Diagnostics;
diff --git a/CORP-MAUI/AppShell.xaml b/CORP-MAUI/AppShell.xaml
index 088f691..4581b27 100644
--- a/CORP-MAUI/AppShell.xaml
+++ b/CORP-MAUI/AppShell.xaml
@@ -4,33 +4,79 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:CORP_MAUI.Pages"
+ xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
Shell.TabBarBackgroundColor="#0D6EFD"
Shell.TabBarForegroundColor="White"
Shell.TabBarTitleColor="White"
Shell.TabBarUnselectedColor="#FFFFFF99"
- Shell.NavBarIsVisible="False">
+ FlyoutBackgroundColor="#0D6EFD"
+ FlyoutWidth="220"
+ Shell.NavBarIsVisible="False"
+ x:Name="MainShell"
+ FlyoutBehavior="Flyout">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/AppShell.xaml.cs b/CORP-MAUI/AppShell.xaml.cs
index fb24165..b02da94 100644
--- a/CORP-MAUI/AppShell.xaml.cs
+++ b/CORP-MAUI/AppShell.xaml.cs
@@ -5,6 +5,9 @@ namespace CORP_MAUI;
public partial class AppShell : Shell
{
+ // Порог ширины для переключения на вертикальное меню (в пикселях)
+ private const double WideScreenThreshold = 768;
+
public AppShell()
{
try
@@ -25,6 +28,9 @@ public partial class AppShell : Shell
Trace.WriteLine("[AppShell] ✅ AppShell инициализирован");
Trace.WriteLine("=== APPSHELL CONSTRUCTOR END ===");
+ // Устанавливаем начальный режим меню
+ UpdateFlyoutBehavior();
+
// Инициализация без ожидания (так await нельзя из синхронного конструктора)
_ = InitializeAsync();
}
@@ -46,7 +52,7 @@ public partial class AppShell : Shell
await Task.Delay(100);
await MainThread.InvokeOnMainThreadAsync(async () =>
{
- Page? mainPage = Application.Current?.Windows[0]?.Page;
+ Page? mainPage = Application.Current?.Windows.FirstOrDefault()?.Page;
if (mainPage != null)
{
await mainPage.DisplayAlert("Ошибка AppShell",
@@ -106,6 +112,55 @@ public partial class AppShell : Shell
await DisplayAlert("Ошибка", $"Ошибка инициализации: {ex.Message}", "OK");
}
}
+
+ protected override void OnSizeAllocated(double width, double height)
+ {
+ base.OnSizeAllocated(width, height);
+ UpdateFlyoutBehavior(width);
+ }
+
+ ///
+ /// Обновляет режим меню в зависимости от ширины экрана
+ ///
+ private void UpdateFlyoutBehavior(double? width = null)
+ {
+ try
+ {
+ // Получаем текущую ширину окна
+ double currentWidth = width ?? (Application.Current?.Windows.FirstOrDefault()?.Width ?? 0);
+
+ if (currentWidth <= 0)
+ {
+ // Если ширина не определена, используем DeviceDisplay
+ currentWidth = DeviceDisplay.MainDisplayInfo.Width / DeviceDisplay.MainDisplayInfo.Density;
+ }
+
+ Trace.WriteLine($"[AppShell] UpdateFlyoutBehavior: width={currentWidth}, threshold={WideScreenThreshold}");
+
+ if (currentWidth >= WideScreenThreshold)
+ {
+ // Широкий экран - Flyout всегда открыт (Locked)
+ if (FlyoutBehavior != FlyoutBehavior.Locked)
+ {
+ FlyoutBehavior = FlyoutBehavior.Locked;
+ FlyoutIsPresented = true;
+ Trace.WriteLine("[AppShell] ✅ Режим: Locked (вертикальное меню слева)");
+ }
+ }
+ else
+ {
+ // Узкий экран - Flyout выдвигается по кнопке/свайпу
+ if (FlyoutBehavior != FlyoutBehavior.Flyout)
+ {
+ FlyoutBehavior = FlyoutBehavior.Flyout;
+ FlyoutIsPresented = false;
+ Trace.WriteLine("[AppShell] ✅ Режим: Flyout (выдвижное меню)");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[AppShell] ⚠️ Ошибка UpdateFlyoutBehavior: {ex.Message}");
+ }
+ }
}
-
-
diff --git a/CORP-MAUI/CORP-MAUI.csproj b/CORP-MAUI/CORP-MAUI.csproj
index e8e6dda..789c68e 100644
--- a/CORP-MAUI/CORP-MAUI.csproj
+++ b/CORP-MAUI/CORP-MAUI.csproj
@@ -53,7 +53,6 @@
-
diff --git a/CORP-MAUI/Models/User.cs b/CORP-MAUI/Models/User.cs
index 2077725..34fa239 100644
--- a/CORP-MAUI/Models/User.cs
+++ b/CORP-MAUI/Models/User.cs
@@ -1,11 +1,13 @@
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
+public class User : INotifyPropertyChanged
{
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
@@ -40,10 +42,47 @@ public class User
[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
{
@@ -52,7 +91,7 @@ public class User
// Сначала проверяем Avatar (может содержать полный URL или data:image)
if (!string.IsNullOrEmpty(Avatar))
{
- // Если это data:image - не возвращаем как URL
+ // Если это data:image - не возвращаем как URL (будет обработано отдельно)
if (Avatar.StartsWith("data:image"))
return null;
@@ -105,9 +144,20 @@ public class User
///
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));
+ });
}
///
@@ -118,6 +168,7 @@ public class User
_cachedImageSource = null;
IsAvatarLoaded = false;
CachedAvatarFilePath = null;
+ OnPropertyChanged(nameof(DisplayAvatarSource));
}
///
@@ -128,15 +179,19 @@ public class User
{
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;
@@ -151,19 +206,16 @@ public class User
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;
}
- // Если нет в кэше - используем UriImageSource
- return new UriImageSource
- {
- Uri = new Uri(url),
- CachingEnabled = true,
- CacheValidity = TimeSpan.FromDays(7)
- };
+ // Если нет в кэше - возвращаем placeholder
+ // Аватар будет загружен асинхронно через LoadUserAvatarAsync
+ Trace.WriteLine($"[User.DisplayAvatarSource] {FullName}: Not in cache, returning placeholder");
}
}
catch (Exception ex)
diff --git a/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs b/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs
index 3b6e4a5..b3a35ea 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;
@@ -11,17 +11,17 @@ public partial class DepartmentDetailsPage : ContentPage
{
try
{
- Trace.WriteLine($"=== DEPARTMENTDETAILSPAGE CONSTRUCTOR START (ID: {departmentId}) ===");
+ Trace.WriteLine($"=== DEPARTMENT DETAILS PAGE CONSTRUCTOR START (ID: {departmentId}) ===");
InitializeComponent();
_departmentId = departmentId;
_teamMembers = new ObservableCollection();
TeamCollectionView.ItemsSource = _teamMembers;
_ = LoadDepartmentDataAsync();
- Trace.WriteLine("=== DEPARTMENTDETAILSPAGE CONSTRUCTOR END ===");
+ Trace.WriteLine("=== DEPARTMENT DETAILS PAGE CONSTRUCTOR END ===");
}
catch (Exception ex)
{
- Trace.WriteLine("=== DEPARTMENTDETAILSPAGE CONSTRUCTOR ERROR ===");
+ Trace.WriteLine("=== DEPARTMENT DETAILS PAGE CONSTRUCTOR ERROR ===");
Trace.WriteLine($"[DepartmentDetailsPage] ❌ Тип ошибки: {ex.GetType().FullName}");
Trace.WriteLine($"[DepartmentDetailsPage] ❌ Сообщение: {ex.Message}");
Trace.WriteLine($"[DepartmentDetailsPage] ❌ StackTrace: {ex.StackTrace}");
@@ -75,14 +75,20 @@ public partial class DepartmentDetailsPage : ContentPage
HeadPositionLabel.Text = _department.Head.PositionName ?? "Должность не указана";
// Загружаем аватар руководителя
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> Загрузка аватара руководителя...");
await ServiceProvider.ApiService.LoadUserAvatarAsync(_department.Head);
- HeadAvatarImage.Source = _department.Head.DisplayAvatarSource;
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> IsAvatarLoaded: {_department.Head.IsAvatarLoaded}");
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> CachedAvatarFilePath: {_department.Head.CachedAvatarFilePath}");
+
+ var avatarSource = _department.Head.DisplayAvatarSource;
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> AvatarSource type: {avatarSource?.GetType().Name ?? "null"}");
+ HeadAvatarImage.Source = avatarSource;
}
Trace.WriteLine($"[DepartmentDetailsPage] >>> Вызов GetDepartmentEmployeesAsync({_departmentId})");
var employees = await ServiceProvider.ApiService.GetDepartmentEmployeesAsync(_departmentId);
Trace.WriteLine($"[DepartmentDetailsPage] >>> Получено сотрудников: {employees.Count}");
_teamMembers.Clear();
- if (_department.Head != null && !employees.Any(e => e.Id == _department.Head.Id))
+ if (_department.Head != null && employees.All(e => e.Id != _department.Head.Id))
{
_teamMembers.Add(_department.Head);
Trace.WriteLine($"[DepartmentDetailsPage] >>> Руководитель добавлен в команду");
@@ -172,21 +178,9 @@ public partial class DepartmentDetailsPage : ContentPage
await ServiceProvider.ApiService.LoadUserAvatarAsync(_department.Head);
}
- // Загружаем аватары команды
+ // Загружаем аватары команды - UI обновится автоматически через INotifyPropertyChanged
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)
diff --git a/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs b/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
index 548ebc4..0fc63cf 100644
--- a/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
+++ b/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
@@ -82,12 +82,15 @@ public partial class EmployeeProfilePage : ContentPage
Trace.WriteLine($"[EmployeeProfilePage] ✅ Профиль загружен: {_user.FullName}");
// Предзагружаем аватар пользователя
+ Trace.WriteLine($"[EmployeeProfilePage] >>> Загрузка аватара, URL: {_user.DisplayAvatarUrl}");
await ServiceProvider.ApiService.LoadUserAvatarAsync(_user);
+ Trace.WriteLine($"[EmployeeProfilePage] IsAvatarLoaded: {_user.IsAvatarLoaded}, CachedPath: {_user.CachedAvatarFilePath}");
// Заполняем основную информацию
FullNameLabel.Text = _user.FullName;
PositionLabel.Text = _user.PositionName ?? "Должность не указана";
AvatarImage.Source = _user.DisplayAvatarSource;
+ Trace.WriteLine($"[EmployeeProfilePage] ✅ Аватар установлен");
OnlineIndicator.IsVisible = _user.IsOnline;
// Email
diff --git a/CORP-MAUI/Pages/EmployeesPage.xaml.cs b/CORP-MAUI/Pages/EmployeesPage.xaml.cs
index 291a924..28cf7e7 100644
--- a/CORP-MAUI/Pages/EmployeesPage.xaml.cs
+++ b/CORP-MAUI/Pages/EmployeesPage.xaml.cs
@@ -208,15 +208,6 @@ public partial class EmployeesPage : ContentPage
{
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)
diff --git a/CORP-MAUI/Pages/LoginPage.xaml b/CORP-MAUI/Pages/LoginPage.xaml
index 2f14349..abac6a2 100644
--- a/CORP-MAUI/Pages/LoginPage.xaml
+++ b/CORP-MAUI/Pages/LoginPage.xaml
@@ -21,7 +21,7 @@
public async Task GetOrLoadImageFileAsync(string? url, Func>? loader = null)
{
- if (string.IsNullOrEmpty(url)) return null;
+ if (string.IsNullOrEmpty(url))
+ {
+ Trace.WriteLine($"[ImageCacheService] URL пустой, пропускаем");
+ return null;
+ }
+
+ Trace.WriteLine($"[ImageCacheService] >>> GetOrLoadImageFileAsync: {url}");
// 1. Проверяем кэш путей
if (_pathCache.TryGetValue(url, out string? cachedPath) && File.Exists(cachedPath))
{
+ Trace.WriteLine($"[ImageCacheService] ✅ Найден в кэше путей: {cachedPath}");
return cachedPath;
}
// 2. Проверяем файл на диске
string filePath = GetCacheFilePath(url);
+ Trace.WriteLine($"[ImageCacheService] Ожидаемый путь: {filePath}");
+
if (File.Exists(filePath))
{
var fileInfo = new FileInfo(filePath);
+ Trace.WriteLine($"[ImageCacheService] Файл существует, размер: {fileInfo.Length}, время: {fileInfo.LastWriteTime}");
+
if (fileInfo.Length > 0 && fileInfo.LastWriteTime > DateTime.Now - _cacheExpiry)
{
_pathCache[url] = filePath;
+ Trace.WriteLine($"[ImageCacheService] ✅ Загружен с диска: {filePath}");
return filePath;
}
try { File.Delete(filePath); } catch { }
@@ -64,19 +76,31 @@ public class ImageCacheService
{
try
{
+ Trace.WriteLine($"[ImageCacheService] >>> Загрузка изображения через loader...");
byte[]? imageBytes = await loader(url);
+
if (imageBytes != null && imageBytes.Length > 0)
{
+ Trace.WriteLine($"[ImageCacheService] Получено {imageBytes.Length} байт, сохраняем в {filePath}");
await File.WriteAllBytesAsync(filePath, imageBytes);
_pathCache[url] = filePath;
+ Trace.WriteLine($"[ImageCacheService] ✅ Сохранено и добавлено в кэш");
return filePath;
}
+ else
+ {
+ Trace.WriteLine($"[ImageCacheService] ⚠️ Loader вернул пустые данные");
+ }
}
catch (Exception ex)
{
- Trace.WriteLine($"[ImageCacheService] Ошибка загрузки {url}: {ex.Message}");
+ Trace.WriteLine($"[ImageCacheService] ❌ Ошибка загрузки {url}: {ex.Message}");
}
}
+ else
+ {
+ Trace.WriteLine($"[ImageCacheService] ⚠️ Loader не задан");
+ }
return null;
}
diff --git a/CORP-MAUI/ViewModels/ProfileViewModel.cs b/CORP-MAUI/ViewModels/ProfileViewModel.cs
index f767c19..893921c 100644
--- a/CORP-MAUI/ViewModels/ProfileViewModel.cs
+++ b/CORP-MAUI/ViewModels/ProfileViewModel.cs
@@ -143,10 +143,20 @@ public class ProfileViewModel : INotifyPropertyChanged
try
{
+ Trace.WriteLine($"[ProfileViewModel] >>> Загрузка аватара для {_user.FullName}, URL: {_user.DisplayAvatarUrl}");
+
// Используем централизованный метод загрузки аватара
await ServiceProvider.ApiService.LoadUserAvatarAsync(_user);
- OnPropertyChanged(nameof(DisplayImageSource));
- Trace.WriteLine("[ProfileViewModel] ✅ Аватар загружен");
+
+ Trace.WriteLine($"[ProfileViewModel] IsAvatarLoaded: {_user.IsAvatarLoaded}, CachedPath: {_user.CachedAvatarFilePath}");
+
+ // Обновляем UI на главном потоке
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ OnPropertyChanged(nameof(DisplayImageSource));
+ });
+
+ Trace.WriteLine("[ProfileViewModel] ✅ Аватар загружен и UI обновлен");
}
catch (Exception ex)
{