using System.Collections.ObjectModel; using System.Diagnostics; using CORP_MAUI.Models; using CORP_MAUI.ViewModels; namespace CORP_MAUI.Pages; public partial class PostsPage { private readonly ObservableCollection _posts; private List _allPosts = new(); private int _currentPage = 1; private bool _isLoading; private string _searchText = string.Empty; public PostsPage() { InitializeComponent(); _posts = new ObservableCollection(); PostsCollectionView.ItemsSource = _posts; } // Добавляем метод OnAppearing, чтобы обновлять данные при показе страницы, если нужно protected override void OnAppearing() { base.OnAppearing(); // Загрузка аватара текущего пользователя _ = LoadUserAvatarAsync(); if (_posts.Count == 0) { _ = LoadPostsAsync(); } } private async Task LoadUserAvatarAsync() { try { var user = await ServiceProvider.ApiService.GetCurrentUserAsync(); if (user != null) { Trace.WriteLine($"[PostsPage] >>> Загрузка аватара для {user.FullName}"); await ServiceProvider.ApiService.LoadUserAvatarAsync(user); // Обновляем на главном потоке await MainThread.InvokeOnMainThreadAsync(() => { UserAvatarImage.Source = user.DisplayAvatarSource; Trace.WriteLine($"[PostsPage] ✅ Аватар установлен"); }); } } catch (Exception ex) { Trace.WriteLine($"[PostsPage] Ошибка загрузки аватара: {ex.Message}"); } } private void OnSearchTextChanged(object sender, TextChangedEventArgs e) { _searchText = e.NewTextValue ?? string.Empty; ApplyFilter(); } private void ApplyFilter() { IEnumerable filtered = _allPosts; if (!string.IsNullOrWhiteSpace(_searchText)) { string search = _searchText.ToLower(); filtered = filtered.Where(p => (p.Post.Title?.ToLower().Contains(search) ?? false) || (p.Post.Author?.FullName?.ToLower().Contains(search) ?? false) || (p.PreviewContent?.ToLower().Contains(search) ?? false)); } _posts.Clear(); foreach (var post in filtered) { _posts.Add(post); } } private async Task LoadPostsAsync(bool refresh = false) { if (_isLoading) return; _isLoading = true; if (refresh) { _currentPage = 1; } try { LoadingIndicator.IsVisible = true; LoadingIndicator.IsRunning = true; PaginatedResponse response = await ServiceProvider.ApiService.GetPostsPaginatedAsync(_currentPage); if (refresh) { _posts.Clear(); } if (response.Results != null) { IEnumerable> tasks = response.Results.Select(async post => { PostViewModel vm = new(post); // Логируем информацию об изображении if (!string.IsNullOrEmpty(post.Image)) { string imagePreview = post.Image.Length > 50 ? post.Image.Substring(0, 50) + "..." : post.Image; Trace.WriteLine($"[PostsPage] Пост {post.Id} - Post.Image: {imagePreview}"); } if (!string.IsNullOrEmpty(post.ImageUrl)) { Trace.WriteLine($"[PostsPage] Пост {post.Id} - Post.ImageUrl: {post.ImageUrl}"); } // Проверяем, есть ли Base64 изображение в Post.Image if (!string.IsNullOrEmpty(post.Image) && post.Image.StartsWith("data:image")) { Trace.WriteLine($"[PostsPage] ✅ Пост {post.Id} содержит Base64 изображение, пропускаем загрузку"); // DisplayImageSource автоматически обработает Base64 return vm; } // Если есть URL для изображения (через DisplayImageUrl, который проверяет ImageUrl и Image) if (!string.IsNullOrEmpty(post.DisplayImageUrl)) { try { Trace.WriteLine($"[PostsPage] 🔄 Обработка URL изображения для поста {post.Id}: {post.DisplayImageUrl}"); Uri uri = new Uri(post.DisplayImageUrl); // Sanitize filename & use extension from URL if available, else .jpg string extension = Path.GetExtension(uri.LocalPath); if (string.IsNullOrEmpty(extension)) extension = ".jpg"; string fileName = Path.GetFileNameWithoutExtension(uri.LocalPath); if (string.IsNullOrEmpty(fileName)) fileName = $"image_{post.Id}"; fileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (current, invalidChar) => current.Replace(invalidChar, '_')); string cacheFileName = $"post_img_{post.Id}_{fileName}{extension}"; string cacheFilePath = Path.Combine(FileSystem.CacheDirectory, cacheFileName); bool isCached = false; // 1. Проверяем кэш if (File.Exists(cacheFilePath)) { FileInfo fileInfo = new FileInfo(cacheFilePath); if (fileInfo.Length > 0) { Trace.WriteLine($"[PostsPage] ✅ Изображение из кэша: {cacheFilePath}"); vm.LocalImagePath = cacheFilePath; isCached = true; } else { Trace.WriteLine($"[PostsPage] ⚠️ Файл кэша пуст, удаляем: {cacheFilePath}"); try { File.Delete(cacheFilePath); } catch { } } } // 2. Если нет в кэше - загружаем if (!isCached) { Trace.WriteLine($"[PostsPage] Загрузка изображения: {post.DisplayImageUrl}"); byte[]? bytes = await ServiceProvider.ApiService.GetImageBytesAsync(post.DisplayImageUrl); if (bytes is { Length: > 0 }) { // Basic signature check bool isLikelyImage = false; if (bytes.Length > 2) { if (bytes[0] == 0xFF && bytes[1] == 0xD8) isLikelyImage = true; // JPEG else if (bytes[0] == 0x89 && bytes[1] == 0x50) isLikelyImage = true; // PNG else isLikelyImage = true; // Others // Check for HTML if (bytes.Length > 5 && bytes[0] == '<') isLikelyImage = false; } if (isLikelyImage) { await File.WriteAllBytesAsync(cacheFilePath, bytes); Trace.WriteLine($"[PostsPage] Сохранено в кэш: {cacheFilePath}"); vm.LocalImagePath = cacheFilePath; } else { Trace.WriteLine($"[PostsPage] ⚠️ Received bytes are likely NOT an image (starts with {bytes[0]:X}), skipping cache."); } } } } catch(Exception imgEx) { Trace.WriteLine($"[PostsPage] Ошибка обработки изображения для поста {post.Id}: {imgEx.Message}"); } } return vm; }); // Ждем загрузки всех изображений перед отображением списка PostViewModel[] viewModels = await Task.WhenAll(tasks); if (refresh) { _allPosts.Clear(); } foreach (PostViewModel vm in viewModels) { _allPosts.Add(vm); } // Применяем фильтр поиска ApplyFilter(); // Предзагружаем аватары авторов постов в фоне _ = PreloadPostAuthorsAvatarsAsync(viewModels); } } catch (Exception ex) { Trace.WriteLine($"Ошибка загрузки постов: {ex.Message}"); await DisplayAlert("Ошибка", "Не удалось загрузить новости", "OK"); } finally { _isLoading = false; LoadingIndicator.IsVisible = false; LoadingIndicator.IsRunning = false; 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 обновится автоматически через INotifyPropertyChanged } } catch (Exception ex) { Trace.WriteLine($"[PostsPage] Ошибка предзагрузки аватаров авторов: {ex.Message}"); } } private async void OnRefreshing(object sender, EventArgs e) { await LoadPostsAsync(true); } private async void OnPostSelected(object sender, SelectionChangedEventArgs e) { if (e.CurrentSelection.FirstOrDefault() is not PostViewModel selectedPost) return; // Навигация на страницу деталей await Navigation.PushAsync(new PostDetailsPage(selectedPost)); // Снимаем выделение PostsCollectionView.SelectedItem = null; } }