using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MangaReader.Core.Metadata; using MangaReader.Core.Pipeline; using MangaReader.Core.Search; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media.Imaging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; namespace MangaReader.WinUI.ViewModels; public partial class SearchViewModel( IMangaSearchCoordinator searchCoordinator, IMangaMetadataCoordinator metadataCoordinator, IMangaPipeline pipeline) : ViewModelBase { private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); private CancellationTokenSource? _cancellationTokenSource; private string? _keyword; public string? Keyword { get { return _keyword; } set { SetProperty(ref _keyword, value); } } private ObservableCollection _searchResults = []; public ObservableCollection SearchResults { get { return _searchResults; } set { SetProperty(ref _searchResults, value); } } private ObservableCollection _searchResults2 = []; public ObservableCollection SearchResults2 { get { return _searchResults2; } set { SetProperty(ref _searchResults2, value); } } public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync); //public ICommand ImportCommand => new AsyncRelayCommand(ImportAsync); public async Task SearchAsync() { if (string.IsNullOrWhiteSpace(Keyword)) return; _cancellationTokenSource?.Cancel(); _cancellationTokenSource = new(); Dictionary result = await searchCoordinator.SearchAsync(Keyword, _cancellationTokenSource.Token); List searchResults = []; List mangaSearchResults = []; foreach (var item in result) { foreach (MangaSearchResult searchResult in item.Value) { //searchResults.Add(searchResult); ObservableMangaSearchResult mangaSearchResult = new() { Source = searchResult.Source, Url = searchResult.Url, Title = searchResult.Title, Thumbnail = searchResult.Thumbnail, Description = searchResult.Description, Genres = searchResult.Genres }; Task.Run(() => mangaSearchResult.LoadThumbnailAsync(_dispatcherQueue)); // or defer this if you want lazy loading searchResults.Add(searchResult); mangaSearchResults.Add(mangaSearchResult); } } SearchResults = new(searchResults); SearchResults2 = new(mangaSearchResults); } public static async Task LoadWebpAsBitmapImageAsync(string? url) { if (string.IsNullOrWhiteSpace(url)) return null; using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0"); using var webpStream = await httpClient.GetStreamAsync(url); using var image = await Image.LoadAsync(webpStream); // from SixLabors.ImageSharp using var ms = new MemoryStream(); //await image.SaveAsPngAsync(ms); // Convert to PNG in memory await image.SaveAsJpegAsync(ms); ms.Position = 0; var bitmap = new BitmapImage(); await bitmap.SetSourceAsync(ms.AsRandomAccessStream()); return bitmap; } public async Task ImportAsync(ObservableMangaSearchResult searchResult, CancellationToken cancellationToken) { IMangaMetadataProvider metadataProvider = metadataCoordinator.GetProvider(searchResult.Source); SourceManga? sourceManga = await metadataProvider.GetMangaAsync(searchResult.Url, cancellationToken); if (sourceManga == null) return; MangaMetadataPipelineRequest request = new() { SourceName = searchResult.Source, SourceUrl = searchResult.Url, SourceManga = sourceManga, }; await pipeline.RunMetadataAsync(request, cancellationToken); } } public partial class ObservableMangaSearchResult : ObservableObject { public required string Source { get; init; } public required string Url { get; init; } public string? Title { get; init; } public string? Description { get; init; } public string? Thumbnail { get; init; } public string[] Genres { get; init; } = []; private BitmapImage? _thumbnailBitmap; public BitmapImage? ThumbnailBitmap { get { return _thumbnailBitmap; } set { SetProperty(ref _thumbnailBitmap, value); } } public async Task LoadThumbnailAsync(DispatcherQueue dispatchQueue) { if (string.IsNullOrWhiteSpace(Thumbnail)) return; try { using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0"); using var stream = await httpClient.GetStreamAsync(Thumbnail); using var image = await Image.LoadAsync(stream); // Important: use a pixel type using var ms = new MemoryStream(); await image.SaveAsJpegAsync(ms); // or SaveAsPngAsync ms.Position = 0; TaskCompletionSource taskCompletionSource = new(); dispatchQueue.TryEnqueue(async () => { var bitmap = new BitmapImage(); await bitmap.SetSourceAsync(ms.AsRandomAccessStream()); ThumbnailBitmap = bitmap; taskCompletionSource.SetResult(); }); taskCompletionSource.Task.GetAwaiter().GetResult(); //var bitmap = new BitmapImage(); //await bitmap.SetSourceAsync(ms.AsRandomAccessStream()); //ThumbnailBitmap = bitmap; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[Thumbnail Load Failed] {ex.Message}"); } } }