More updates.

This commit is contained in:
2025-06-25 10:40:03 -04:00
parent a82eab0ecb
commit 33e521e8bb
28 changed files with 334 additions and 63 deletions

View File

@@ -0,0 +1,8 @@
namespace MangaReader.Core.Common;
public enum ContributorRole
{
Unknown,
Author,
Artist
}

View File

@@ -7,7 +7,7 @@ public class Manga
public virtual ICollection<MangaCover> Covers { get; set; } = []; public virtual ICollection<MangaCover> Covers { get; set; } = [];
public virtual ICollection<MangaTitle> Titles { get; set; } = []; public virtual ICollection<MangaTitle> Titles { get; set; } = [];
public virtual ICollection<MangaDescription> Descriptions { get; set; } = []; //public virtual ICollection<MangaDescription> Descriptions { get; set; } = [];
public virtual ICollection<MangaSource> Sources { get; set; } = []; public virtual ICollection<MangaSource> Sources { get; set; } = [];
public virtual ICollection<MangaContributor> Contributors { get; set; } = []; public virtual ICollection<MangaContributor> Contributors { get; set; } = [];
public virtual ICollection<MangaGenre> Genres { get; set; } = []; public virtual ICollection<MangaGenre> Genres { get; set; } = [];

View File

@@ -37,7 +37,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
//ConfigureMangaChapter(modelBuilder); //ConfigureMangaChapter(modelBuilder);
//ConfigureChapterSource(modelBuilder); //ConfigureChapterSource(modelBuilder);
//ConfigureChapterPage(modelBuilder); //ConfigureChapterPage(modelBuilder);
ConfigureSourceChapter(modelBuilder); ConfigureMangaSourceChapter(modelBuilder);
ConfigureSourcePage(modelBuilder); ConfigureSourcePage(modelBuilder);
} }
@@ -114,29 +114,29 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
{ {
modelBuilder modelBuilder
.Entity<MangaDescription>() .Entity<MangaDescription>()
.HasKey(mangaTitle => mangaTitle.MangaTitleId); .HasKey(mangaDescription => mangaDescription.MangaDescriptionId);
modelBuilder.Entity<MangaDescription>() modelBuilder.Entity<MangaDescription>()
.Property(mt => mt.Name) .Property(mangaDescription => mangaDescription.Name)
.IsRequired(); .IsRequired();
modelBuilder.Entity<MangaDescription>() modelBuilder.Entity<MangaDescription>()
.Property(mt => mt.Language) .Property(mangaDescription => mangaDescription.Language)
.IsRequired(); .IsRequired();
modelBuilder.Entity<MangaDescription>() modelBuilder.Entity<MangaDescription>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name, mangaTitle.Language }) .HasIndex(mangaDescription => new { mangaDescription.MangaSourceId, mangaDescription.Name, mangaDescription.Language })
.IsUnique(); .IsUnique();
modelBuilder modelBuilder
.Entity<MangaDescription>() .Entity<MangaDescription>()
.HasIndex(mangaTitle => mangaTitle.Name); .HasIndex(mangaDescription => mangaDescription.Name);
modelBuilder modelBuilder
.Entity<MangaDescription>() .Entity<MangaDescription>()
.HasOne(x => x.Manga) .HasOne(x => x.MangaSource)
.WithMany(x => x.Descriptions) .WithMany(x => x.Descriptions)
.HasForeignKey(x => x.MangaId) .HasForeignKey(x => x.MangaSourceId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
} }
@@ -273,7 +273,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
// .OnDelete(DeleteBehavior.Cascade); // .OnDelete(DeleteBehavior.Cascade);
//} //}
private static void ConfigureSourceChapter(ModelBuilder modelBuilder) private static void ConfigureMangaSourceChapter(ModelBuilder modelBuilder)
{ {
modelBuilder modelBuilder
.Entity<SourceChapter>() .Entity<SourceChapter>()

View File

@@ -1,4 +1,6 @@
namespace MangaReader.Core.Data; using MangaReader.Core.Common;
namespace MangaReader.Core.Data;
public class MangaContributor public class MangaContributor
{ {
@@ -8,5 +10,5 @@ public class MangaContributor
public int ContributorId { get; set; } public int ContributorId { get; set; }
public required Contributor Contributor { get; set; } public required Contributor Contributor { get; set; }
public MangaContributorRole Role { get; set; } public ContributorRole Role { get; set; }
} }

View File

@@ -1,7 +0,0 @@
namespace MangaReader.Core.Data;
public enum MangaContributorRole
{
Author,
Artist
}

View File

@@ -4,10 +4,10 @@ namespace MangaReader.Core.Data;
public class MangaDescription public class MangaDescription
{ {
public int MangaTitleId { get; set; } public int MangaDescriptionId { get; set; }
public int MangaId { get; set; } public int MangaSourceId { get; set; }
public required Manga Manga { get; set; } public required MangaSource MangaSource { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public required Language Language { get; set; } public required Language Language { get; set; }

View File

@@ -12,5 +12,6 @@ public class MangaSource
public required string Url { get; set; } public required string Url { get; set; }
public virtual ICollection<MangaDescription> Descriptions { get; set; } = [];
public virtual ICollection<SourceChapter> Chapters { get; set; } = []; public virtual ICollection<SourceChapter> Chapters { get; set; } = [];
} }

View File

@@ -1,5 +1,7 @@
using MangaReader.Core.Http; using MangaReader.Core.Data;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata; using MangaReader.Core.Metadata;
using MangaReader.Core.Pipeline;
using MangaReader.Core.Search; using MangaReader.Core.Search;
using MangaReader.Core.Sources.MangaDex.Api; using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Core.Sources.MangaDex.Metadata; 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.Api;
using MangaReader.Core.Sources.NatoManga.Metadata; using MangaReader.Core.Sources.NatoManga.Metadata;
using MangaReader.Core.Sources.NatoManga.Search; using MangaReader.Core.Sources.NatoManga.Search;
using Microsoft.EntityFrameworkCore;
#pragma warning disable IDE0130 // Namespace does not match folder structure #pragma warning disable IDE0130 // Namespace does not match folder structure
namespace Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection;
@@ -14,7 +17,7 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddMangaReader(this IServiceCollection services) public static IServiceCollection AddMangaReader(this IServiceCollection services, Action<DbContextOptionsBuilder>? optionsAction = null)
{ {
// Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0 // Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0
services.AddHttpClient<IHttpService, HttpService>(client => services.AddHttpClient<IHttpService, HttpService>(client =>
@@ -34,9 +37,32 @@ public static class ServiceCollectionExtensions
// MangaDex // MangaDex
services.AddScoped<IMangaDexClient, MangaDexClient>(); services.AddScoped<IMangaDexClient, MangaDexClient>();
services.AddScoped<IMangaSearchProvider, MangaDexSearchProvider>(); services.AddScoped<IMangaSearchProvider, MangaDexSearchProvider>();
services.AddScoped<IMangaMetadataProvider, MangaDexMetadataProvider>(); //services.AddScoped<IMangaMetadataProvider, MangaDexMetadataProvider>();
services.AddKeyedScoped<IMangaMetadataProvider, MangaDexMetadataProvider>("MangaDex");
services.AddScoped<IMangaSearchCoordinator, MangaSearchCoordinator>(); services.AddScoped<IMangaSearchCoordinator, MangaSearchCoordinator>();
services.AddScoped<IMangaMetadataCoordinator, MangaMetadataCoordinator>();
services.AddScoped<IMangaPipeline, MangaPipeline>();
// Database
services.AddDbContext<MangaContext>(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; return services;
} }

View File

@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,6 @@
namespace MangaReader.Core.Metadata;
public interface IMangaMetadataCoordinator
{
IMangaMetadataProvider GetProvider(string sourceName);
}

View File

@@ -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<IMangaMetadataProvider>(sourceName);
}
}

View File

@@ -1,7 +1,9 @@
namespace MangaReader.Core.Metadata; using MangaReader.Core.Common;
namespace MangaReader.Core.Metadata;
public class SourceMangaContributor public class SourceMangaContributor
{ {
public required string Name { get; set; } public required string Name { get; set; }
public SourceMangaContributorRole Role { get; set; } public ContributorRole Role { get; set; }
} }

View File

@@ -1,8 +0,0 @@
namespace MangaReader.Core.Metadata;
public enum SourceMangaContributorRole
{
Unknown,
Author,
Artist
}

View File

@@ -2,6 +2,6 @@
public interface IMangaPipeline public interface IMangaPipeline
{ {
Task RunMetadataAsync(MangaMetadataPipelineRequest request); Task RunMetadataAsync(MangaMetadataPipelineRequest request, CancellationToken cancellationToken);
Task RunPagesAsync(MangaPagePipelineRequest request); Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken);
} }

View File

@@ -13,7 +13,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
Secondary Secondary
} }
public async Task RunMetadataAsync(MangaMetadataPipelineRequest request) public async Task RunMetadataAsync(MangaMetadataPipelineRequest request, CancellationToken cancellationToken)
{ {
string sourceName = request.SourceName; string sourceName = request.SourceName;
string sourceUrl = request.SourceUrl; string sourceUrl = request.SourceUrl;
@@ -24,7 +24,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
MangaSource mangaSource = await AddMangaSourceAsync(sourceUrl, manga, source); MangaSource mangaSource = await AddMangaSourceAsync(sourceUrl, manga, source);
await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary); await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary);
await AddDescriptionAsync(manga, sourceManga.Description); await AddDescriptionAsync(mangaSource, sourceManga.Description);
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
{ {
@@ -36,6 +36,11 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
await LinkGenreAsync(manga, genre); await LinkGenreAsync(manga, genre);
} }
foreach (SourceMangaContributor contributor in sourceManga.Contributors)
{
await LinkMangaContributorAsync(manga, contributor);
}
foreach (SourceMangaChapter chapter in sourceManga.Chapters) foreach (SourceMangaChapter chapter in sourceManga.Chapters)
{ {
await AddChapterAsync(mangaSource, chapter); await AddChapterAsync(mangaSource, chapter);
@@ -133,20 +138,23 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
context.MangaTitles.Add(mangaTitle); context.MangaTitles.Add(mangaTitle);
} }
private async Task AddDescriptionAsync(Manga manga, SourceMangaDescription? sourceMangaDescription) private async Task AddDescriptionAsync(MangaSource mangaSource, SourceMangaDescription? sourceMangaDescription)
{ {
if (sourceMangaDescription == null) if (sourceMangaDescription == null)
return; return;
MangaDescription? mangaDescription = await context.MangaDescriptions.FirstOrDefaultAsync(md => MangaDescription? mangaDescription = await context.MangaDescriptions.FirstOrDefaultAsync(md =>
md.Manga == manga && md.Name == sourceMangaDescription.Name); md.MangaSource == mangaSource && md.Language == sourceMangaDescription.Language);
if (mangaDescription != null) if (mangaDescription != null)
{
mangaDescription.Name = sourceMangaDescription.Name;
return; return;
}
mangaDescription = new() mangaDescription = new()
{ {
Manga = manga, MangaSource = mangaSource,
Name = sourceMangaDescription.Name, Name = sourceMangaDescription.Name,
Language = sourceMangaDescription.Language Language = sourceMangaDescription.Language
}; };
@@ -189,6 +197,51 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
return genre; 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<Contributor> GetOrAddContributorAsync(string contributorName)
{
Contributor? trackedContributor = context.ChangeTracker
.Entries<Contributor>()
.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) private async Task AddChapterAsync(MangaSource mangaSource, SourceMangaChapter sourceMangaChapter)
{ {
SourceChapter sourceChapter = await GetSourceChapter(mangaSource, sourceMangaChapter) SourceChapter sourceChapter = await GetSourceChapter(mangaSource, sourceMangaChapter)
@@ -225,9 +278,9 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
return sourceChapter; 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) if (sourceChapter == null)
return; return;
@@ -236,14 +289,14 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
foreach (string pageImageUrl in request.PageImageUrls) 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 => SourcePage? sourcePage = await context.SourcePages.FirstOrDefaultAsync(x =>
x.Chapter == sourceChapter && x.PageNumber == pageNumber); x.Chapter == sourceChapter && x.PageNumber == pageNumber, cancellationToken);
if (sourcePage == null) if (sourcePage == null)
{ {
@@ -254,7 +307,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
Url = pageImageUrl Url = pageImageUrl
}; };
context.SourcePages.Add(sourcePage); await context.SourcePages.AddAsync(sourcePage, cancellationToken);
} }
else else
{ {

View File

@@ -137,7 +137,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
SourceMangaContributor contributor = new() SourceMangaContributor contributor = new()
{ {
Name = authorEntity.Attributes.Name, Name = authorEntity.Attributes.Name,
Role = SourceMangaContributorRole.Author Role = ContributorRole.Author
}; };
contributors.Add(contributor); contributors.Add(contributor);
@@ -151,7 +151,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
SourceMangaContributor contributor = new() SourceMangaContributor contributor = new()
{ {
Name = artistEntity.Attributes.Name, Name = artistEntity.Attributes.Name,
Role = SourceMangaContributorRole.Artist Role = ContributorRole.Artist
}; };
contributors.Add(contributor); contributors.Add(contributor);

View File

@@ -79,7 +79,7 @@ public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler
SourceMangaContributor contributor = new() SourceMangaContributor contributor = new()
{ {
Name = name, Name = name,
Role = SourceMangaContributorRole.Author Role = ContributorRole.Author
}; };
contributors.Add(contributor); contributors.Add(contributor);

View File

@@ -31,6 +31,24 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
} }
], ],
Genres = ["Action", "Adventure"], Genres = ["Action", "Adventure"],
Contributors =
[
new()
{
Name = "Manga Author",
Role = ContributorRole.Author
},
new()
{
Name = "Manga Author",
Role = ContributorRole.Artist
},
new()
{
Name = "Helper Artist",
Role = ContributorRole.Artist
}
],
Chapters = Chapters =
[ [
new() new()
@@ -50,7 +68,7 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
SourceManga = sourceManga SourceManga = sourceManga
}; };
await pipeline.RunMetadataAsync(request); await pipeline.RunMetadataAsync(request, CancellationToken.None);
context.Mangas.ShouldHaveSingleItem(); context.Mangas.ShouldHaveSingleItem();
context.MangaTitles.Count().ShouldBe(2); context.MangaTitles.Count().ShouldBe(2);
@@ -58,6 +76,15 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
context.MangaTitles.Where(mt => mt.IsPrimary).First().Name.ShouldBe("Fullmetal Alchemist"); context.MangaTitles.Where(mt => mt.IsPrimary).First().Name.ShouldBe("Fullmetal Alchemist");
context.MangaTitles.Where(mt => mt.IsPrimary).First().Language.ShouldBe(Language.English); context.MangaTitles.Where(mt => mt.IsPrimary).First().Language.ShouldBe(Language.English);
context.Genres.Count().ShouldBe(2); 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(); context.SourceChapters.ShouldHaveSingleItem();
} }
} }

View File

@@ -258,10 +258,10 @@ public class MangaDexMetadataTests
sourceManga.Contributors.Length.ShouldBe(2); sourceManga.Contributors.Length.ShouldBe(2);
sourceManga.Contributors[0].Name.ShouldBe("Norishiro-chan"); 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].Name.ShouldBe("Sakana Uozimi");
sourceManga.Contributors[1].Role.ShouldBe(SourceMangaContributorRole.Artist); sourceManga.Contributors[1].Role.ShouldBe(ContributorRole.Artist);
sourceManga.Chapters.Count.ShouldBe(3); sourceManga.Chapters.Count.ShouldBe(3);

View File

@@ -1,4 +1,5 @@
using MangaReader.Core.Http; using MangaReader.Core.Common;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata; using MangaReader.Core.Metadata;
using MangaReader.Core.Sources.MangaNato.Metadata; using MangaReader.Core.Sources.MangaNato.Metadata;
using MangaReader.Tests.Utilities; using MangaReader.Tests.Utilities;
@@ -37,7 +38,7 @@ public class MangaNatoMetadataTests
SourceMangaContributor[] expectedContributors = SourceMangaContributor[] expectedContributors =
[ [
new() { Name = "Nagaoka Taichi", Role = SourceMangaContributorRole.Author } new() { Name = "Nagaoka Taichi", Role = ContributorRole.Author }
]; ];
manga.Contributors.ShouldBeEquivalentTo(expectedContributors); manga.Contributors.ShouldBeEquivalentTo(expectedContributors);

View File

@@ -1,7 +1,9 @@
using MangaReader.WinUI.ViewModels; using MangaReader.Core.Data;
using MangaReader.WinUI.ViewModels;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using System; using System;
using System.IO;
namespace MangaReader.WinUI; namespace MangaReader.WinUI;
@@ -22,6 +24,11 @@ public partial class App : Application
services.AddMangaReader(); services.AddMangaReader();
ServiceProvider = services.BuildServiceProvider(); ServiceProvider = services.BuildServiceProvider();
// Ensure the database is created
using var scope = ServiceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<MangaContext>();
dbContext.Database.EnsureCreated();
} }
public App() public App()

View File

@@ -51,7 +51,7 @@
<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.250606001" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.9" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="SkiaSharp" Version="3.119.0" /> <PackageReference Include="SkiaSharp" Version="3.119.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -67,6 +67,9 @@
<Page Update="Resources\Fonts.xaml"> <Page Update="Resources\Fonts.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>
<Page Update="Views\LibraryView.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Views\SearchView.xaml"> <Page Update="Views\SearchView.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>

View File

@@ -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
{
}

View File

@@ -1,5 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using MangaReader.Core.Metadata;
using MangaReader.Core.Pipeline;
using MangaReader.Core.Search; using MangaReader.Core.Search;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
@@ -16,7 +18,10 @@ using System.Windows.Input;
namespace MangaReader.WinUI.ViewModels; 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(); private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
@@ -65,6 +70,7 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator)
} }
public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync); public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync);
//public ICommand ImportCommand => new AsyncRelayCommand(ImportAsync);
public async Task SearchAsync() public async Task SearchAsync()
{ {
@@ -87,6 +93,8 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator)
ObservableMangaSearchResult mangaSearchResult = new() ObservableMangaSearchResult mangaSearchResult = new()
{ {
Source = searchResult.Source,
Url = searchResult.Url,
Title = searchResult.Title, Title = searchResult.Title,
Thumbnail = searchResult.Thumbnail, Thumbnail = searchResult.Thumbnail,
Description = searchResult.Description, Description = searchResult.Description,
@@ -124,10 +132,30 @@ public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator)
return bitmap; 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 partial class ObservableMangaSearchResult : ObservableObject
{ {
public required string Source { get; init; }
public required string Url { get; init; }
public string? Title { get; init; } public string? Title { get; init; }
public string? Description { get; init; } public string? Description { get; init; }
public string? Thumbnail { get; init; } public string? Thumbnail { get; init; }

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="MangaReader.WinUI.Views.LibraryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MangaReader.WinUI.Views"
xmlns:vm="using:MangaReader.WinUI.ViewModels"
xmlns:search="using:MangaReader.Core.Search"
xmlns:UI="using:CommunityToolkit.WinUI"
xmlns:Controls="using:CommunityToolkit.WinUI.Controls"
xmlns:Media="using:CommunityToolkit.WinUI.Media"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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}"
mc:Ignorable="d">
<Grid Padding="0" RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Border HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Padding="10">
<TextBlock Text="Library" Width="300"></TextBlock>
</StackPanel>
</Border>
<!--<ScrollViewer Grid.Row="1" RenderTransformOrigin=".5,.5" Padding="50">
<ScrollViewer.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1" />
</ScrollViewer.RenderTransform>
<ItemsRepeater ItemsSource="{Binding SearchResults2, Mode=OneWay}" ItemTemplate="{StaticResource MangaSearchResultTemplate}">
<ItemsRepeater.Layout>
<UniformGridLayout MinRowSpacing="50" MinColumnSpacing="50" ItemsStretch="Fill" MinItemWidth="800"></UniformGridLayout>
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>-->
</Grid>
</UserControl>

View File

@@ -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;
}
}

View File

@@ -34,6 +34,7 @@
<RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{x:Bind Title, Mode=OneTime}" 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=OneTime}" ItemTemplate="{StaticResource GenreTemplate}"> <ItemsControl Grid.Row="1" ItemsSource="{x:Bind Genres, Mode=OneTime}" ItemTemplate="{StaticResource GenreTemplate}">
@@ -43,10 +44,17 @@
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
</ItemsControl> </ItemsControl>
<ScrollViewer Grid.Row="3"> <ScrollViewer Grid.Row="2">
<TextBlock Text="{x:Bind Description}" Foreground="{StaticResource TextFillColorSecondaryBrush}" FontSize="16" TextWrapping="Wrap" LineStackingStrategy="BlockLineHeight" LineHeight="22"></TextBlock> <TextBlock Text="{x:Bind Description}" Foreground="{StaticResource TextFillColorSecondaryBrush}" FontSize="16" TextWrapping="Wrap" LineStackingStrategy="BlockLineHeight" LineHeight="22"></TextBlock>
</ScrollViewer> </ScrollViewer>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button
Content="Import"
MinWidth="100"
Tag="{x:Bind}"
Click="Button_Click">
</Button>
</StackPanel>
</Grid> </Grid>
</Grid> </Grid>
</DataTemplate> </DataTemplate>

View File

@@ -1,12 +1,30 @@
using MangaReader.Core.Search; using MangaReader.WinUI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System.Threading;
using System.Threading.Tasks;
namespace MangaReader.WinUI.Views; namespace MangaReader.WinUI.Views;
public sealed partial class SearchView : UserControl public sealed partial class SearchView : UserControl
{ {
private readonly SearchViewModel viewModel;
public SearchView() public SearchView()
{ {
InitializeComponent(); 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);
} }
} }