285 lines
12 KiB
C#
285 lines
12 KiB
C#
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<PostViewModel> _posts;
|
||
private List<PostViewModel> _allPosts = new();
|
||
private int _currentPage = 1;
|
||
private bool _isLoading;
|
||
private string _searchText = string.Empty;
|
||
|
||
public PostsPage()
|
||
{
|
||
InitializeComponent();
|
||
|
||
_posts = new ObservableCollection<PostViewModel>();
|
||
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<PostViewModel> 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<Post> response = await ServiceProvider.ApiService.GetPostsPaginatedAsync(_currentPage);
|
||
|
||
if (refresh)
|
||
{
|
||
_posts.Clear();
|
||
}
|
||
|
||
if (response.Results != null)
|
||
{
|
||
IEnumerable<Task<PostViewModel>> 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<PostViewModel> 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;
|
||
}
|
||
}
|