From 4797d3c5592b6f813820b4c76c00093c313aacb3 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Tue, 14 Oct 2025 00:06:31 -0400 Subject: [PATCH] Restrcutured the database, and updated pipeline to include cover art. --- MangaReader.Core/Data/Manga.cs | 2 +- MangaReader.Core/Data/MangaContext.cs | 106 +++++++++++++++++- MangaReader.Core/Data/MangaDescription.cs | 6 +- MangaReader.Core/Data/MangaSource.cs | 4 +- MangaReader.Core/Data/SourceCover.cs | 14 +++ MangaReader.Core/Data/SourceDescription.cs | 14 +++ MangaReader.Core/Data/SourceTitle.cs | 15 +++ MangaReader.Core/MangaReader.Core.csproj | 8 +- MangaReader.Core/Metadata/SourceManga.cs | 10 +- MangaReader.Core/Pipeline/MangaPipeline.cs | 93 +++++++++++++-- .../Metadata/MangaDexMetadataProvider.cs | 65 ++++++++--- .../MangaNato/Metadata/MangaNatoWebCrawler.cs | 27 +++-- .../NatoManga/Metadata/NatoMangaWebCrawler.cs | 12 +- MangaReader.Tests/MangaReader.Tests.csproj | 6 +- .../Metadata/MangaDexMetadataTests.cs | 12 +- .../Metadata/MangaNatoMetadataTests.cs | 7 +- .../Metadata/NatoMangaWebCrawlerTests.cs | 2 +- MangaReader.WinUI/App.xaml.cs | 5 + MangaReader.WinUI/MainWindow.xaml | 11 +- MangaReader.WinUI/MangaReader.WinUI.csproj | 11 +- .../ViewModels/LibraryViewModel.cs | 12 +- MangaReader.WinUI/ViewModels/MainViewModel.cs | 39 +++++++ .../ViewModels/ViewModelLocator.cs | 6 + MangaReader.WinUI/Views/LibraryView.xaml | 4 +- MangaReader.WinUI/Views/LibraryView.xaml.cs | 2 +- MangaReader.WinUI/Views/MainView.xaml | 26 +++++ MangaReader.WinUI/Views/MainView.xaml.cs | 50 +++++++++ MangaReader.WinUI/Views/SearchView.xaml | 12 +- MangaReader.WinUI/Views/SearchView.xaml.cs | 2 +- 29 files changed, 488 insertions(+), 95 deletions(-) create mode 100644 MangaReader.Core/Data/SourceCover.cs create mode 100644 MangaReader.Core/Data/SourceDescription.cs create mode 100644 MangaReader.Core/Data/SourceTitle.cs create mode 100644 MangaReader.WinUI/ViewModels/MainViewModel.cs create mode 100644 MangaReader.WinUI/Views/MainView.xaml create mode 100644 MangaReader.WinUI/Views/MainView.xaml.cs diff --git a/MangaReader.Core/Data/Manga.cs b/MangaReader.Core/Data/Manga.cs index 7fd72b3..dc3ad08 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 4d4ac01..3896959 100644 --- a/MangaReader.Core/Data/MangaContext.cs +++ b/MangaReader.Core/Data/MangaContext.cs @@ -17,6 +17,10 @@ public class MangaContext(DbContextOptions options) : DbContext(options) //public DbSet MangaChapters { get; set; } //public DbSet ChapterSources { get; set; } //public DbSet ChapterPages { get; set; } + + public DbSet SourceTitles { get; set; } + public DbSet SourceDescriptions { get; set; } + public DbSet SourceCovers { get; set; } public DbSet SourceChapters { get; set; } public DbSet SourcePages { get; set; } @@ -37,6 +41,9 @@ public class MangaContext(DbContextOptions options) : DbContext(options) //ConfigureMangaChapter(modelBuilder); //ConfigureChapterSource(modelBuilder); //ConfigureChapterPage(modelBuilder); + ConfigureSourceTitle(modelBuilder); + ConfigureSourceDescription(modelBuilder); + ConfigureMangaSourceCover(modelBuilder); ConfigureMangaSourceChapter(modelBuilder); ConfigureSourcePage(modelBuilder); } @@ -117,7 +124,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options) .HasKey(mangaDescription => mangaDescription.MangaDescriptionId); modelBuilder.Entity() - .Property(mangaDescription => mangaDescription.Name) + .Property(mangaDescription => mangaDescription.Text) .IsRequired(); modelBuilder.Entity() @@ -125,18 +132,18 @@ public class MangaContext(DbContextOptions options) : DbContext(options) .IsRequired(); modelBuilder.Entity() - .HasIndex(mangaDescription => new { mangaDescription.MangaSourceId, mangaDescription.Name, mangaDescription.Language }) + .HasIndex(mangaDescription => new { mangaDescription.MangaId, mangaDescription.Text, mangaDescription.Language }) .IsUnique(); modelBuilder .Entity() - .HasIndex(mangaDescription => mangaDescription.Name); + .HasIndex(mangaDescription => mangaDescription.Text); modelBuilder .Entity() - .HasOne(x => x.MangaSource) + .HasOne(x => x.Manga) .WithMany(x => x.Descriptions) - .HasForeignKey(x => x.MangaSourceId) + .HasForeignKey(x => x.MangaId) .OnDelete(DeleteBehavior.Cascade); } @@ -273,6 +280,95 @@ public class MangaContext(DbContextOptions options) : DbContext(options) // .OnDelete(DeleteBehavior.Cascade); //} + private static void ConfigureSourceTitle(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasKey(x => x.SourceTitleId); + + modelBuilder + .Entity() + .Property(x => x.Name) + .IsRequired() + .HasMaxLength(512); + + // Avoid duplicate rows coming from the same source record + modelBuilder + .Entity() + .HasIndex(x => new { x.MangaSourceId, x.Language, x.Name }) + .IsUnique(); + + modelBuilder + .Entity() + .HasOne(x => x.MangaSource) + .WithMany(x => x.Titles) + .HasForeignKey(x => x.MangaSourceId) + .OnDelete(DeleteBehavior.Cascade); + } + + private static void ConfigureSourceDescription(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasKey(x => x.SourceDescriptionId); + + modelBuilder + .Entity() + .Property(x => x.Text) + .IsRequired(); + + // If sources can emit multiple descriptions per language, keep it non-unique. + // If not, uncomment: + //modelBuilder + // .Entity() + // .HasIndex(x => new { x.MangaSourceId, x.Language }) + // .IsUnique(); + + modelBuilder + .Entity() + .HasOne(x => x.MangaSource) + .WithMany(x => x.Descriptions) + .HasForeignKey(x => x.MangaSourceId) + .OnDelete(DeleteBehavior.Cascade); + } + + private static void ConfigureMangaSourceCover(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasKey(sourceCover => sourceCover.SourceCoverId); + + modelBuilder + .Entity() + .Property(x => x.Url) + .IsRequired() + .HasMaxLength(2048); + + modelBuilder + .Entity() + .HasIndex(sourceCover => new { sourceCover.MangaSourceId, sourceCover.Url }) + .IsUnique(true); + + modelBuilder + .Entity() + .HasOne(x => x.MangaSource) + .WithMany(x => x.Covers) + .HasForeignKey(x => x.MangaSourceId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder + .Entity() + .HasOne(x => x.MangaCover) + .WithMany() // or .WithMany(c => c.SourceCovers) if you add a collection nav on MangaCover + .HasForeignKey(x => x.MangaCoverId) + .OnDelete(DeleteBehavior.SetNull); + + // Helpful for lookups when you dedupe/file-link after download + modelBuilder + .Entity() + .HasIndex(x => x.MangaCoverId); + } + private static void ConfigureMangaSourceChapter(ModelBuilder modelBuilder) { modelBuilder diff --git a/MangaReader.Core/Data/MangaDescription.cs b/MangaReader.Core/Data/MangaDescription.cs index 745e050..18e0957 100644 --- a/MangaReader.Core/Data/MangaDescription.cs +++ b/MangaReader.Core/Data/MangaDescription.cs @@ -6,9 +6,9 @@ public class MangaDescription { public int MangaDescriptionId { get; set; } - public int MangaSourceId { get; set; } - public required MangaSource MangaSource { get; set; } + public int MangaId { get; set; } + public required Manga Manga { get; set; } - public required string Name { get; set; } + public required string Text { get; set; } public required Language Language { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Data/MangaSource.cs b/MangaReader.Core/Data/MangaSource.cs index 4478b47..d4ad2b5 100644 --- a/MangaReader.Core/Data/MangaSource.cs +++ b/MangaReader.Core/Data/MangaSource.cs @@ -12,6 +12,8 @@ public class MangaSource public required string Url { get; set; } - public virtual ICollection Descriptions { get; set; } = []; + public virtual ICollection Titles { get; set; } = []; + public virtual ICollection Descriptions { get; set; } = []; + public virtual ICollection Covers { get; set; } = []; public virtual ICollection Chapters { get; set; } = []; } \ No newline at end of file diff --git a/MangaReader.Core/Data/SourceCover.cs b/MangaReader.Core/Data/SourceCover.cs new file mode 100644 index 0000000..9123860 --- /dev/null +++ b/MangaReader.Core/Data/SourceCover.cs @@ -0,0 +1,14 @@ +namespace MangaReader.Core.Data; + +public class SourceCover +{ + public int SourceCoverId { get; set; } + + public int MangaSourceId { get; set; } + public required MangaSource MangaSource { get; set; } + + public required string Url { get; set; } + + public int? MangaCoverId { get; set; } + public MangaCover? MangaCover { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/Data/SourceDescription.cs b/MangaReader.Core/Data/SourceDescription.cs new file mode 100644 index 0000000..bef66b1 --- /dev/null +++ b/MangaReader.Core/Data/SourceDescription.cs @@ -0,0 +1,14 @@ +using MangaReader.Core.Common; + +namespace MangaReader.Core.Data; + +public class SourceDescription +{ + public int SourceDescriptionId { get; set; } + + public int MangaSourceId { get; set; } + public required MangaSource MangaSource { get; set; } + + public required string Text { get; set; } + public required Language Language { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/Data/SourceTitle.cs b/MangaReader.Core/Data/SourceTitle.cs new file mode 100644 index 0000000..f1ae7ec --- /dev/null +++ b/MangaReader.Core/Data/SourceTitle.cs @@ -0,0 +1,15 @@ +using MangaReader.Core.Common; + +namespace MangaReader.Core.Data; + +public class SourceTitle +{ + public int SourceTitleId { 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; } + public bool IsPrimary { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/MangaReader.Core.csproj b/MangaReader.Core/MangaReader.Core.csproj index a9c978f..b203ea2 100644 --- a/MangaReader.Core/MangaReader.Core.csproj +++ b/MangaReader.Core/MangaReader.Core.csproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/MangaReader.Core/Metadata/SourceManga.cs b/MangaReader.Core/Metadata/SourceManga.cs index 9555b72..7adb18f 100644 --- a/MangaReader.Core/Metadata/SourceManga.cs +++ b/MangaReader.Core/Metadata/SourceManga.cs @@ -3,15 +3,15 @@ public class SourceManga { public required SourceMangaTitle Title { get; set; } - public SourceMangaDescription? Description { get; set; } - public List AlternateTitles { get; set; } = []; + public SourceMangaDescription[] Descriptions { get; set; } = []; + public SourceMangaTitle[] AlternateTitles { get; set; } = []; public SourceMangaContributor[] Contributors { get; set; } = []; public MangaStatus Status { get; set; } = MangaStatus.Unknown; - public List Genres { get; set; } = []; + public string[] Genres { get; set; } = []; public DateTime? UpdateDate { get; set; } public long? Views { get; set; } public float? RatingPercent { get; set; } public int? Votes { get; set; } - public List Chapters { get; set; } = []; - public string[] CoverArt { get; set; } = []; + public SourceMangaChapter[] Chapters { get; set; } = []; + public string[] CoverArtUrls { get; set; } = []; } \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/MangaPipeline.cs b/MangaReader.Core/Pipeline/MangaPipeline.cs index bf82c06..eef63bd 100644 --- a/MangaReader.Core/Pipeline/MangaPipeline.cs +++ b/MangaReader.Core/Pipeline/MangaPipeline.cs @@ -20,14 +20,21 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline SourceManga sourceManga = request.SourceManga; Source source = await GetOrAddSourceAsync(sourceName); - Manga manga = await GetOrAddMangaAsync(sourceManga); + Manga manga = await GetOrAddMangaAsync(sourceManga, sourceUrl); MangaSource mangaSource = await AddMangaSourceAsync(sourceUrl, manga, source); + await AddSourceTitleAsync(mangaSource, sourceManga.Title, TitleType.Primary); await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary); - await AddDescriptionAsync(mangaSource, sourceManga.Description); + + foreach (SourceMangaDescription description in sourceManga.Descriptions) + { + await AddSourceDescriptionAsync(mangaSource, description); + //await AddDescriptionAsync(mangaSource, description); + } foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) { + await AddSourceTitleAsync(mangaSource, alternateTitle, TitleType.Secondary); await AddTitleAsync(manga, alternateTitle, TitleType.Secondary); } @@ -46,6 +53,11 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline await AddChapterAsync(mangaSource, chapter); } + foreach (string coverArtUrl in sourceManga.CoverArtUrls) + { + await AddCoverAsync(mangaSource, coverArtUrl); + } + context.SaveChanges(); } @@ -66,10 +78,13 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline return source; } - private async Task GetOrAddMangaAsync(SourceManga sourceManga) + private async Task GetOrAddMangaAsync(SourceManga sourceManga, string sourceUrl) { + //Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga => + // manga.Titles.Any(mangaTitle => mangaTitle.Name == sourceManga.Title.Name)); + Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga => - manga.Titles.Any(mangaTitle => mangaTitle.Name == sourceManga.Title.Name)); + manga.Sources.Any(mangaSource => mangaSource.Url == sourceUrl)); if (manga != null) return manga; @@ -119,6 +134,49 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline return mangaSource; } + private async Task AddSourceTitleAsync(MangaSource mangaSource, SourceMangaTitle sourceMangaTitle, TitleType titleType) + { + SourceTitle? sourceTitle = await context.SourceTitles.FirstOrDefaultAsync(mt => + mt.MangaSource == mangaSource && mt.Name == sourceMangaTitle.Name); + + if (sourceTitle != null) + return; + + sourceTitle = new() + { + MangaSource = mangaSource, + Name = sourceMangaTitle.Name, + Language = sourceMangaTitle.Language, + IsPrimary = titleType == TitleType.Primary + }; + + context.SourceTitles.Add(sourceTitle); + } + + private async Task AddSourceDescriptionAsync(MangaSource mangaSource, SourceMangaDescription? sourceMangaDescription) + { + if (sourceMangaDescription == null) + return; + + SourceDescription? sourceDescription = await context.SourceDescriptions.FirstOrDefaultAsync(md => + md.MangaSource == mangaSource && md.Language == sourceMangaDescription.Language); + + if (sourceDescription != null) + { + sourceDescription.Text = sourceMangaDescription.Name; + return; + } + + sourceDescription = new() + { + MangaSource = mangaSource, + Text = sourceMangaDescription.Name, + Language = sourceMangaDescription.Language + }; + + context.SourceDescriptions.Add(sourceDescription); + } + private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle, TitleType titleType) { MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt => @@ -138,24 +196,24 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline context.MangaTitles.Add(mangaTitle); } - private async Task AddDescriptionAsync(MangaSource mangaSource, SourceMangaDescription? sourceMangaDescription) + private async Task AddDescriptionAsync(Manga manga, SourceMangaDescription? sourceMangaDescription) { if (sourceMangaDescription == null) return; MangaDescription? mangaDescription = await context.MangaDescriptions.FirstOrDefaultAsync(md => - md.MangaSource == mangaSource && md.Language == sourceMangaDescription.Language); + md.Manga == manga && md.Language == sourceMangaDescription.Language); if (mangaDescription != null) { - mangaDescription.Name = sourceMangaDescription.Name; + mangaDescription.Text = sourceMangaDescription.Name; return; } mangaDescription = new() { - MangaSource = mangaSource, - Name = sourceMangaDescription.Name, + Manga = manga, + Text = sourceMangaDescription.Name, Language = sourceMangaDescription.Language }; @@ -278,6 +336,23 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline return sourceChapter; } + private async Task AddCoverAsync(MangaSource mangaSource, string coverArtUrl) + { + SourceCover? sourceCover = await context.SourceCovers.FirstOrDefaultAsync(x => + x.MangaSource == mangaSource && x.Url == coverArtUrl); + + if (sourceCover == null) + { + sourceCover = new() + { + MangaSource = mangaSource, + Url = coverArtUrl + }; + + context.SourceCovers.Add(sourceCover); + } + } + 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/Metadata/MangaDexMetadataProvider.cs b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs index 828e599..3193c27 100644 --- a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs @@ -31,10 +31,11 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe { Title = GetTitle(mangaAttributes), AlternateTitles = GetAlternateTitles(mangaAttributes), + Descriptions = GetDescriptions(mangaAttributes), Genres = GetGenres(mangaAttributes), Contributors = GetContributors(mangaRelationships), Chapters = GetChapters(mangaDexFeedResponse), - CoverArt = GetCoverArt(mangaGuid, coverArtResponse) + CoverArtUrls = GetCoverArtUrls(mangaGuid, coverArtResponse) }; } @@ -52,22 +53,27 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe private static SourceMangaTitle GetTitle(MangaAttributes attributes) { + (string title, Language langauge) = GetTileAndLanguage(attributes); + return new() { - Name = GetTileName(attributes), - Language = Language.English + Name = title, + Language = langauge }; } - private static string GetTileName(MangaAttributes attributes) + private static (string title, Language langauge) GetTileAndLanguage(MangaAttributes attributes) { - if (attributes.Title.TryGetValue("en", out string? title)) - return title; + if (attributes.Title.TryGetValue("en", out string? englishTitle)) + return (englishTitle, Language.English); - return string.Empty; + if (attributes.Title.TryGetValue("ja-ro", out string? japaneseTitle)) + return (japaneseTitle, Language.Japanese); + + return (string.Empty, Language.Unknown); } - private static List GetAlternateTitles(MangaAttributes attributes) + private static SourceMangaTitle[] GetAlternateTitles(MangaAttributes attributes) { if (attributes.AltTitles == null || attributes.AltTitles.Count == 0) return []; @@ -98,10 +104,41 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe } } - return sourceMangaTitles; + return [.. sourceMangaTitles]; } - private static List GetGenres(MangaAttributes attributes) + private static SourceMangaDescription[] GetDescriptions(MangaAttributes attributes) + { + if (attributes.AltTitles == null || attributes.AltTitles.Count == 0) + return []; + + Dictionary languageIdMap = new() + { + { "en", Language.English }, + { "ja", Language.Japanese }, + { "ja-ro", Language.Romaji }, + }; + + List sourceMangaDescriptions = []; + + foreach (string key in attributes.Description.Keys) + { + if (languageIdMap.TryGetValue(key, out Language language) == false) + continue; + + SourceMangaDescription sourceMangaDescription = new() + { + Name = attributes.Description[key], + Language = language + }; + + sourceMangaDescriptions.Add(sourceMangaDescription); + } + + return [.. sourceMangaDescriptions]; + } + + private static string[] GetGenres(MangaAttributes attributes) { if (attributes.Tags == null || attributes.Tags.Count == 0) return []; @@ -119,7 +156,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe tags.Add(tagEntity.Attributes.Name.FirstOrDefault().Value); } - return tags; + return [.. tags]; } private static SourceMangaContributor[] GetContributors(List relationships) @@ -160,7 +197,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe return [.. contributors]; } - private static List GetChapters(MangaDexResponse? mangaDexFeedResponse) + private static SourceMangaChapter[] GetChapters(MangaDexResponse? mangaDexFeedResponse) { if (mangaDexFeedResponse == null || mangaDexFeedResponse is not MangaDexCollectionResponse collectionResponse) return []; @@ -191,10 +228,10 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe chapters.Add(chapter); } - return chapters; + return [.. chapters]; } - private static string[] GetCoverArt(Guid mangaGuid, MangaDexResponse? coverArtResponse) + private static string[] GetCoverArtUrls(Guid mangaGuid, MangaDexResponse? coverArtResponse) { if (coverArtResponse == null || coverArtResponse is not MangaDexCollectionResponse collectionResponse) return []; diff --git a/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs b/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs index a5742fe..a969f24 100644 --- a/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs +++ b/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs @@ -31,18 +31,21 @@ public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler RatingPercent = GetRatingPercent(node.AverageRatingNode, node.BestRatingNode), Votes = node.VotesNode != null ? int.Parse(node.VotesNode.InnerText) : 0, Views = GetViews(node.ViewsNode), - Description = new() - { - Name = GetTextFromNodes(node.StoryDescriptionTextNodes), - Language = Language.Unknown - }, + Descriptions = + [ + new() + { + Name = GetTextFromNodes(node.StoryDescriptionTextNodes), + Language = Language.Unknown + } + ], Chapters = GetChapters(node.ChapterNodes) }; return manga; } - private static List GetAlternateTitles(HtmlNode? node) + private static SourceMangaTitle[] GetAlternateTitles(HtmlNode? node) { if (node == null) return []; @@ -98,7 +101,7 @@ public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler }; } - private static List GetGenres(HtmlNode? node) + private static string[] GetGenres(HtmlNode? node) { if (node == null) return []; @@ -162,12 +165,12 @@ public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler return (int)Math.Round(average / best * 100); } - private static List GetChapters(HtmlNodeCollection? chapterNodes) + private static SourceMangaChapter[] GetChapters(HtmlNodeCollection? chapterNodes) { - List chapters = []; - if (chapterNodes == null) - return chapters; + return []; + + List chapters = []; foreach (var node in chapterNodes) { @@ -187,7 +190,7 @@ public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler chapters.Add(chapter); } - return chapters; + return [.. chapters]; } private static float GetChapterNumber(HtmlNode? chapterNameNode) diff --git a/MangaReader.Core/Sources/NatoManga/Metadata/NatoMangaWebCrawler.cs b/MangaReader.Core/Sources/NatoManga/Metadata/NatoMangaWebCrawler.cs index f56e5b8..c08138b 100644 --- a/MangaReader.Core/Sources/NatoManga/Metadata/NatoMangaWebCrawler.cs +++ b/MangaReader.Core/Sources/NatoManga/Metadata/NatoMangaWebCrawler.cs @@ -27,7 +27,7 @@ public class NatoMangaWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler return manga; } - private static List GetGenres(HtmlNode? node) + private static string[] GetGenres(HtmlNode? node) { if (node == null) return []; @@ -77,12 +77,12 @@ public class NatoMangaWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler }; } - private static List GetChapters(HtmlNodeCollection? chapterNodes) + private static SourceMangaChapter[] GetChapters(HtmlNodeCollection? chapterNodes) { - List chapters = []; - if (chapterNodes == null) - return chapters; + return []; + + List chapters = []; foreach (var node in chapterNodes) { @@ -110,7 +110,7 @@ public class NatoMangaWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler chapters.Add(chapter); } - return chapters; + return [.. chapters]; } private static float GetChapterNumber(HtmlNode chapterNameNode) diff --git a/MangaReader.Tests/MangaReader.Tests.csproj b/MangaReader.Tests/MangaReader.Tests.csproj index 94fd41f..1620a3e 100644 --- a/MangaReader.Tests/MangaReader.Tests.csproj +++ b/MangaReader.Tests/MangaReader.Tests.csproj @@ -42,12 +42,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs b/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs index 366345a..a7e8076 100644 --- a/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs +++ b/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs @@ -231,7 +231,7 @@ public class MangaDexMetadataTests sourceManga.ShouldNotBeNull(); sourceManga.Title.Name.ShouldBe("Gals Can’t Be Kind to Otaku!?"); - sourceManga.AlternateTitles.Count.ShouldBe(5); + sourceManga.AlternateTitles.Length.ShouldBe(5); sourceManga.AlternateTitles[0].Name.ShouldBe("オタクに優しいギャルはいない!?"); sourceManga.AlternateTitles[0].Language.ShouldBe(Language.Japanese); @@ -248,7 +248,7 @@ public class MangaDexMetadataTests sourceManga.AlternateTitles[4].Name.ShouldBe("Gals Can't Be Kind To A Geek!?"); sourceManga.AlternateTitles[4].Language.ShouldBe(Language.English); - sourceManga.Genres.Count.ShouldBe(5); + sourceManga.Genres.Length.ShouldBe(5); sourceManga.Genres[0].ShouldBe("Romance"); sourceManga.Genres[1].ShouldBe("Comedy"); sourceManga.Genres[2].ShouldBe("School Life"); @@ -263,7 +263,7 @@ public class MangaDexMetadataTests sourceManga.Contributors[1].Name.ShouldBe("Sakana Uozimi"); sourceManga.Contributors[1].Role.ShouldBe(ContributorRole.Artist); - sourceManga.Chapters.Count.ShouldBe(3); + sourceManga.Chapters.Length.ShouldBe(3); sourceManga.Chapters[0].Volume.ShouldBeNull(); sourceManga.Chapters[0].Number.ShouldBe(69); @@ -280,8 +280,8 @@ public class MangaDexMetadataTests sourceManga.Chapters[2].Title.ShouldBe("Otaku & Gyaru & Protegee"); sourceManga.Chapters[2].Url.ShouldBe("https://mangadex.org/chapter/b5206e9b-6e3e-4ef0-aa62-381fd0ff75a5"); - sourceManga.CoverArt.Length.ShouldBe(2); - sourceManga.CoverArt[0].ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/2569ffd8-4ba1-4030-8d08-b7a21333a7a6.jpg"); - sourceManga.CoverArt[1].ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/d2314e9b-4287-4e65-8045-b713d97c0b28.jpg"); + sourceManga.CoverArtUrls.Length.ShouldBe(2); + sourceManga.CoverArtUrls[0].ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/2569ffd8-4ba1-4030-8d08-b7a21333a7a6.jpg"); + sourceManga.CoverArtUrls[1].ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/d2314e9b-4287-4e65-8045-b713d97c0b28.jpg"); } } \ No newline at end of file diff --git a/MangaReader.Tests/Sources/MangaNato/Metadata/MangaNatoMetadataTests.cs b/MangaReader.Tests/Sources/MangaNato/Metadata/MangaNatoMetadataTests.cs index 3cc41cd..eb9551a 100644 --- a/MangaReader.Tests/Sources/MangaNato/Metadata/MangaNatoMetadataTests.cs +++ b/MangaReader.Tests/Sources/MangaNato/Metadata/MangaNatoMetadataTests.cs @@ -50,11 +50,12 @@ public class MangaNatoMetadataTests manga.RatingPercent.ShouldBe(97); manga.Votes.ShouldBe(15979); + manga.Descriptions.Length.ShouldBe(1); //manga.Description.ShouldStartWith("Ooyama-kun normally doesn’t get involved with Akutsu-san, a delinquent girl in his class"); - manga.Description?.Name.ShouldStartWith("Ooyama-kun normally doesn’t get involved with Akutsu-san, a delinquent girl in his class"); - manga.Description?.Name.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935"); + manga.Descriptions[0].Name.ShouldStartWith("Ooyama-kun normally doesn’t get involved with Akutsu-san, a delinquent girl in his class"); + manga.Descriptions[0].Name.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935"); - manga.Chapters.Count.ShouldBe(236); + manga.Chapters.Length.ShouldBe(236); manga.Chapters[0].Url.ShouldBe("https://chapmanganato.to/manga-hf984788/chapter-186"); manga.Chapters[0].Number.ShouldBe(186); diff --git a/MangaReader.Tests/Sources/NatoManga/Metadata/NatoMangaWebCrawlerTests.cs b/MangaReader.Tests/Sources/NatoManga/Metadata/NatoMangaWebCrawlerTests.cs index 1cbc5af..a983b47 100644 --- a/MangaReader.Tests/Sources/NatoManga/Metadata/NatoMangaWebCrawlerTests.cs +++ b/MangaReader.Tests/Sources/NatoManga/Metadata/NatoMangaWebCrawlerTests.cs @@ -46,7 +46,7 @@ public class NatoMangaWebCrawlerTests //manga.Description.ShouldStartWith("Ooyama-kun normally doesn’t get involved with Akutsu-san, a delinquent girl in his class"); //manga.Description.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935"); - manga.Chapters.Count.ShouldBe(83); + manga.Chapters.Length.ShouldBe(83); manga.Chapters[0].Url.ShouldBe("https://www.natomanga.com/manga/gal-cant-be-kind-to-otaku/chapter-69"); manga.Chapters[0].Number.ShouldBe(69); diff --git a/MangaReader.WinUI/App.xaml.cs b/MangaReader.WinUI/App.xaml.cs index c8a487b..997a04a 100644 --- a/MangaReader.WinUI/App.xaml.cs +++ b/MangaReader.WinUI/App.xaml.cs @@ -1,5 +1,7 @@ using MangaReader.Core.Data; using MangaReader.WinUI.ViewModels; +using MangaReader.WinUI.Views; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using System; @@ -19,7 +21,9 @@ public partial class App : Application services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddMangaReader(); @@ -29,6 +33,7 @@ public partial class App : Application using var scope = ServiceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.Database.EnsureCreated(); + dbContext.Database.Migrate(); } public App() diff --git a/MangaReader.WinUI/MainWindow.xaml b/MangaReader.WinUI/MainWindow.xaml index 240ec9a..ad529c5 100644 --- a/MangaReader.WinUI/MainWindow.xaml +++ b/MangaReader.WinUI/MainWindow.xaml @@ -19,6 +19,7 @@ + @@ -27,13 +28,7 @@ - - + + diff --git a/MangaReader.WinUI/MangaReader.WinUI.csproj b/MangaReader.WinUI/MangaReader.WinUI.csproj index 09211a4..7d88077 100644 --- a/MangaReader.WinUI/MangaReader.WinUI.csproj +++ b/MangaReader.WinUI/MangaReader.WinUI.csproj @@ -49,10 +49,10 @@ - - - - + + + + @@ -67,6 +67,9 @@ Designer + + Designer + Designer diff --git a/MangaReader.WinUI/ViewModels/LibraryViewModel.cs b/MangaReader.WinUI/ViewModels/LibraryViewModel.cs index 523c1d9..14ef4d6 100644 --- a/MangaReader.WinUI/ViewModels/LibraryViewModel.cs +++ b/MangaReader.WinUI/ViewModels/LibraryViewModel.cs @@ -1,8 +1,10 @@ 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; @@ -11,6 +13,7 @@ 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; @@ -18,7 +21,14 @@ using System.Windows.Input; namespace MangaReader.WinUI.ViewModels; -public partial class LibraryViewModel : ViewModelBase +public partial class LibraryViewModel(MangaContext context) : ViewModelBase { + public void GetSomething() + { + var mangas = context.Mangas + .Include(x => x.Sources) + .ThenInclude(x => x.Chapters); + //mangas.Select(x => new MangaS) + } } \ No newline at end of file diff --git a/MangaReader.WinUI/ViewModels/MainViewModel.cs b/MangaReader.WinUI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..a21fa63 --- /dev/null +++ b/MangaReader.WinUI/ViewModels/MainViewModel.cs @@ -0,0 +1,39 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using MangaReader.Core.Metadata; +using MangaReader.Core.Pipeline; +using MangaReader.Core.Search; +using MangaReader.WinUI.Views; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Animation; +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 MainViewModel : ViewModelBase +{ + private string? _keyword; + + public string? Keyword + { + get + { + return _keyword; + } + set + { + SetProperty(ref _keyword, value); + } + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/ViewModels/ViewModelLocator.cs b/MangaReader.WinUI/ViewModels/ViewModelLocator.cs index c97793c..84f379b 100644 --- a/MangaReader.WinUI/ViewModels/ViewModelLocator.cs +++ b/MangaReader.WinUI/ViewModels/ViewModelLocator.cs @@ -4,6 +4,12 @@ namespace MangaReader.WinUI.ViewModels; public class ViewModelLocator { + public static MainViewModel MainViewModel + => App.ServiceProvider.GetRequiredService(); + public static SearchViewModel SearchViewModel => App.ServiceProvider.GetRequiredService(); + + public static LibraryViewModel LibraryViewModel + => App.ServiceProvider.GetRequiredService(); } \ No newline at end of file diff --git a/MangaReader.WinUI/Views/LibraryView.xaml b/MangaReader.WinUI/Views/LibraryView.xaml index 6a196ad..5831ac4 100644 --- a/MangaReader.WinUI/Views/LibraryView.xaml +++ b/MangaReader.WinUI/Views/LibraryView.xaml @@ -1,5 +1,5 @@ ---> - + diff --git a/MangaReader.WinUI/Views/LibraryView.xaml.cs b/MangaReader.WinUI/Views/LibraryView.xaml.cs index 152dd53..8fb4dec 100644 --- a/MangaReader.WinUI/Views/LibraryView.xaml.cs +++ b/MangaReader.WinUI/Views/LibraryView.xaml.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace MangaReader.WinUI.Views; -public sealed partial class LibraryView : UserControl +public sealed partial class LibraryView : Page { private readonly LibraryViewModel viewModel; diff --git a/MangaReader.WinUI/Views/MainView.xaml b/MangaReader.WinUI/Views/MainView.xaml new file mode 100644 index 0000000..5ff99e5 --- /dev/null +++ b/MangaReader.WinUI/Views/MainView.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/MangaReader.WinUI/Views/MainView.xaml.cs b/MangaReader.WinUI/Views/MainView.xaml.cs new file mode 100644 index 0000000..208998a --- /dev/null +++ b/MangaReader.WinUI/Views/MainView.xaml.cs @@ -0,0 +1,50 @@ +using MangaReader.WinUI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Animation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MangaReader.WinUI.Views; + +public sealed partial class MainView : UserControl +{ + private readonly MainViewModel viewModel; + + private readonly List<(string Tag, Type ViewType)> _tagViewMap = + [ + ("Search", typeof(SearchView)), + ("Library", typeof(LibraryView)) + ]; + + public MainView() + { + InitializeComponent(); + + viewModel = (MainViewModel)DataContext; + } + + private void nvSample_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) + { + if (args.SelectedItem is not NavigationViewItem navigationViewItem) + return; + + if (navigationViewItem.Tag is not string tagName) + return; + + UpdateContent(tagName, null); + } + + private void UpdateContent(string tag, NavigationTransitionInfo? transitionInfo) + { + Type? viewType = _tagViewMap.FirstOrDefault(x => x.Tag == tag).ViewType; + + if (viewType == null) + return; + + ContentFrame.Navigate(viewType); + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/Views/SearchView.xaml b/MangaReader.WinUI/Views/SearchView.xaml index 32e513d..e9c7c1d 100644 --- a/MangaReader.WinUI/Views/SearchView.xaml +++ b/MangaReader.WinUI/Views/SearchView.xaml @@ -1,5 +1,5 @@ - - + @@ -36,7 +36,9 @@ - + + + @@ -65,7 +67,7 @@ - + @@ -90,4 +92,4 @@ - + diff --git a/MangaReader.WinUI/Views/SearchView.xaml.cs b/MangaReader.WinUI/Views/SearchView.xaml.cs index 5c04447..09e8ff3 100644 --- a/MangaReader.WinUI/Views/SearchView.xaml.cs +++ b/MangaReader.WinUI/Views/SearchView.xaml.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace MangaReader.WinUI.Views; -public sealed partial class SearchView : UserControl +public sealed partial class SearchView : Page { private readonly SearchViewModel viewModel;