From 33e521e8bb162bf9f3d4e32e9039d4161ae42568 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Wed, 25 Jun 2025 10:40:03 -0400 Subject: [PATCH] More updates. --- MangaReader.Core/Common/ContributorRole.cs | 8 ++ MangaReader.Core/Data/Manga.cs | 2 +- MangaReader.Core/Data/MangaContext.cs | 18 ++--- MangaReader.Core/Data/MangaContributor.cs | 6 +- MangaReader.Core/Data/MangaContributorRole.cs | 7 -- MangaReader.Core/Data/MangaDescription.cs | 6 +- MangaReader.Core/Data/MangaSource.cs | 1 + .../Extensions/ServiceCollectionExtensions.cs | 32 +++++++- MangaReader.Core/MangaReader.Core.csproj | 1 + .../Metadata/IMangaMetadataCoordinator.cs | 6 ++ .../Metadata/MangaMetadataCoordinator.cs | 11 +++ .../Metadata/SourceMangaContributor.cs | 6 +- .../Metadata/SourceMangaContributorRole.cs | 8 -- MangaReader.Core/Pipeline/IMangaPipeline.cs | 4 +- MangaReader.Core/Pipeline/MangaPipeline.cs | 77 ++++++++++++++++--- .../Metadata/MangaDexMetadataProvider.cs | 4 +- .../MangaNato/Metadata/MangaNatoWebCrawler.cs | 2 +- .../Pipeline/MangaPipelineTests.cs | 29 ++++++- .../Metadata/MangaDexMetadataTests.cs | 4 +- .../Metadata/MangaNatoMetadataTests.cs | 5 +- MangaReader.WinUI/App.xaml.cs | 9 ++- MangaReader.WinUI/MangaReader.WinUI.csproj | 5 +- .../ViewModels/LibraryViewModel.cs | 24 ++++++ .../ViewModels/SearchViewModel.cs | 30 +++++++- MangaReader.WinUI/Views/LibraryView.xaml | 41 ++++++++++ MangaReader.WinUI/Views/LibraryView.xaml.cs | 19 +++++ MangaReader.WinUI/Views/SearchView.xaml | 12 ++- MangaReader.WinUI/Views/SearchView.xaml.cs | 20 ++++- 28 files changed, 334 insertions(+), 63 deletions(-) create mode 100644 MangaReader.Core/Common/ContributorRole.cs delete mode 100644 MangaReader.Core/Data/MangaContributorRole.cs create mode 100644 MangaReader.Core/Metadata/IMangaMetadataCoordinator.cs create mode 100644 MangaReader.Core/Metadata/MangaMetadataCoordinator.cs delete mode 100644 MangaReader.Core/Metadata/SourceMangaContributorRole.cs create mode 100644 MangaReader.WinUI/ViewModels/LibraryViewModel.cs create mode 100644 MangaReader.WinUI/Views/LibraryView.xaml create mode 100644 MangaReader.WinUI/Views/LibraryView.xaml.cs diff --git a/MangaReader.Core/Common/ContributorRole.cs b/MangaReader.Core/Common/ContributorRole.cs new file mode 100644 index 0000000..5a73240 --- /dev/null +++ b/MangaReader.Core/Common/ContributorRole.cs @@ -0,0 +1,8 @@ +namespace MangaReader.Core.Common; + +public enum ContributorRole +{ + Unknown, + Author, + Artist +} \ No newline at end of file diff --git a/MangaReader.Core/Data/Manga.cs b/MangaReader.Core/Data/Manga.cs index dc3ad08..7fd72b3 100644 --- a/MangaReader.Core/Data/Manga.cs +++ b/MangaReader.Core/Data/Manga.cs @@ -7,7 +7,7 @@ public class Manga public virtual ICollection Covers { get; set; } = []; public virtual ICollection Titles { get; set; } = []; - public virtual ICollection Descriptions { get; set; } = []; + //public virtual ICollection Descriptions { get; set; } = []; public virtual ICollection Sources { get; set; } = []; public virtual ICollection Contributors { get; set; } = []; public virtual ICollection Genres { get; set; } = []; diff --git a/MangaReader.Core/Data/MangaContext.cs b/MangaReader.Core/Data/MangaContext.cs index f9f1624..4d4ac01 100644 --- a/MangaReader.Core/Data/MangaContext.cs +++ b/MangaReader.Core/Data/MangaContext.cs @@ -37,7 +37,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options) //ConfigureMangaChapter(modelBuilder); //ConfigureChapterSource(modelBuilder); //ConfigureChapterPage(modelBuilder); - ConfigureSourceChapter(modelBuilder); + ConfigureMangaSourceChapter(modelBuilder); ConfigureSourcePage(modelBuilder); } @@ -114,29 +114,29 @@ public class MangaContext(DbContextOptions options) : DbContext(options) { modelBuilder .Entity() - .HasKey(mangaTitle => mangaTitle.MangaTitleId); + .HasKey(mangaDescription => mangaDescription.MangaDescriptionId); modelBuilder.Entity() - .Property(mt => mt.Name) + .Property(mangaDescription => mangaDescription.Name) .IsRequired(); modelBuilder.Entity() - .Property(mt => mt.Language) + .Property(mangaDescription => mangaDescription.Language) .IsRequired(); modelBuilder.Entity() - .HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name, mangaTitle.Language }) + .HasIndex(mangaDescription => new { mangaDescription.MangaSourceId, mangaDescription.Name, mangaDescription.Language }) .IsUnique(); modelBuilder .Entity() - .HasIndex(mangaTitle => mangaTitle.Name); + .HasIndex(mangaDescription => mangaDescription.Name); modelBuilder .Entity() - .HasOne(x => x.Manga) + .HasOne(x => x.MangaSource) .WithMany(x => x.Descriptions) - .HasForeignKey(x => x.MangaId) + .HasForeignKey(x => x.MangaSourceId) .OnDelete(DeleteBehavior.Cascade); } @@ -273,7 +273,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options) // .OnDelete(DeleteBehavior.Cascade); //} - private static void ConfigureSourceChapter(ModelBuilder modelBuilder) + private static void ConfigureMangaSourceChapter(ModelBuilder modelBuilder) { modelBuilder .Entity() diff --git a/MangaReader.Core/Data/MangaContributor.cs b/MangaReader.Core/Data/MangaContributor.cs index eba712a..5ed647c 100644 --- a/MangaReader.Core/Data/MangaContributor.cs +++ b/MangaReader.Core/Data/MangaContributor.cs @@ -1,4 +1,6 @@ -namespace MangaReader.Core.Data; +using MangaReader.Core.Common; + +namespace MangaReader.Core.Data; public class MangaContributor { @@ -8,5 +10,5 @@ public class MangaContributor public int ContributorId { get; set; } public required Contributor Contributor { get; set; } - public MangaContributorRole Role { get; set; } + public ContributorRole Role { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Data/MangaContributorRole.cs b/MangaReader.Core/Data/MangaContributorRole.cs deleted file mode 100644 index 95e6c04..0000000 --- a/MangaReader.Core/Data/MangaContributorRole.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MangaReader.Core.Data; - -public enum MangaContributorRole -{ - Author, - Artist -} \ No newline at end of file diff --git a/MangaReader.Core/Data/MangaDescription.cs b/MangaReader.Core/Data/MangaDescription.cs index 211f703..745e050 100644 --- a/MangaReader.Core/Data/MangaDescription.cs +++ b/MangaReader.Core/Data/MangaDescription.cs @@ -4,10 +4,10 @@ namespace MangaReader.Core.Data; public class MangaDescription { - public int MangaTitleId { get; set; } + public int MangaDescriptionId { get; set; } - public int MangaId { get; set; } - public required Manga Manga { get; set; } + public int MangaSourceId { get; set; } + public required MangaSource MangaSource { get; set; } public required string Name { get; set; } public required Language Language { get; set; } diff --git a/MangaReader.Core/Data/MangaSource.cs b/MangaReader.Core/Data/MangaSource.cs index 46131be..4478b47 100644 --- a/MangaReader.Core/Data/MangaSource.cs +++ b/MangaReader.Core/Data/MangaSource.cs @@ -12,5 +12,6 @@ public class MangaSource public required string Url { get; set; } + public virtual ICollection Descriptions { get; set; } = []; public virtual ICollection Chapters { get; set; } = []; } \ No newline at end of file diff --git a/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs b/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs index de57399..f1b25b0 100644 --- a/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs +++ b/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ -using MangaReader.Core.Http; +using MangaReader.Core.Data; +using MangaReader.Core.Http; using MangaReader.Core.Metadata; +using MangaReader.Core.Pipeline; using MangaReader.Core.Search; using MangaReader.Core.Sources.MangaDex.Api; using MangaReader.Core.Sources.MangaDex.Metadata; @@ -7,6 +9,7 @@ using MangaReader.Core.Sources.MangaDex.Search; using MangaReader.Core.Sources.NatoManga.Api; using MangaReader.Core.Sources.NatoManga.Metadata; using MangaReader.Core.Sources.NatoManga.Search; +using Microsoft.EntityFrameworkCore; #pragma warning disable IDE0130 // Namespace does not match folder structure namespace Microsoft.Extensions.DependencyInjection; @@ -14,7 +17,7 @@ namespace Microsoft.Extensions.DependencyInjection; public static class ServiceCollectionExtensions { - public static IServiceCollection AddMangaReader(this IServiceCollection services) + public static IServiceCollection AddMangaReader(this IServiceCollection services, Action? optionsAction = null) { // Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0 services.AddHttpClient(client => @@ -34,9 +37,32 @@ public static class ServiceCollectionExtensions // MangaDex services.AddScoped(); services.AddScoped(); - services.AddScoped(); + //services.AddScoped(); + services.AddKeyedScoped("MangaDex"); services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + + // Database + services.AddDbContext(options => + { + if (optionsAction is not null) + { + optionsAction(options); + } + else + { + var dbPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "MangaReader", + "manga.db"); + + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + options.UseSqlite($"Data Source={dbPath}"); + } + }); return services; } diff --git a/MangaReader.Core/MangaReader.Core.csproj b/MangaReader.Core/MangaReader.Core.csproj index 87003c0..a9c978f 100644 --- a/MangaReader.Core/MangaReader.Core.csproj +++ b/MangaReader.Core/MangaReader.Core.csproj @@ -9,6 +9,7 @@ + diff --git a/MangaReader.Core/Metadata/IMangaMetadataCoordinator.cs b/MangaReader.Core/Metadata/IMangaMetadataCoordinator.cs new file mode 100644 index 0000000..2234b55 --- /dev/null +++ b/MangaReader.Core/Metadata/IMangaMetadataCoordinator.cs @@ -0,0 +1,6 @@ +namespace MangaReader.Core.Metadata; + +public interface IMangaMetadataCoordinator +{ + IMangaMetadataProvider GetProvider(string sourceName); +} \ No newline at end of file diff --git a/MangaReader.Core/Metadata/MangaMetadataCoordinator.cs b/MangaReader.Core/Metadata/MangaMetadataCoordinator.cs new file mode 100644 index 0000000..3cee304 --- /dev/null +++ b/MangaReader.Core/Metadata/MangaMetadataCoordinator.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace MangaReader.Core.Metadata; + +public class MangaMetadataCoordinator(IServiceProvider serviceProvider) : IMangaMetadataCoordinator +{ + public IMangaMetadataProvider GetProvider(string sourceName) + { + return serviceProvider.GetRequiredKeyedService(sourceName); + } +} \ No newline at end of file diff --git a/MangaReader.Core/Metadata/SourceMangaContributor.cs b/MangaReader.Core/Metadata/SourceMangaContributor.cs index 0773dd9..76a3c87 100644 --- a/MangaReader.Core/Metadata/SourceMangaContributor.cs +++ b/MangaReader.Core/Metadata/SourceMangaContributor.cs @@ -1,7 +1,9 @@ -namespace MangaReader.Core.Metadata; +using MangaReader.Core.Common; + +namespace MangaReader.Core.Metadata; public class SourceMangaContributor { public required string Name { get; set; } - public SourceMangaContributorRole Role { get; set; } + public ContributorRole Role { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Metadata/SourceMangaContributorRole.cs b/MangaReader.Core/Metadata/SourceMangaContributorRole.cs deleted file mode 100644 index 4f7fb61..0000000 --- a/MangaReader.Core/Metadata/SourceMangaContributorRole.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MangaReader.Core.Metadata; - -public enum SourceMangaContributorRole -{ - Unknown, - Author, - Artist -} \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/IMangaPipeline.cs b/MangaReader.Core/Pipeline/IMangaPipeline.cs index 013ff17..c3b3589 100644 --- a/MangaReader.Core/Pipeline/IMangaPipeline.cs +++ b/MangaReader.Core/Pipeline/IMangaPipeline.cs @@ -2,6 +2,6 @@ public interface IMangaPipeline { - Task RunMetadataAsync(MangaMetadataPipelineRequest request); - Task RunPagesAsync(MangaPagePipelineRequest request); + Task RunMetadataAsync(MangaMetadataPipelineRequest request, CancellationToken cancellationToken); + Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/MangaPipeline.cs b/MangaReader.Core/Pipeline/MangaPipeline.cs index c29a5e8..bf82c06 100644 --- a/MangaReader.Core/Pipeline/MangaPipeline.cs +++ b/MangaReader.Core/Pipeline/MangaPipeline.cs @@ -13,7 +13,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline Secondary } - public async Task RunMetadataAsync(MangaMetadataPipelineRequest request) + public async Task RunMetadataAsync(MangaMetadataPipelineRequest request, CancellationToken cancellationToken) { string sourceName = request.SourceName; string sourceUrl = request.SourceUrl; @@ -24,7 +24,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline MangaSource mangaSource = await AddMangaSourceAsync(sourceUrl, manga, source); await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary); - await AddDescriptionAsync(manga, sourceManga.Description); + await AddDescriptionAsync(mangaSource, sourceManga.Description); foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) { @@ -36,6 +36,11 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline await LinkGenreAsync(manga, genre); } + foreach (SourceMangaContributor contributor in sourceManga.Contributors) + { + await LinkMangaContributorAsync(manga, contributor); + } + foreach (SourceMangaChapter chapter in sourceManga.Chapters) { await AddChapterAsync(mangaSource, chapter); @@ -133,20 +138,23 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline context.MangaTitles.Add(mangaTitle); } - private async Task AddDescriptionAsync(Manga manga, SourceMangaDescription? sourceMangaDescription) + private async Task AddDescriptionAsync(MangaSource mangaSource, SourceMangaDescription? sourceMangaDescription) { if (sourceMangaDescription == null) return; MangaDescription? mangaDescription = await context.MangaDescriptions.FirstOrDefaultAsync(md => - md.Manga == manga && md.Name == sourceMangaDescription.Name); + md.MangaSource == mangaSource && md.Language == sourceMangaDescription.Language); if (mangaDescription != null) + { + mangaDescription.Name = sourceMangaDescription.Name; return; + } mangaDescription = new() { - Manga = manga, + MangaSource = mangaSource, Name = sourceMangaDescription.Name, Language = sourceMangaDescription.Language }; @@ -189,6 +197,51 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline return genre; } + private async Task LinkMangaContributorAsync(Manga manga, SourceMangaContributor sourceMangaContributor) + { + Contributor contributor = await GetOrAddContributorAsync(sourceMangaContributor.Name); + + MangaContributor? mangaContributor = await context.MangaContributors.FirstOrDefaultAsync(x => + x.Manga == manga && x.Contributor == contributor && x.Role == sourceMangaContributor.Role); + + if (mangaContributor != null) + return; + + mangaContributor = new() + { + Manga = manga, + Contributor = contributor, + Role = sourceMangaContributor.Role + }; + + context.MangaContributors.Add(mangaContributor); + } + + private async Task GetOrAddContributorAsync(string contributorName) + { + Contributor? trackedContributor = context.ChangeTracker + .Entries() + .Select(e => e.Entity) + .FirstOrDefault(c => c.Name == contributorName); + + if (trackedContributor is not null) + return trackedContributor; + + Contributor? contributor = await context.Contributors.FirstOrDefaultAsync(x => x.Name == contributorName); + + if (contributor == null) + { + contributor = new() + { + Name = contributorName, + }; + + await context.Contributors.AddAsync(contributor); + } + + return contributor; + } + private async Task AddChapterAsync(MangaSource mangaSource, SourceMangaChapter sourceMangaChapter) { SourceChapter sourceChapter = await GetSourceChapter(mangaSource, sourceMangaChapter) @@ -225,9 +278,9 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline return sourceChapter; } - public async Task RunPagesAsync(MangaPagePipelineRequest request) + public async Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken) { - SourceChapter? sourceChapter = await context.SourceChapters.FirstOrDefaultAsync(x => x.SourceChapterId == request.SourceChapterId); + SourceChapter? sourceChapter = await context.SourceChapters.FirstOrDefaultAsync(x => x.SourceChapterId == request.SourceChapterId, cancellationToken); if (sourceChapter == null) return; @@ -236,14 +289,14 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline foreach (string pageImageUrl in request.PageImageUrls) { - await AddOrUpdateSourcePageAsync(sourceChapter, currentPageNumber++, pageImageUrl); + await AddOrUpdateSourcePageAsync(sourceChapter, currentPageNumber++, pageImageUrl, cancellationToken); } } - private async Task AddOrUpdateSourcePageAsync(SourceChapter sourceChapter, int pageNumber, string pageImageUrl) + private async Task AddOrUpdateSourcePageAsync(SourceChapter sourceChapter, int pageNumber, string pageImageUrl, CancellationToken cancellationToken) { SourcePage? sourcePage = await context.SourcePages.FirstOrDefaultAsync(x => - x.Chapter == sourceChapter && x.PageNumber == pageNumber); + x.Chapter == sourceChapter && x.PageNumber == pageNumber, cancellationToken); if (sourcePage == null) { @@ -254,11 +307,11 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline Url = pageImageUrl }; - context.SourcePages.Add(sourcePage); + await context.SourcePages.AddAsync(sourcePage, cancellationToken); } else { sourcePage.Url = pageImageUrl; } } -} +} \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs index 3d15836..828e599 100644 --- a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs @@ -137,7 +137,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe SourceMangaContributor contributor = new() { Name = authorEntity.Attributes.Name, - Role = SourceMangaContributorRole.Author + Role = ContributorRole.Author }; contributors.Add(contributor); @@ -151,7 +151,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe SourceMangaContributor contributor = new() { Name = artistEntity.Attributes.Name, - Role = SourceMangaContributorRole.Artist + Role = ContributorRole.Artist }; contributors.Add(contributor); diff --git a/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs b/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs index 25ca72e..a5742fe 100644 --- a/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs +++ b/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs @@ -79,7 +79,7 @@ public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler SourceMangaContributor contributor = new() { Name = name, - Role = SourceMangaContributorRole.Author + Role = ContributorRole.Author }; contributors.Add(contributor); diff --git a/MangaReader.Tests/Pipeline/MangaPipelineTests.cs b/MangaReader.Tests/Pipeline/MangaPipelineTests.cs index 97b4550..3aab6b5 100644 --- a/MangaReader.Tests/Pipeline/MangaPipelineTests.cs +++ b/MangaReader.Tests/Pipeline/MangaPipelineTests.cs @@ -31,6 +31,24 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture mt.IsPrimary).First().Name.ShouldBe("Fullmetal Alchemist"); context.MangaTitles.Where(mt => mt.IsPrimary).First().Language.ShouldBe(Language.English); context.Genres.Count().ShouldBe(2); + + context.MangaContributors.Count().ShouldBe(3); + context.MangaContributors.ElementAt(0).Contributor.Name.ShouldBe("Manga Author"); + context.MangaContributors.ElementAt(0).Role.ShouldBe(ContributorRole.Author); + context.MangaContributors.ElementAt(1).Contributor.Name.ShouldBe("Manga Author"); + context.MangaContributors.ElementAt(1).Role.ShouldBe(ContributorRole.Artist); + context.MangaContributors.ElementAt(2).Contributor.Name.ShouldBe("Helper Artist"); + context.MangaContributors.ElementAt(2).Role.ShouldBe(ContributorRole.Artist); + context.SourceChapters.ShouldHaveSingleItem(); } } \ No newline at end of file diff --git a/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs b/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs index b59e9bb..366345a 100644 --- a/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs +++ b/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs @@ -258,10 +258,10 @@ public class MangaDexMetadataTests sourceManga.Contributors.Length.ShouldBe(2); sourceManga.Contributors[0].Name.ShouldBe("Norishiro-chan"); - sourceManga.Contributors[0].Role.ShouldBe(SourceMangaContributorRole.Author); + sourceManga.Contributors[0].Role.ShouldBe(ContributorRole.Author); sourceManga.Contributors[1].Name.ShouldBe("Sakana Uozimi"); - sourceManga.Contributors[1].Role.ShouldBe(SourceMangaContributorRole.Artist); + sourceManga.Contributors[1].Role.ShouldBe(ContributorRole.Artist); sourceManga.Chapters.Count.ShouldBe(3); diff --git a/MangaReader.Tests/Sources/MangaNato/Metadata/MangaNatoMetadataTests.cs b/MangaReader.Tests/Sources/MangaNato/Metadata/MangaNatoMetadataTests.cs index 5993137..3cc41cd 100644 --- a/MangaReader.Tests/Sources/MangaNato/Metadata/MangaNatoMetadataTests.cs +++ b/MangaReader.Tests/Sources/MangaNato/Metadata/MangaNatoMetadataTests.cs @@ -1,4 +1,5 @@ -using MangaReader.Core.Http; +using MangaReader.Core.Common; +using MangaReader.Core.Http; using MangaReader.Core.Metadata; using MangaReader.Core.Sources.MangaNato.Metadata; using MangaReader.Tests.Utilities; @@ -37,7 +38,7 @@ public class MangaNatoMetadataTests SourceMangaContributor[] expectedContributors = [ - new() { Name = "Nagaoka Taichi", Role = SourceMangaContributorRole.Author } + new() { Name = "Nagaoka Taichi", Role = ContributorRole.Author } ]; manga.Contributors.ShouldBeEquivalentTo(expectedContributors); diff --git a/MangaReader.WinUI/App.xaml.cs b/MangaReader.WinUI/App.xaml.cs index 7f2b68f..c8a487b 100644 --- a/MangaReader.WinUI/App.xaml.cs +++ b/MangaReader.WinUI/App.xaml.cs @@ -1,7 +1,9 @@ -using MangaReader.WinUI.ViewModels; +using MangaReader.Core.Data; +using MangaReader.WinUI.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using System; +using System.IO; namespace MangaReader.WinUI; @@ -22,6 +24,11 @@ public partial class App : Application services.AddMangaReader(); ServiceProvider = services.BuildServiceProvider(); + + // Ensure the database is created + using var scope = ServiceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); } public App() diff --git a/MangaReader.WinUI/MangaReader.WinUI.csproj b/MangaReader.WinUI/MangaReader.WinUI.csproj index 99f64ab..09211a4 100644 --- a/MangaReader.WinUI/MangaReader.WinUI.csproj +++ b/MangaReader.WinUI/MangaReader.WinUI.csproj @@ -51,7 +51,7 @@ - + @@ -67,6 +67,9 @@ Designer + + Designer + MSBuild:Compile diff --git a/MangaReader.WinUI/ViewModels/LibraryViewModel.cs b/MangaReader.WinUI/ViewModels/LibraryViewModel.cs new file mode 100644 index 0000000..523c1d9 --- /dev/null +++ b/MangaReader.WinUI/ViewModels/LibraryViewModel.cs @@ -0,0 +1,24 @@ +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 LibraryViewModel : ViewModelBase +{ + +} \ No newline at end of file diff --git a/MangaReader.WinUI/ViewModels/SearchViewModel.cs b/MangaReader.WinUI/ViewModels/SearchViewModel.cs index 1c7e296..c8b5de3 100644 --- a/MangaReader.WinUI/ViewModels/SearchViewModel.cs +++ b/MangaReader.WinUI/ViewModels/SearchViewModel.cs @@ -1,5 +1,7 @@ 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; @@ -16,7 +18,10 @@ using System.Windows.Input; namespace MangaReader.WinUI.ViewModels; -public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) : ViewModelBase +public partial class SearchViewModel( + IMangaSearchCoordinator searchCoordinator, + IMangaMetadataCoordinator metadataCoordinator, + IMangaPipeline pipeline) : ViewModelBase { private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); @@ -65,6 +70,7 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) } public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync); + //public ICommand ImportCommand => new AsyncRelayCommand(ImportAsync); public async Task SearchAsync() { @@ -87,6 +93,8 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) ObservableMangaSearchResult mangaSearchResult = new() { + Source = searchResult.Source, + Url = searchResult.Url, Title = searchResult.Title, Thumbnail = searchResult.Thumbnail, Description = searchResult.Description, @@ -124,10 +132,30 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) 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; } diff --git a/MangaReader.WinUI/Views/LibraryView.xaml b/MangaReader.WinUI/Views/LibraryView.xaml new file mode 100644 index 0000000..6a196ad --- /dev/null +++ b/MangaReader.WinUI/Views/LibraryView.xaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/MangaReader.WinUI/Views/LibraryView.xaml.cs b/MangaReader.WinUI/Views/LibraryView.xaml.cs new file mode 100644 index 0000000..152dd53 --- /dev/null +++ b/MangaReader.WinUI/Views/LibraryView.xaml.cs @@ -0,0 +1,19 @@ +using MangaReader.WinUI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.Threading; +using System.Threading.Tasks; + +namespace MangaReader.WinUI.Views; + +public sealed partial class LibraryView : UserControl +{ + private readonly LibraryViewModel viewModel; + + public LibraryView() + { + InitializeComponent(); + + viewModel = (LibraryViewModel)DataContext; + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/Views/SearchView.xaml b/MangaReader.WinUI/Views/SearchView.xaml index e44ae8c..32e513d 100644 --- a/MangaReader.WinUI/Views/SearchView.xaml +++ b/MangaReader.WinUI/Views/SearchView.xaml @@ -34,6 +34,7 @@ + @@ -43,10 +44,17 @@ - + - + + + diff --git a/MangaReader.WinUI/Views/SearchView.xaml.cs b/MangaReader.WinUI/Views/SearchView.xaml.cs index fce05e8..5c04447 100644 --- a/MangaReader.WinUI/Views/SearchView.xaml.cs +++ b/MangaReader.WinUI/Views/SearchView.xaml.cs @@ -1,12 +1,30 @@ -using MangaReader.Core.Search; +using MangaReader.WinUI.ViewModels; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using System.Threading; +using System.Threading.Tasks; namespace MangaReader.WinUI.Views; public sealed partial class SearchView : UserControl { + private readonly SearchViewModel viewModel; + public SearchView() { InitializeComponent(); + + viewModel = (SearchViewModel)DataContext; + } + + private async void Button_Click(object sender, RoutedEventArgs e) + { + if (sender is not Button button) + return; + + if (button.Tag is not ObservableMangaSearchResult searchResult) + return; + + await viewModel.ImportAsync(searchResult, CancellationToken.None); } } \ No newline at end of file