diff --git a/MangaReader.Core/Common/Language.cs b/MangaReader.Core/Common/Language.cs new file mode 100644 index 0000000..059f1dd --- /dev/null +++ b/MangaReader.Core/Common/Language.cs @@ -0,0 +1,8 @@ +namespace MangaReader.Core.Common; + +public enum Language +{ + Japanese, + Romanji, + English +} \ No newline at end of file diff --git a/MangaReader.Core/Data/MangaContext.cs b/MangaReader.Core/Data/MangaContext.cs index fdc7dce..33a27f8 100644 --- a/MangaReader.Core/Data/MangaContext.cs +++ b/MangaReader.Core/Data/MangaContext.cs @@ -81,12 +81,12 @@ public class MangaContext(DbContextOptions options) : DbContext(options) .HasKey(mangaTitle => mangaTitle.MangaTitleId); modelBuilder.Entity() - .HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.TitleEntry }) + .HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name }) .IsUnique(); modelBuilder .Entity() - .HasIndex(mangaTitle => mangaTitle.TitleEntry); + .HasIndex(mangaTitle => mangaTitle.Name); modelBuilder .Entity() diff --git a/MangaReader.Core/Data/MangaDescription.cs b/MangaReader.Core/Data/MangaDescription.cs new file mode 100644 index 0000000..211f703 --- /dev/null +++ b/MangaReader.Core/Data/MangaDescription.cs @@ -0,0 +1,14 @@ +using MangaReader.Core.Common; + +namespace MangaReader.Core.Data; + +public class MangaDescription +{ + public int MangaTitleId { get; set; } + + public int MangaId { get; set; } + public required Manga Manga { get; set; } + + public required string Name { get; set; } + public required Language Language { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/Data/MangaTitle.cs b/MangaReader.Core/Data/MangaTitle.cs index dff039f..b931b06 100644 --- a/MangaReader.Core/Data/MangaTitle.cs +++ b/MangaReader.Core/Data/MangaTitle.cs @@ -7,6 +7,6 @@ public class MangaTitle public int MangaId { get; set; } public required Manga Manga { get; set; } - public required string TitleEntry { get; set; } + public required string Name { get; set; } public TitleType TitleType { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/IMangaPipeline.cs b/MangaReader.Core/Pipeline/IMangaPipeline.cs index 8d73838..313ef66 100644 --- a/MangaReader.Core/Pipeline/IMangaPipeline.cs +++ b/MangaReader.Core/Pipeline/IMangaPipeline.cs @@ -1,8 +1,6 @@ -using MangaReader.Core.Metadata; - -namespace MangaReader.Core.Pipeline; +namespace MangaReader.Core.Pipeline; public interface IMangaPipeline { - Task RunAsync(SourceManga mangaDto); + Task RunAsync(MangaPipelineRequest request); } \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/MangaPipeline.cs b/MangaReader.Core/Pipeline/MangaPipeline.cs index 24f9e09..e33fbc2 100644 --- a/MangaReader.Core/Pipeline/MangaPipeline.cs +++ b/MangaReader.Core/Pipeline/MangaPipeline.cs @@ -7,8 +7,12 @@ namespace MangaReader.Core.Pipeline; public partial class MangaPipeline(MangaContext context) : IMangaPipeline { - public async Task RunAsync(SourceManga sourceManga) + public async Task RunAsync(MangaPipelineRequest request) { + string sourceName = request.SourceName; + SourceManga sourceManga = request.SourceManga; + + Source source = await GetOrAddSourceAsync(sourceName); Manga manga = await GetOrAddMangaAsync(sourceManga); foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) @@ -29,6 +33,23 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline context.SaveChanges(); } + private async Task GetOrAddSourceAsync(string sourceName) + { + Source? source = await context.Sources.FirstOrDefaultAsync(s => s.Name == sourceName); + + if (source != null) + return source; + + source = new() + { + Name = sourceName + }; + + context.Sources.Add(source); + + return source; + } + private async Task GetOrAddMangaAsync(SourceManga sourceManga) { Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga => manga.Title == sourceManga.Title); @@ -65,7 +86,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle) { MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt => - mt.Manga == manga && mt.TitleEntry == sourceMangaTitle.Title); + mt.Manga == manga && mt.Name == sourceMangaTitle.Title); if (mangaTitle != null) return; @@ -73,7 +94,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline mangaTitle = new() { Manga = manga, - TitleEntry = sourceMangaTitle.Title, + Name = sourceMangaTitle.Title, }; context.MangaTitles.Add(mangaTitle); diff --git a/MangaReader.Core/Pipeline/MangaPipelineRequest.cs b/MangaReader.Core/Pipeline/MangaPipelineRequest.cs new file mode 100644 index 0000000..d8186c9 --- /dev/null +++ b/MangaReader.Core/Pipeline/MangaPipelineRequest.cs @@ -0,0 +1,9 @@ +using MangaReader.Core.Metadata; + +namespace MangaReader.Core.Pipeline; + +public class MangaPipelineRequest +{ + public required string SourceName { get; init; } + public required SourceManga SourceManga { get; init; } +} \ No newline at end of file diff --git a/MangaReader.Core/Search/MangaSearchResult.cs b/MangaReader.Core/Search/MangaSearchResult.cs index 19b9b15..ce89829 100644 --- a/MangaReader.Core/Search/MangaSearchResult.cs +++ b/MangaReader.Core/Search/MangaSearchResult.cs @@ -7,4 +7,5 @@ public record MangaSearchResult public string? Thumbnail { get; init; } public string? Author { get; init; } public string? Description { get; init; } + public string[] Genres { get; init; } = []; } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs index a01fb69..ee808a2 100644 --- a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs @@ -67,6 +67,7 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM { Title = title, Description = GetDescription(mangaAttributes), + Genres = GetGenres(mangaAttributes), Url = $"https://mangadex.org/title/{mangaEntity.Id}/{slug}", Thumbnail = GetThumbnail(mangaEntity, coverArtEntites) }; @@ -95,9 +96,33 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM if (attributes.Description.TryGetValue("en", out string? description)) return description; + if (attributes.Description.Count > 0) + return attributes.Description.ElementAt(0).Value; + return string.Empty; } + private static string[] GetGenres(MangaAttributes attributes) + { + if (attributes.Tags == null || attributes.Tags.Count == 0) + return []; + + List tags = []; + + foreach (TagEntity tagEntity in attributes.Tags) + { + if (tagEntity.Attributes == null) + continue; + + if (tagEntity.Attributes.Name == null || tagEntity.Attributes.Name.Count == 0) + continue; + + tags.Add(tagEntity.Attributes.Name.FirstOrDefault().Value); + } + + return [.. tags]; + } + public static string GenerateSlug(string title) { // title.ToLowerInvariant().Normalize(NormalizationForm.FormD); diff --git a/MangaReader.Tests/MangaReader.Tests.csproj b/MangaReader.Tests/MangaReader.Tests.csproj index 6ef018a..f03cb00 100644 --- a/MangaReader.Tests/MangaReader.Tests.csproj +++ b/MangaReader.Tests/MangaReader.Tests.csproj @@ -40,7 +40,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/MangaReader.Tests/Pipeline/MangaPipelineTests.cs b/MangaReader.Tests/Pipeline/MangaPipelineTests.cs new file mode 100644 index 0000000..79c345f --- /dev/null +++ b/MangaReader.Tests/Pipeline/MangaPipelineTests.cs @@ -0,0 +1,53 @@ +using MangaReader.Core.Data; +using MangaReader.Core.Metadata; +using MangaReader.Core.Pipeline; +using MangaReader.Tests.Utilities; + +namespace MangaReader.Tests.Pipeline; + +public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture +{ + [Fact] + public async Task RunAsync_SavesMangaTitlesChaptersGenres() + { + using MangaContext context = factory.CreateContext(); + var pipeline = new MangaPipeline(context); + + var sourceManga = new SourceManga + { + Title = "Fullmetal Alchemist", + AlternateTitles = + [ + new() + { + Title = "Hagane no Renkinjutsushi", + Language = SourceMangaLanguage.Romanji + } + ], + Genres = ["Action", "Adventure"], + Chapters = + [ + new() + { + Number = 1, + Title = "The Two Alchemists", + Volume = 1, + Url = string.Empty + } + ] + }; + + MangaPipelineRequest request = new() + { + SourceName = "MySource", + SourceManga = sourceManga + }; + + await pipeline.RunAsync(request); + + Assert.Single(context.Mangas); + Assert.Single(context.MangaTitles); + Assert.Equal(2, context.Genres.Count()); + Assert.Single(context.MangaChapters); + } +} \ No newline at end of file diff --git a/MangaReader.Tests/Utilities/TestDbContextFactory.cs b/MangaReader.Tests/Utilities/TestDbContextFactory.cs new file mode 100644 index 0000000..051215b --- /dev/null +++ b/MangaReader.Tests/Utilities/TestDbContextFactory.cs @@ -0,0 +1,36 @@ +using MangaReader.Core.Data; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace MangaReader.Tests.Utilities; + +public class TestDbContextFactory : IDisposable +{ + private readonly SqliteConnection _connection; + private readonly DbContextOptions _options; + + public TestDbContextFactory() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + _options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .EnableSensitiveDataLogging() // Optional: helps with debugging + .Options; + + using MangaContext context = new(_options); + context.Database.EnsureCreated(); + } + + public MangaContext CreateContext() + { + return new MangaContext(_options); + } + + public void Dispose() + { + _connection.Close(); + _connection.Dispose(); + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/App.xaml b/MangaReader.WinUI/App.xaml index 66a08fe..0cd54fc 100644 --- a/MangaReader.WinUI/App.xaml +++ b/MangaReader.WinUI/App.xaml @@ -9,6 +9,7 @@ + diff --git a/MangaReader.WinUI/Assets/Images/MangaReader.png b/MangaReader.WinUI/Assets/Images/MangaReader.png new file mode 100644 index 0000000..4d24e0c Binary files /dev/null and b/MangaReader.WinUI/Assets/Images/MangaReader.png differ diff --git a/MangaReader.WinUI/Converters/UppercaseConverter.cs b/MangaReader.WinUI/Converters/UppercaseConverter.cs new file mode 100644 index 0000000..6052138 --- /dev/null +++ b/MangaReader.WinUI/Converters/UppercaseConverter.cs @@ -0,0 +1,20 @@ +using Microsoft.UI.Xaml.Data; +using System; + +namespace MangaReader.WinUI.Converters; + +public partial class UppercaseConverter : IValueConverter +{ + public object? Convert(object value, Type targetType, object parameter, string language) + { + if (value == null) + return null; + + return value.ToString().ToUpperInvariant(); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/MainWindow.xaml b/MangaReader.WinUI/MainWindow.xaml index 7c73399..240ec9a 100644 --- a/MangaReader.WinUI/MainWindow.xaml +++ b/MangaReader.WinUI/MainWindow.xaml @@ -16,9 +16,18 @@ + - + + + + + + + + +