Various updates.
This commit is contained in:
@@ -4,6 +4,7 @@ public class Manga
|
||||
{
|
||||
public int MangaId { get; set; }
|
||||
public required string Slug { get; set; }
|
||||
public int? Year { get; set; }
|
||||
|
||||
public virtual ICollection<MangaCover> Covers { get; set; } = [];
|
||||
public virtual ICollection<MangaTitle> Titles { get; set; } = [];
|
||||
|
||||
@@ -8,9 +8,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,4 +14,5 @@ public class SourceManga
|
||||
public int? Votes { get; set; }
|
||||
public SourceMangaChapter[] Chapters { get; set; } = [];
|
||||
public string[] CoverArtUrls { get; set; } = [];
|
||||
public int? Year { get; set; }
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
using MangaReader.Core.Data;
|
||||
using MangaReader.Core.Metadata;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MangaReader.Core.Pipeline;
|
||||
@@ -29,7 +33,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
||||
foreach (SourceMangaDescription description in sourceManga.Descriptions)
|
||||
{
|
||||
await AddSourceDescriptionAsync(mangaSource, description);
|
||||
//await AddDescriptionAsync(mangaSource, description);
|
||||
await AddDescriptionAsync(manga, description);
|
||||
}
|
||||
|
||||
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
|
||||
@@ -87,7 +91,10 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
||||
manga.Sources.Any(mangaSource => mangaSource.Url == sourceUrl));
|
||||
|
||||
if (manga != null)
|
||||
{
|
||||
manga.Year = sourceManga.Year ?? manga.Year;
|
||||
return manga;
|
||||
}
|
||||
|
||||
manga = new()
|
||||
{
|
||||
@@ -353,6 +360,101 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
||||
}
|
||||
}
|
||||
|
||||
//public async Task RunCoversAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken)
|
||||
//{
|
||||
// SourceCover[] sourceCovers = await context.SourceCovers
|
||||
// .Where(x => x.MangaCoverId == null)
|
||||
// .ToArrayAsync(cancellationToken);
|
||||
|
||||
// foreach (SourceCover sourceCover in sourceCovers)
|
||||
// {
|
||||
// await AddOrUpdateCoverAsync(sourceCover, cancellationToken);
|
||||
// }
|
||||
//}
|
||||
|
||||
//private async Task AddOrUpdateCoverAsync(SourceCover sourceCover, CancellationToken cancellationToken)
|
||||
//{
|
||||
// HttpClient client = httpClientFactory.CreateClient();
|
||||
// client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0");
|
||||
|
||||
// using HttpResponseMessage responseMessage = await client.GetAsync(sourceCover.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
// responseMessage.EnsureSuccessStatusCode();
|
||||
|
||||
// await using Stream networkStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken);
|
||||
|
||||
// Image image = await Image.LoadAsync(networkStream, cancellationToken);
|
||||
// networkStream.Position = 0;
|
||||
|
||||
// IImageFormat imageFormat = await Image.DetectFormatAsync(networkStream, cancellationToken);
|
||||
|
||||
// // Normalize output extension (you can keep WEBP if your viewer supports it; WinUI often prefers JPEG/PNG)
|
||||
// string extension = (imageFormat.Name?.ToLowerInvariant()) switch
|
||||
// {
|
||||
// "png" => "png",
|
||||
// _ => "jpg"
|
||||
// };
|
||||
|
||||
// // Save to memory to hash & maybe convert formats
|
||||
// await using MemoryStream memoryStream = new();
|
||||
|
||||
// if (extension == "png")
|
||||
// await image.SaveAsPngAsync(memoryStream, cancellationToken);
|
||||
// else
|
||||
// await image.SaveAsJpegAsync(memoryStream, cancellationToken);
|
||||
|
||||
// memoryStream.Position = 0;
|
||||
|
||||
// // Compute hash for dedupe
|
||||
// string sha256;
|
||||
// using (var sha = SHA256.Create())
|
||||
// sha256 = Convert.ToHexString(sha.ComputeHash(memoryStream)).ToLowerInvariant();
|
||||
// memoryStream.Position = 0;
|
||||
|
||||
// // Check if we already have this exact file
|
||||
// var existing = await context.MangaCovers.FirstOrDefaultAsync(c => c.Sha256 == sha256 && c.MangaId == sc.MangaSource.MangaId, ct);
|
||||
// var existing2 = await context.MangaCovers.FirstOrDefaultAsync(c => c. == sha256 && c.MangaId == sc.MangaSource.MangaId, ct);
|
||||
|
||||
// MangaCover cover;
|
||||
|
||||
// if (existing is not null)
|
||||
// {
|
||||
// cover = existing;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // Choose file path
|
||||
// var folder = _paths.GetMangaCoverFolder(sc.MangaSource.MangaId);
|
||||
// var guid = Guid.NewGuid();
|
||||
// var fileName = $"{guid}.{ext}";
|
||||
// var fullPath = Path.Combine(folder, fileName);
|
||||
|
||||
// // Write to disk
|
||||
// await using (var fs = File.Create(fullPath))
|
||||
// await ms.CopyToAsync(fs, ct);
|
||||
|
||||
// // Create DB record
|
||||
// cover = new MangaCover
|
||||
// {
|
||||
// Manga = sc.MangaSource.Manga,
|
||||
// Guid = guid,
|
||||
// FileExtension = ext,
|
||||
// Sha256 = sha256,
|
||||
// Width = image.Width,
|
||||
// Height = image.Height,
|
||||
// IsPrimary = false
|
||||
// };
|
||||
|
||||
// context.MangaCovers.Add(cover);
|
||||
// }
|
||||
|
||||
// // Link and mark done
|
||||
// sourceCover.MangaCover = cover;
|
||||
// //sc.Status = CoverDownloadStatus.Done;
|
||||
// //sc.Error = null;
|
||||
|
||||
// await context.SaveChangesAsync(cancellationToken);
|
||||
//}
|
||||
|
||||
public async Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
SourceChapter? sourceChapter = await context.SourceChapters.FirstOrDefaultAsync(x => x.SourceChapterId == request.SourceChapterId, cancellationToken);
|
||||
|
||||
@@ -6,4 +6,5 @@ public class MangaAttributes
|
||||
public List<Dictionary<string, string>> AltTitles { get; set; } = [];
|
||||
public Dictionary<string, string> Description { get; set; } = [];
|
||||
public List<TagEntity> Tags { get; set; } = [];
|
||||
public int? Year { get; set; }
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
|
||||
@@ -68,6 +68,8 @@ public class MangaDexClientTests
|
||||
CoverArtEntity coverArtEntity = (mangaEntity.Relationships[2] as CoverArtEntity)!;
|
||||
coverArtEntity.Attributes.ShouldNotBeNull();
|
||||
coverArtEntity.Attributes.FileName.ShouldBe("6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg");
|
||||
|
||||
mangaEntity.Attributes.Year.ShouldBe(2021);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.250916003" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251003001" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="SkiaSharp" Version="3.119.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MangaReader.Core.Data;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Pipeline;
|
||||
using MangaReader.Core.Search;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
@@ -23,12 +15,107 @@ namespace MangaReader.WinUI.ViewModels;
|
||||
|
||||
public partial class LibraryViewModel(MangaContext context) : ViewModelBase
|
||||
{
|
||||
public void GetSomething()
|
||||
{
|
||||
var mangas = context.Mangas
|
||||
.Include(x => x.Sources)
|
||||
.ThenInclude(x => x.Chapters);
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
//mangas.Select(x => new MangaS)
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
private string? _keyword;
|
||||
|
||||
public string? Keyword
|
||||
{
|
||||
get
|
||||
{
|
||||
return _keyword;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _keyword, value);
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableCollection<ObservableMangaItem> _mangaItems = [];
|
||||
|
||||
public ObservableCollection<ObservableMangaItem> MangaItems
|
||||
{
|
||||
get
|
||||
{
|
||||
return _mangaItems;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _mangaItems, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync);
|
||||
//public ICommand ImportCommand => new AsyncRelayCommand(ImportAsync);
|
||||
|
||||
public async Task SearchAsync()
|
||||
{
|
||||
//if (string.IsNullOrWhiteSpace(Keyword))
|
||||
// return;
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
ObservableMangaItem[] mangaItems = await GetMangaItemsAsync(_cancellationTokenSource.Token);
|
||||
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
|
||||
{
|
||||
MangaItems = new(mangaItems);
|
||||
});
|
||||
|
||||
//MangaItems = new(mangaItems);
|
||||
}
|
||||
|
||||
public async Task<ObservableMangaItem[]> GetMangaItemsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Manga[] mangas = await context.Mangas
|
||||
.Include(x => x.Titles)
|
||||
.Include(x => x.Descriptions)
|
||||
.Include(x => x.Genres).ThenInclude(x => x.Genre)
|
||||
//.Include(x => x.Sources)
|
||||
//.ThenInclude(x => x.Chapters)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
List<ObservableMangaItem> mangaItems = [];
|
||||
|
||||
foreach (Manga manga in mangas)
|
||||
{
|
||||
ObservableMangaItem mangaItem = new()
|
||||
{
|
||||
MangaId = manga.MangaId,
|
||||
Title = manga.Titles.OrderByDescending(title => title.IsPrimary).FirstOrDefault()?.Name,
|
||||
Description = manga.Descriptions.FirstOrDefault()?.Text,
|
||||
Genres = [.. manga.Genres.Select(x => x.Genre.Name)]
|
||||
};
|
||||
|
||||
mangaItems.Add(mangaItem);
|
||||
}
|
||||
|
||||
return [.. mangaItems];
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ObservableMangaItem : ObservableObject
|
||||
{
|
||||
public int MangaId { get; init; }
|
||||
public string? Title { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? Thumbnail { get; init; }
|
||||
public string[] Genres { get; init; } = [];
|
||||
|
||||
private BitmapImage? _thumbnailBitmap;
|
||||
|
||||
public BitmapImage? ThumbnailBitmap
|
||||
{
|
||||
get
|
||||
{
|
||||
return _thumbnailBitmap;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _thumbnailBitmap, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,63 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
DataContext="{Binding Source={StaticResource Locator}, Path=LibraryViewModel}"
|
||||
d:DataContext="{d:DesignInstance Type=vm:LibraryViewModel, IsDesignTimeCreatable=True}"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<Media:AttachedCardShadow x:Key="CommonShadow" Offset="5" BlurRadius="10" Opacity=".4" />
|
||||
|
||||
<DataTemplate x:Key="MangaItemTemplate" x:DataType="vm:ObservableMangaItem">
|
||||
<Grid Padding="20" ColumnSpacing="20" Height="400" VerticalAlignment="Stretch" Background="{StaticResource CardBackgroundFillColorDefault}" CornerRadius="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" MaxWidth="300" UI:Effects.Shadow="{StaticResource CommonShadow}" VerticalAlignment="Top" HorizontalAlignment="Left">
|
||||
<Grid VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="8">
|
||||
<!--<Image Source="{x:Bind ThumbnailBitmap, Mode=OneWay}" MaxWidth="300"></Image>-->
|
||||
<Canvas Background="#19000000"></Canvas>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid Grid.Column="1" RowSpacing="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<Border>
|
||||
<TextBlock Grid.Row="0" Text="{x:Bind Title, Mode=OneTime}" FontSize="24" FontFamily="{StaticResource PoppinsSemiBold}" TextWrapping="Wrap"></TextBlock>
|
||||
</Border>
|
||||
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind Genres, Mode=OneTime}" ItemTemplate="{StaticResource GenreTemplate}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Controls:WrapPanel Orientation="Horizontal" HorizontalSpacing="10" VerticalSpacing="10" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<TextBlock Text="{x:Bind Description}" Foreground="{StaticResource TextFillColorSecondaryBrush}" FontSize="16" TextWrapping="Wrap" LineStackingStrategy="BlockLineHeight" LineHeight="22"></TextBlock>
|
||||
</ScrollViewer>
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<!--<Button
|
||||
Content="Import"
|
||||
MinWidth="100"
|
||||
Tag="{x:Bind}"
|
||||
Click="Button_Click">
|
||||
</Button>-->
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GenreTemplate" x:DataType="x:String">
|
||||
<Border>
|
||||
<TextBlock FontSize="12" Foreground="{StaticResource TextFillColorTertiary}" Text="{x:Bind Mode=OneTime, Converter={StaticResource UppercaseConverter}}" FontFamily="{StaticResource PoppinsSemiBold}"></TextBlock>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
</Page.Resources>
|
||||
<Grid Padding="0" RowSpacing="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
@@ -22,20 +77,21 @@
|
||||
</Grid.RowDefinitions>
|
||||
<Border HorizontalAlignment="Stretch">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Padding="10">
|
||||
<TextBlock Text="Library" Width="300"></TextBlock>
|
||||
<TextBox Text="{Binding Keyword, Mode=TwoWay}" Width="300"></TextBox>
|
||||
<Button Content="Search" Command="{Binding SearchCommand}"></Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--<ScrollViewer Grid.Row="1" RenderTransformOrigin=".5,.5" Padding="50">
|
||||
<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 ItemsSource="{Binding MangaItems, Mode=OneWay}" ItemTemplate="{StaticResource MangaItemTemplate}">
|
||||
<ItemsRepeater.Layout>
|
||||
<UniformGridLayout MinRowSpacing="50" MinColumnSpacing="50" ItemsStretch="Fill" MinItemWidth="800"></UniformGridLayout>
|
||||
</ItemsRepeater.Layout>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>-->
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using MangaReader.WinUI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MangaReader.WinUI.Views;
|
||||
@@ -16,4 +15,9 @@ public sealed partial class LibraryView : Page
|
||||
|
||||
viewModel = (LibraryViewModel)DataContext;
|
||||
}
|
||||
|
||||
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Task.Run(viewModel.SearchAsync);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user