From d747f68df8c7617d9a90af7fbb7a9b95454cfa75 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Sun, 25 Jan 2026 18:33:32 -0500 Subject: [PATCH] Various updates. --- MangaReader.Core/Data/Manga.cs | 1 + MangaReader.Core/MangaReader.Core.csproj | 11 +- MangaReader.Core/Metadata/SourceManga.cs | 1 + MangaReader.Core/Pipeline/MangaPipeline.cs | 104 +++++++++++++++- .../Sources/MangaDex/Api/MangaAttributes.cs | 1 + MangaReader.Tests/MangaReader.Tests.csproj | 2 +- .../MangaDex/Api/MangaDexClientTests.cs | 2 + MangaReader.WinUI/MangaReader.WinUI.csproj | 2 +- .../ViewModels/LibraryViewModel.cs | 115 +++++++++++++++--- MangaReader.WinUI/Views/LibraryView.xaml | 68 ++++++++++- MangaReader.WinUI/Views/LibraryView.xaml.cs | 6 +- 11 files changed, 286 insertions(+), 27 deletions(-) diff --git a/MangaReader.Core/Data/Manga.cs b/MangaReader.Core/Data/Manga.cs index dc3ad08..76ab47d 100644 --- a/MangaReader.Core/Data/Manga.cs +++ b/MangaReader.Core/Data/Manga.cs @@ -4,6 +4,7 @@ public class Manga { public int MangaId { get; set; } public required string Slug { get; set; } + public int? Year { get; set; } public virtual ICollection Covers { get; set; } = []; public virtual ICollection Titles { get; set; } = []; diff --git a/MangaReader.Core/MangaReader.Core.csproj b/MangaReader.Core/MangaReader.Core.csproj index b203ea2..2e88cf1 100644 --- a/MangaReader.Core/MangaReader.Core.csproj +++ b/MangaReader.Core/MangaReader.Core.csproj @@ -8,9 +8,14 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/MangaReader.Core/Metadata/SourceManga.cs b/MangaReader.Core/Metadata/SourceManga.cs index 7adb18f..5780be7 100644 --- a/MangaReader.Core/Metadata/SourceManga.cs +++ b/MangaReader.Core/Metadata/SourceManga.cs @@ -14,4 +14,5 @@ public class SourceManga public int? Votes { get; set; } public SourceMangaChapter[] Chapters { get; set; } = []; public string[] CoverArtUrls { get; set; } = []; + public int? Year { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/MangaPipeline.cs b/MangaReader.Core/Pipeline/MangaPipeline.cs index eef63bd..c8ca106 100644 --- a/MangaReader.Core/Pipeline/MangaPipeline.cs +++ b/MangaReader.Core/Pipeline/MangaPipeline.cs @@ -1,6 +1,10 @@ using MangaReader.Core.Data; using MangaReader.Core.Metadata; using Microsoft.EntityFrameworkCore; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; +using System.Security.Cryptography; using System.Text.RegularExpressions; namespace MangaReader.Core.Pipeline; @@ -29,7 +33,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline foreach (SourceMangaDescription description in sourceManga.Descriptions) { await AddSourceDescriptionAsync(mangaSource, description); - //await AddDescriptionAsync(mangaSource, description); + await AddDescriptionAsync(manga, description); } foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) @@ -87,7 +91,10 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline manga.Sources.Any(mangaSource => mangaSource.Url == sourceUrl)); if (manga != null) + { + manga.Year = sourceManga.Year ?? manga.Year; return manga; + } manga = new() { @@ -353,6 +360,101 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline } } + //public async Task RunCoversAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken) + //{ + // SourceCover[] sourceCovers = await context.SourceCovers + // .Where(x => x.MangaCoverId == null) + // .ToArrayAsync(cancellationToken); + + // foreach (SourceCover sourceCover in sourceCovers) + // { + // await AddOrUpdateCoverAsync(sourceCover, cancellationToken); + // } + //} + + //private async Task AddOrUpdateCoverAsync(SourceCover sourceCover, CancellationToken cancellationToken) + //{ + // HttpClient client = httpClientFactory.CreateClient(); + // client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0"); + + // using HttpResponseMessage responseMessage = await client.GetAsync(sourceCover.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + // responseMessage.EnsureSuccessStatusCode(); + + // await using Stream networkStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken); + + // Image image = await Image.LoadAsync(networkStream, cancellationToken); + // networkStream.Position = 0; + + // IImageFormat imageFormat = await Image.DetectFormatAsync(networkStream, cancellationToken); + + // // Normalize output extension (you can keep WEBP if your viewer supports it; WinUI often prefers JPEG/PNG) + // string extension = (imageFormat.Name?.ToLowerInvariant()) switch + // { + // "png" => "png", + // _ => "jpg" + // }; + + // // Save to memory to hash & maybe convert formats + // await using MemoryStream memoryStream = new(); + + // if (extension == "png") + // await image.SaveAsPngAsync(memoryStream, cancellationToken); + // else + // await image.SaveAsJpegAsync(memoryStream, cancellationToken); + + // memoryStream.Position = 0; + + // // Compute hash for dedupe + // string sha256; + // using (var sha = SHA256.Create()) + // sha256 = Convert.ToHexString(sha.ComputeHash(memoryStream)).ToLowerInvariant(); + // memoryStream.Position = 0; + + // // Check if we already have this exact file + // var existing = await context.MangaCovers.FirstOrDefaultAsync(c => c.Sha256 == sha256 && c.MangaId == sc.MangaSource.MangaId, ct); + // var existing2 = await context.MangaCovers.FirstOrDefaultAsync(c => c. == sha256 && c.MangaId == sc.MangaSource.MangaId, ct); + + // MangaCover cover; + + // if (existing is not null) + // { + // cover = existing; + // } + // else + // { + // // Choose file path + // var folder = _paths.GetMangaCoverFolder(sc.MangaSource.MangaId); + // var guid = Guid.NewGuid(); + // var fileName = $"{guid}.{ext}"; + // var fullPath = Path.Combine(folder, fileName); + + // // Write to disk + // await using (var fs = File.Create(fullPath)) + // await ms.CopyToAsync(fs, ct); + + // // Create DB record + // cover = new MangaCover + // { + // Manga = sc.MangaSource.Manga, + // Guid = guid, + // FileExtension = ext, + // Sha256 = sha256, + // Width = image.Width, + // Height = image.Height, + // IsPrimary = false + // }; + + // context.MangaCovers.Add(cover); + // } + + // // Link and mark done + // sourceCover.MangaCover = cover; + // //sc.Status = CoverDownloadStatus.Done; + // //sc.Error = null; + + // await context.SaveChangesAsync(cancellationToken); + //} + public async Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken) { SourceChapter? sourceChapter = await context.SourceChapters.FirstOrDefaultAsync(x => x.SourceChapterId == request.SourceChapterId, cancellationToken); diff --git a/MangaReader.Core/Sources/MangaDex/Api/MangaAttributes.cs b/MangaReader.Core/Sources/MangaDex/Api/MangaAttributes.cs index fa6986c..63c66ec 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/MangaAttributes.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/MangaAttributes.cs @@ -6,4 +6,5 @@ public class MangaAttributes public List> AltTitles { get; set; } = []; public Dictionary Description { get; set; } = []; public List Tags { get; set; } = []; + public int? Year { get; set; } } diff --git a/MangaReader.Tests/MangaReader.Tests.csproj b/MangaReader.Tests/MangaReader.Tests.csproj index 1620a3e..56b82eb 100644 --- a/MangaReader.Tests/MangaReader.Tests.csproj +++ b/MangaReader.Tests/MangaReader.Tests.csproj @@ -42,7 +42,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs b/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs index 130eba2..e0be2d2 100644 --- a/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs +++ b/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs @@ -68,6 +68,8 @@ public class MangaDexClientTests CoverArtEntity coverArtEntity = (mangaEntity.Relationships[2] as CoverArtEntity)!; coverArtEntity.Attributes.ShouldNotBeNull(); coverArtEntity.Attributes.FileName.ShouldBe("6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg"); + + mangaEntity.Attributes.Year.ShouldBe(2021); } [Fact] diff --git a/MangaReader.WinUI/MangaReader.WinUI.csproj b/MangaReader.WinUI/MangaReader.WinUI.csproj index 7d88077..6fe0958 100644 --- a/MangaReader.WinUI/MangaReader.WinUI.csproj +++ b/MangaReader.WinUI/MangaReader.WinUI.csproj @@ -50,7 +50,7 @@ - + diff --git a/MangaReader.WinUI/ViewModels/LibraryViewModel.cs b/MangaReader.WinUI/ViewModels/LibraryViewModel.cs index 14ef4d6..9fe8fcf 100644 --- a/MangaReader.WinUI/ViewModels/LibraryViewModel.cs +++ b/MangaReader.WinUI/ViewModels/LibraryViewModel.cs @@ -1,20 +1,12 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MangaReader.Core.Data; -using MangaReader.Core.Metadata; -using MangaReader.Core.Pipeline; -using MangaReader.Core.Search; using Microsoft.EntityFrameworkCore; 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.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; @@ -23,12 +15,107 @@ namespace MangaReader.WinUI.ViewModels; public partial class LibraryViewModel(MangaContext context) : ViewModelBase { - public void GetSomething() - { - var mangas = context.Mangas - .Include(x => x.Sources) - .ThenInclude(x => x.Chapters); + private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - //mangas.Select(x => new MangaS) + private CancellationTokenSource? _cancellationTokenSource; + + private string? _keyword; + + public string? Keyword + { + get + { + return _keyword; + } + set + { + SetProperty(ref _keyword, value); + } + } + + private ObservableCollection _mangaItems = []; + + public ObservableCollection MangaItems + { + get + { + return _mangaItems; + } + set + { + SetProperty(ref _mangaItems, 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(); + + ObservableMangaItem[] mangaItems = await GetMangaItemsAsync(_cancellationTokenSource.Token); + + _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => + { + MangaItems = new(mangaItems); + }); + + //MangaItems = new(mangaItems); + } + + public async Task GetMangaItemsAsync(CancellationToken cancellationToken) + { + Manga[] mangas = await context.Mangas + .Include(x => x.Titles) + .Include(x => x.Descriptions) + .Include(x => x.Genres).ThenInclude(x => x.Genre) + //.Include(x => x.Sources) + //.ThenInclude(x => x.Chapters) + .ToArrayAsync(cancellationToken); + + List mangaItems = []; + + foreach (Manga manga in mangas) + { + ObservableMangaItem mangaItem = new() + { + MangaId = manga.MangaId, + Title = manga.Titles.OrderByDescending(title => title.IsPrimary).FirstOrDefault()?.Name, + Description = manga.Descriptions.FirstOrDefault()?.Text, + Genres = [.. manga.Genres.Select(x => x.Genre.Name)] + }; + + mangaItems.Add(mangaItem); + } + + return [.. mangaItems]; + } +} + +public partial class ObservableMangaItem : ObservableObject +{ + public int MangaId { 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); + } } } \ No newline at end of file diff --git a/MangaReader.WinUI/Views/LibraryView.xaml b/MangaReader.WinUI/Views/LibraryView.xaml index 5831ac4..d928b68 100644 --- a/MangaReader.WinUI/Views/LibraryView.xaml +++ b/MangaReader.WinUI/Views/LibraryView.xaml @@ -13,8 +13,63 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" DataContext="{Binding Source={StaticResource Locator}, Path=LibraryViewModel}" d:DataContext="{d:DesignInstance Type=vm:LibraryViewModel, IsDesignTimeCreatable=True}" + Loaded="Page_Loaded" mc:Ignorable="d"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -22,20 +77,21 @@ - + + - - + + diff --git a/MangaReader.WinUI/Views/LibraryView.xaml.cs b/MangaReader.WinUI/Views/LibraryView.xaml.cs index 8fb4dec..7b59093 100644 --- a/MangaReader.WinUI/Views/LibraryView.xaml.cs +++ b/MangaReader.WinUI/Views/LibraryView.xaml.cs @@ -1,7 +1,6 @@ using MangaReader.WinUI.ViewModels; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using System.Threading; using System.Threading.Tasks; namespace MangaReader.WinUI.Views; @@ -16,4 +15,9 @@ public sealed partial class LibraryView : Page viewModel = (LibraryViewModel)DataContext; } + + private void Page_Loaded(object sender, RoutedEventArgs e) + { + Task.Run(viewModel.SearchAsync); + } } \ No newline at end of file