Files
manga-reader/MangaReader.WinUI/ViewModels/SearchViewModel.cs
2025-06-25 10:40:03 -04:00

218 lines
6.6 KiB
C#

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<MangaSearchResult> _searchResults = [];
public ObservableCollection<MangaSearchResult> SearchResults
{
get
{
return _searchResults;
}
set
{
SetProperty(ref _searchResults, value);
}
}
private ObservableCollection<ObservableMangaSearchResult> _searchResults2 = [];
public ObservableCollection<ObservableMangaSearchResult> 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<string, MangaSearchResult[]> result = await searchCoordinator.SearchAsync(Keyword, _cancellationTokenSource.Token);
List<MangaSearchResult> searchResults = [];
List<ObservableMangaSearchResult> 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<BitmapImage?> 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}");
}
}
}