Added common language enum. Fixed "romaji" spelling. More UI updates.
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
public enum Language
|
public enum Language
|
||||||
{
|
{
|
||||||
|
Unknown,
|
||||||
Japanese,
|
Japanese,
|
||||||
Romanji,
|
Romaji,
|
||||||
English
|
English
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IMangaSearchProvider, MangaDexSearchProvider>();
|
services.AddScoped<IMangaSearchProvider, MangaDexSearchProvider>();
|
||||||
services.AddScoped<IMangaSearchCoordinator, MangaSearchCoordinator>();
|
services.AddScoped<IMangaSearchCoordinator, MangaSearchCoordinator>();
|
||||||
|
|
||||||
//services.AddScoped<IMangaMetadataProvider, NatoMangaWebCrawler>();
|
///services.AddScoped<IMangaMetadataProvider, NatoMangaWebCrawler>();
|
||||||
services.AddScoped<IMangaMetadataProvider, MangaDexMetadataProvider>();
|
services.AddScoped<IMangaMetadataProvider, MangaDexMetadataProvider>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
namespace MangaReader.Core.HttpService;
|
namespace MangaReader.Core.HttpService;
|
||||||
|
|
||||||
public class HttpService : IHttpService
|
public class HttpService(HttpClient httpClient) : IHttpService
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken)
|
||||||
|
=> GetStringAsync(url, new Dictionary<string, string>(), cancellationToken);
|
||||||
|
|
||||||
public HttpService(HttpClient httpClient)
|
public async Task<string> GetStringAsync(string url, IDictionary<string, string> headers, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Get, url);
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, string> header in headers)
|
||||||
|
{
|
||||||
|
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
//httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("MangaReader/1.0");
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0");
|
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0");
|
||||||
|
|
||||||
_httpClient = httpClient;
|
using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
|
||||||
}
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken)
|
return await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
=> _httpClient.GetStringAsync(url, cancellationToken);
|
}
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
public interface IHttpService
|
public interface IHttpService
|
||||||
{
|
{
|
||||||
Task<string> GetStringAsync(string url, CancellationToken cancellationToken);
|
Task<string> GetStringAsync(string url, CancellationToken cancellationToken);
|
||||||
|
Task<string> GetStringAsync(string url, IDictionary<string, string> headers, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
namespace MangaReader.Core.Metadata;
|
namespace MangaReader.Core.Metadata;
|
||||||
|
|
||||||
public enum SourceMangaLanguage
|
//public enum SourceMangaLanguage
|
||||||
{
|
//{
|
||||||
Unknown,
|
// Unknown,
|
||||||
Japanese,
|
// Japanese,
|
||||||
Romanji,
|
// Romanji,
|
||||||
English
|
// English
|
||||||
}
|
//}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
namespace MangaReader.Core.Metadata;
|
using MangaReader.Core.Common;
|
||||||
|
|
||||||
|
namespace MangaReader.Core.Metadata;
|
||||||
|
|
||||||
public class SourceMangaTitle
|
public class SourceMangaTitle
|
||||||
{
|
{
|
||||||
public required string Title { get; set; }
|
public required string Title { get; set; }
|
||||||
public SourceMangaLanguage Language { get; set; }
|
public Language Language { get; set; }
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,14 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
|||||||
public async Task RunAsync(MangaPipelineRequest request)
|
public async Task RunAsync(MangaPipelineRequest request)
|
||||||
{
|
{
|
||||||
string sourceName = request.SourceName;
|
string sourceName = request.SourceName;
|
||||||
|
string sourceUrl = request.SourceUrl;
|
||||||
SourceManga sourceManga = request.SourceManga;
|
SourceManga sourceManga = request.SourceManga;
|
||||||
|
|
||||||
Source source = await GetOrAddSourceAsync(sourceName);
|
Source source = await GetOrAddSourceAsync(sourceName);
|
||||||
Manga manga = await GetOrAddMangaAsync(sourceManga);
|
Manga manga = await GetOrAddMangaAsync(sourceManga);
|
||||||
|
|
||||||
|
await AddMangaSourceAsync(sourceUrl, manga, source);
|
||||||
|
|
||||||
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
|
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
|
||||||
{
|
{
|
||||||
await AddTitleAsync(manga, alternateTitle);
|
await AddTitleAsync(manga, alternateTitle);
|
||||||
@@ -83,6 +86,24 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
|||||||
[GeneratedRegex(@"\s+")]
|
[GeneratedRegex(@"\s+")]
|
||||||
private static partial Regex RemoveSpacesWithDashRegex();
|
private static partial Regex RemoveSpacesWithDashRegex();
|
||||||
|
|
||||||
|
private async Task AddMangaSourceAsync(string sourceUrl, Manga manga, Source source)
|
||||||
|
{
|
||||||
|
MangaSource? mangaSource = await context.MangaSources.FirstOrDefaultAsync(ms =>
|
||||||
|
ms.Manga == manga && ms.Source == source && ms.Url == sourceUrl);
|
||||||
|
|
||||||
|
if (mangaSource != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mangaSource = new()
|
||||||
|
{
|
||||||
|
Manga = manga,
|
||||||
|
Source = source,
|
||||||
|
Url = sourceUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
context.MangaSources.Add(mangaSource);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle)
|
private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle)
|
||||||
{
|
{
|
||||||
MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt =>
|
MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt =>
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ namespace MangaReader.Core.Pipeline;
|
|||||||
public class MangaPipelineRequest
|
public class MangaPipelineRequest
|
||||||
{
|
{
|
||||||
public required string SourceName { get; init; }
|
public required string SourceName { get; init; }
|
||||||
|
public required string SourceUrl { get; init; }
|
||||||
public required SourceManga SourceManga { get; init; }
|
public required SourceManga SourceManga { get; init; }
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using MangaReader.Core.Metadata;
|
using MangaReader.Core.Common;
|
||||||
|
using MangaReader.Core.Metadata;
|
||||||
using MangaReader.Core.Sources.MangaDex.Api;
|
using MangaReader.Core.Sources.MangaDex.Api;
|
||||||
|
|
||||||
namespace MangaReader.Core.Sources.MangaDex.Metadata;
|
namespace MangaReader.Core.Sources.MangaDex.Metadata;
|
||||||
@@ -62,11 +63,11 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
|||||||
if (attributes.AltTitles == null || attributes.AltTitles.Count == 0)
|
if (attributes.AltTitles == null || attributes.AltTitles.Count == 0)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
Dictionary<string, SourceMangaLanguage> languageIdMap = new()
|
Dictionary<string, Language> languageIdMap = new()
|
||||||
{
|
{
|
||||||
{ "en", SourceMangaLanguage.English },
|
{ "en", Language.English },
|
||||||
{ "ja", SourceMangaLanguage.Japanese },
|
{ "ja", Language.Japanese },
|
||||||
{ "ja-ro", SourceMangaLanguage.Romanji },
|
{ "ja-ro", Language.Romaji },
|
||||||
};
|
};
|
||||||
|
|
||||||
List<SourceMangaTitle> sourceMangaTitles = [];
|
List<SourceMangaTitle> sourceMangaTitles = [];
|
||||||
@@ -75,7 +76,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
|||||||
{
|
{
|
||||||
foreach (string alternateTitleKey in alternateTitle.Keys)
|
foreach (string alternateTitleKey in alternateTitle.Keys)
|
||||||
{
|
{
|
||||||
if (languageIdMap.TryGetValue(alternateTitleKey, out SourceMangaLanguage language) == false)
|
if (languageIdMap.TryGetValue(alternateTitleKey, out Language language) == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
SourceMangaTitle sourceMangaTitle = new()
|
SourceMangaTitle sourceMangaTitle = new()
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
|
|||||||
|
|
||||||
if (thing.Count > 0)
|
if (thing.Count > 0)
|
||||||
{
|
{
|
||||||
Guid[] mangaGuids = thing.Select(x => x.Key).ToArray();
|
Guid[] mangaGuids = [.. thing.Select(x => x.Key)];
|
||||||
var reults = await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken);
|
var reults = await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken);
|
||||||
//var reults = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken);
|
//var reults = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
using MangaReader.Core.Common;
|
||||||
using MangaReader.Core.Metadata;
|
using MangaReader.Core.Metadata;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
@@ -46,7 +47,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler
|
|||||||
SourceMangaTitle sourceMangaTitle = new()
|
SourceMangaTitle sourceMangaTitle = new()
|
||||||
{
|
{
|
||||||
Title = title,
|
Title = title,
|
||||||
Language = SourceMangaLanguage.Unknown
|
Language = Language.Unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
sourceMangaTitles.Add(sourceMangaTitle);
|
sourceMangaTitles.Add(sourceMangaTitle);
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ public partial class NatoMangaClient(IHttpService httpService) : INatoMangaClien
|
|||||||
{
|
{
|
||||||
string url = GetSearchUrl(searchWord);
|
string url = GetSearchUrl(searchWord);
|
||||||
|
|
||||||
string response = await httpService.GetStringAsync(url, cancellationToken);
|
Dictionary<string,string> requestHeader = [];
|
||||||
|
requestHeader.Add("Referer", "https://www.natomanga.com/");
|
||||||
|
|
||||||
|
string response = await httpService.GetStringAsync(url, requestHeader, cancellationToken);
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<NatoMangaSearchResult[]>(response, _jsonSerializerOptions) ?? [];
|
return JsonSerializer.Deserialize<NatoMangaSearchResult[]>(response, _jsonSerializerOptions) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string GetSearchUrl(string searchWord)
|
protected static string GetSearchUrl(string searchWord)
|
||||||
{
|
{
|
||||||
string formattedSeachWord = GetFormattedSearchWord(searchWord);
|
string formattedSeachWord = GetFormattedSearchWord(searchWord);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using MangaReader.Core.Data;
|
using MangaReader.Core.Common;
|
||||||
|
using MangaReader.Core.Data;
|
||||||
using MangaReader.Core.Metadata;
|
using MangaReader.Core.Metadata;
|
||||||
using MangaReader.Core.Pipeline;
|
using MangaReader.Core.Pipeline;
|
||||||
using MangaReader.Tests.Utilities;
|
using MangaReader.Tests.Utilities;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
namespace MangaReader.Tests.Pipeline;
|
namespace MangaReader.Tests.Pipeline;
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
|
|||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Title = "Hagane no Renkinjutsushi",
|
Title = "Hagane no Renkinjutsushi",
|
||||||
Language = SourceMangaLanguage.Romanji
|
Language = Language.Romaji
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
Genres = ["Action", "Adventure"],
|
Genres = ["Action", "Adventure"],
|
||||||
@@ -40,14 +42,15 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
|
|||||||
MangaPipelineRequest request = new()
|
MangaPipelineRequest request = new()
|
||||||
{
|
{
|
||||||
SourceName = "MySource",
|
SourceName = "MySource",
|
||||||
|
SourceUrl = "https://wwww.mymangasource.org/my-manga",
|
||||||
SourceManga = sourceManga
|
SourceManga = sourceManga
|
||||||
};
|
};
|
||||||
|
|
||||||
await pipeline.RunAsync(request);
|
await pipeline.RunAsync(request);
|
||||||
|
|
||||||
Assert.Single(context.Mangas);
|
context.Mangas.ShouldHaveSingleItem();
|
||||||
Assert.Single(context.MangaTitles);
|
context.MangaTitles.ShouldHaveSingleItem();
|
||||||
Assert.Equal(2, context.Genres.Count());
|
context.Genres.Count().ShouldBe(2);
|
||||||
Assert.Single(context.MangaChapters);
|
context.MangaChapters.ShouldHaveSingleItem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using MangaReader.Core.Metadata;
|
using MangaReader.Core.Common;
|
||||||
|
using MangaReader.Core.Metadata;
|
||||||
using MangaReader.Core.Sources.MangaDex.Api;
|
using MangaReader.Core.Sources.MangaDex.Api;
|
||||||
using MangaReader.Core.Sources.MangaDex.Metadata;
|
using MangaReader.Core.Sources.MangaDex.Metadata;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@@ -233,19 +234,19 @@ public class MangaDexMetadataTests
|
|||||||
sourceManga.AlternateTitles.Count.ShouldBe(5);
|
sourceManga.AlternateTitles.Count.ShouldBe(5);
|
||||||
|
|
||||||
sourceManga.AlternateTitles[0].Title.ShouldBe("オタクに優しいギャルはいない!?");
|
sourceManga.AlternateTitles[0].Title.ShouldBe("オタクに優しいギャルはいない!?");
|
||||||
sourceManga.AlternateTitles[0].Language.ShouldBe(SourceMangaLanguage.Japanese);
|
sourceManga.AlternateTitles[0].Language.ShouldBe(Language.Japanese);
|
||||||
|
|
||||||
sourceManga.AlternateTitles[1].Title.ShouldBe("Otaku ni Yasashii Gal wa Inai!?");
|
sourceManga.AlternateTitles[1].Title.ShouldBe("Otaku ni Yasashii Gal wa Inai!?");
|
||||||
sourceManga.AlternateTitles[1].Language.ShouldBe(SourceMangaLanguage.Romanji);
|
sourceManga.AlternateTitles[1].Language.ShouldBe(Language.Romaji);
|
||||||
|
|
||||||
sourceManga.AlternateTitles[2].Title.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?");
|
sourceManga.AlternateTitles[2].Title.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?");
|
||||||
sourceManga.AlternateTitles[2].Language.ShouldBe(SourceMangaLanguage.Romanji);
|
sourceManga.AlternateTitles[2].Language.ShouldBe(Language.Romaji);
|
||||||
|
|
||||||
sourceManga.AlternateTitles[3].Title.ShouldBe("Gal Can't Be Kind to Otaku!?");
|
sourceManga.AlternateTitles[3].Title.ShouldBe("Gal Can't Be Kind to Otaku!?");
|
||||||
sourceManga.AlternateTitles[3].Language.ShouldBe(SourceMangaLanguage.English);
|
sourceManga.AlternateTitles[3].Language.ShouldBe(Language.English);
|
||||||
|
|
||||||
sourceManga.AlternateTitles[4].Title.ShouldBe("Gals Can't Be Kind To A Geek!?");
|
sourceManga.AlternateTitles[4].Title.ShouldBe("Gals Can't Be Kind To A Geek!?");
|
||||||
sourceManga.AlternateTitles[4].Language.ShouldBe(SourceMangaLanguage.English);
|
sourceManga.AlternateTitles[4].Language.ShouldBe(Language.English);
|
||||||
|
|
||||||
sourceManga.Genres.Count.ShouldBe(5);
|
sourceManga.Genres.Count.ShouldBe(5);
|
||||||
sourceManga.Genres[0].ShouldBe("Romance");
|
sourceManga.Genres[0].ShouldBe("Romance");
|
||||||
|
|||||||
@@ -32,5 +32,7 @@ public class TestDbContextFactory : IDisposable
|
|||||||
{
|
{
|
||||||
_connection.Close();
|
_connection.Close();
|
||||||
_connection.Dispose();
|
_connection.Dispose();
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ public partial class UppercaseConverter : IValueConverter
|
|||||||
if (value == null)
|
if (value == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return value.ToString().ToUpperInvariant();
|
return value?.ToString()?.ToUpperInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using MangaReader.Core.Search;
|
|
||||||
using MangaReader.Core.Sources.MangaDex.Api;
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
namespace MangaReader.WinUI;
|
namespace MangaReader.WinUI;
|
||||||
@@ -8,7 +6,7 @@ public sealed partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
private const string ApplicationTitle = "Manga Reader";
|
private const string ApplicationTitle = "Manga Reader";
|
||||||
|
|
||||||
public MainWindow(IMangaSearchCoordinator mangaSearchCoordinator, IMangaDexClient mangaDexClient)
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
||||||
@@ -51,6 +51,8 @@
|
|||||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.9" />
|
||||||
|
<PackageReference Include="SkiaSharp" Version="3.119.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MangaReader.Core\MangaReader.Core.csproj" />
|
<ProjectReference Include="..\MangaReader.Core\MangaReader.Core.csproj" />
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using MangaReader.Core.Search;
|
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.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
@@ -10,6 +18,8 @@ namespace MangaReader.WinUI.ViewModels;
|
|||||||
|
|
||||||
public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) : ViewModelBase
|
public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) : ViewModelBase
|
||||||
{
|
{
|
||||||
|
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
|
||||||
private CancellationTokenSource? _cancellationTokenSource;
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
|
|
||||||
private string? _keyword;
|
private string? _keyword;
|
||||||
@@ -40,6 +50,20 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ObservableCollection<ObservableMangaSearchResult> _searchResults2 = [];
|
||||||
|
|
||||||
|
public ObservableCollection<ObservableMangaSearchResult> SearchResults2
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _searchResults2;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _searchResults2, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync);
|
public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync);
|
||||||
|
|
||||||
public async Task SearchAsync()
|
public async Task SearchAsync()
|
||||||
@@ -53,15 +77,114 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator)
|
|||||||
Dictionary<string, MangaSearchResult[]> result = await searchCoordinator.SearchAsync(Keyword, _cancellationTokenSource.Token);
|
Dictionary<string, MangaSearchResult[]> result = await searchCoordinator.SearchAsync(Keyword, _cancellationTokenSource.Token);
|
||||||
|
|
||||||
List<MangaSearchResult> searchResults = [];
|
List<MangaSearchResult> searchResults = [];
|
||||||
|
List<ObservableMangaSearchResult> mangaSearchResults = [];
|
||||||
|
|
||||||
foreach (var item in result)
|
foreach (var item in result)
|
||||||
{
|
{
|
||||||
foreach (MangaSearchResult searchResult in item.Value)
|
foreach (MangaSearchResult searchResult in item.Value)
|
||||||
{
|
{
|
||||||
|
//searchResults.Add(searchResult);
|
||||||
|
|
||||||
|
ObservableMangaSearchResult mangaSearchResult = new()
|
||||||
|
{
|
||||||
|
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);
|
searchResults.Add(searchResult);
|
||||||
|
mangaSearchResults.Add(mangaSearchResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchResults = new(searchResults);
|
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 partial class ObservableMangaSearchResult : ObservableObject
|
||||||
|
{
|
||||||
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,15 +17,15 @@
|
|||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<Media:AttachedCardShadow x:Key="CommonShadow" Offset="5" BlurRadius="10" Opacity=".4" />
|
<Media:AttachedCardShadow x:Key="CommonShadow" Offset="5" BlurRadius="10" Opacity=".4" />
|
||||||
|
|
||||||
<DataTemplate x:Key="MangaSearchResultTemplate" x:DataType="search:MangaSearchResult">
|
<DataTemplate x:Key="MangaSearchResultTemplate" x:DataType="vm:ObservableMangaSearchResult">
|
||||||
<Grid Padding="20" ColumnSpacing="20" MaxHeight="400" VerticalAlignment="Stretch" Background="{StaticResource CardBackgroundFillColorDefault}" CornerRadius="8">
|
<Grid Padding="20" ColumnSpacing="20" Height="400" VerticalAlignment="Stretch" Background="{StaticResource CardBackgroundFillColorDefault}" CornerRadius="8">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Border Grid.Column="0" MaxWidth="300" UI:Effects.Shadow="{StaticResource CommonShadow}" VerticalAlignment="Top" HorizontalAlignment="Left">
|
<Border Grid.Column="0" MaxWidth="300" UI:Effects.Shadow="{StaticResource CommonShadow}" VerticalAlignment="Top" HorizontalAlignment="Left">
|
||||||
<Grid VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="8">
|
<Grid VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="8">
|
||||||
<Image Source="{x:Bind Thumbnail, Mode=OneWay}" MaxWidth="300"></Image>
|
<Image Source="{x:Bind ThumbnailBitmap, Mode=OneWay}" MaxWidth="300"></Image>
|
||||||
<Canvas Background="#19000000"></Canvas>
|
<Canvas Background="#19000000"></Canvas>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
<RowDefinition Height="Auto"></RowDefinition>
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
<RowDefinition Height="*"></RowDefinition>
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Grid.Row="0" Text="{x:Bind Title}" FontSize="24" FontFamily="{StaticResource PoppinsSemiBold}" TextWrapping="Wrap"></TextBlock>
|
<TextBlock Grid.Row="0" Text="{x:Bind Title, Mode=OneTime}" FontSize="24" FontFamily="{StaticResource PoppinsSemiBold}" TextWrapping="Wrap"></TextBlock>
|
||||||
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind Genres, Mode=OneWay}" ItemTemplate="{StaticResource GenreTemplate}">
|
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind Genres, Mode=OneTime}" ItemTemplate="{StaticResource GenreTemplate}">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<Controls:WrapPanel Orientation="Horizontal" HorizontalSpacing="10" VerticalSpacing="10" />
|
<Controls:WrapPanel Orientation="Horizontal" HorizontalSpacing="10" VerticalSpacing="10" />
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
<DataTemplate x:Key="GenreTemplate" x:DataType="x:String">
|
<DataTemplate x:Key="GenreTemplate" x:DataType="x:String">
|
||||||
<Border>
|
<Border>
|
||||||
<TextBlock FontSize="12" Foreground="{StaticResource TextFillColorTertiary}" Text="{x:Bind Mode=OneWay, Converter={StaticResource UppercaseConverter}}" FontFamily="{StaticResource PoppinsSemiBold}"></TextBlock>
|
<TextBlock FontSize="12" Foreground="{StaticResource TextFillColorTertiary}" Text="{x:Bind Mode=OneTime, Converter={StaticResource UppercaseConverter}}" FontFamily="{StaticResource PoppinsSemiBold}"></TextBlock>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<ScrollViewer.RenderTransform>
|
<ScrollViewer.RenderTransform>
|
||||||
<ScaleTransform ScaleX="1" ScaleY="1" />
|
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||||
</ScrollViewer.RenderTransform>
|
</ScrollViewer.RenderTransform>
|
||||||
<ItemsRepeater ItemsSource="{Binding SearchResults, Mode=OneWay}" ItemTemplate="{StaticResource MangaSearchResultTemplate}">
|
<ItemsRepeater ItemsSource="{Binding SearchResults2, Mode=OneWay}" ItemTemplate="{StaticResource MangaSearchResultTemplate}">
|
||||||
<ItemsRepeater.Layout>
|
<ItemsRepeater.Layout>
|
||||||
<UniformGridLayout MinRowSpacing="50" MinColumnSpacing="50" ItemsStretch="Fill" MinItemWidth="800"></UniformGridLayout>
|
<UniformGridLayout MinRowSpacing="50" MinColumnSpacing="50" ItemsStretch="Fill" MinItemWidth="800"></UniformGridLayout>
|
||||||
</ItemsRepeater.Layout>
|
</ItemsRepeater.Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user