Added more UI logic. Added a manga pipeline test.

This commit is contained in:
2025-06-04 02:21:30 -04:00
parent 7dbcdc6169
commit 70513559cb
21 changed files with 264 additions and 73 deletions

View File

@@ -0,0 +1,8 @@
namespace MangaReader.Core.Common;
public enum Language
{
Japanese,
Romanji,
English
}

View File

@@ -81,12 +81,12 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
.HasKey(mangaTitle => mangaTitle.MangaTitleId); .HasKey(mangaTitle => mangaTitle.MangaTitleId);
modelBuilder.Entity<MangaTitle>() modelBuilder.Entity<MangaTitle>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.TitleEntry }) .HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name })
.IsUnique(); .IsUnique();
modelBuilder modelBuilder
.Entity<MangaTitle>() .Entity<MangaTitle>()
.HasIndex(mangaTitle => mangaTitle.TitleEntry); .HasIndex(mangaTitle => mangaTitle.Name);
modelBuilder modelBuilder
.Entity<MangaTitle>() .Entity<MangaTitle>()

View File

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

View File

@@ -7,6 +7,6 @@ public class MangaTitle
public int MangaId { get; set; } public int MangaId { get; set; }
public required Manga Manga { 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; } public TitleType TitleType { get; set; }
} }

View File

@@ -1,8 +1,6 @@
using MangaReader.Core.Metadata; namespace MangaReader.Core.Pipeline;
namespace MangaReader.Core.Pipeline;
public interface IMangaPipeline public interface IMangaPipeline
{ {
Task RunAsync(SourceManga mangaDto); Task RunAsync(MangaPipelineRequest request);
} }

View File

@@ -7,8 +7,12 @@ namespace MangaReader.Core.Pipeline;
public partial class MangaPipeline(MangaContext context) : IMangaPipeline 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); Manga manga = await GetOrAddMangaAsync(sourceManga);
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
@@ -29,6 +33,23 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
context.SaveChanges(); context.SaveChanges();
} }
private async Task<Source> 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<Manga> GetOrAddMangaAsync(SourceManga sourceManga) private async Task<Manga> GetOrAddMangaAsync(SourceManga sourceManga)
{ {
Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga => manga.Title == sourceManga.Title); 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) private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle)
{ {
MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt => MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt =>
mt.Manga == manga && mt.TitleEntry == sourceMangaTitle.Title); mt.Manga == manga && mt.Name == sourceMangaTitle.Title);
if (mangaTitle != null) if (mangaTitle != null)
return; return;
@@ -73,7 +94,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
mangaTitle = new() mangaTitle = new()
{ {
Manga = manga, Manga = manga,
TitleEntry = sourceMangaTitle.Title, Name = sourceMangaTitle.Title,
}; };
context.MangaTitles.Add(mangaTitle); context.MangaTitles.Add(mangaTitle);

View File

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

View File

@@ -7,4 +7,5 @@ public record MangaSearchResult
public string? Thumbnail { get; init; } public string? Thumbnail { get; init; }
public string? Author { get; init; } public string? Author { get; init; }
public string? Description { get; init; } public string? Description { get; init; }
public string[] Genres { get; init; } = [];
} }

View File

@@ -67,6 +67,7 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
{ {
Title = title, Title = title,
Description = GetDescription(mangaAttributes), Description = GetDescription(mangaAttributes),
Genres = GetGenres(mangaAttributes),
Url = $"https://mangadex.org/title/{mangaEntity.Id}/{slug}", Url = $"https://mangadex.org/title/{mangaEntity.Id}/{slug}",
Thumbnail = GetThumbnail(mangaEntity, coverArtEntites) Thumbnail = GetThumbnail(mangaEntity, coverArtEntites)
}; };
@@ -95,9 +96,33 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
if (attributes.Description.TryGetValue("en", out string? description)) if (attributes.Description.TryGetValue("en", out string? description))
return description; return description;
if (attributes.Description.Count > 0)
return attributes.Description.ElementAt(0).Value;
return string.Empty; return string.Empty;
} }
private static string[] GetGenres(MangaAttributes attributes)
{
if (attributes.Tags == null || attributes.Tags.Count == 0)
return [];
List<string> 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) public static string GenerateSlug(string title)
{ {
// title.ToLowerInvariant().Normalize(NormalizationForm.FormD); // title.ToLowerInvariant().Normalize(NormalizationForm.FormD);

View File

@@ -40,7 +40,8 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" /> <PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Shouldly" Version="4.3.0" /> <PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />

View File

@@ -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<TestDbContextFactory>
{
[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);
}
}

View File

@@ -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<MangaContext> _options;
public TestDbContextFactory()
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
_options = new DbContextOptionsBuilder<MangaContext>()
.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();
}
}

View File

@@ -9,6 +9,7 @@
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here --> <!-- Other merged dictionaries here -->
<ResourceDictionary Source="/Resources/Converters.xaml"/>
<ResourceDictionary Source="/Resources/Fonts.xaml"/> <ResourceDictionary Source="/Resources/Fonts.xaml"/>
<ResourceDictionary Source="/Resources/Styles.xaml"/> <ResourceDictionary Source="/Resources/Styles.xaml"/>
<ResourceDictionary Source="/Resources/ViewModels.xaml"/> <ResourceDictionary Source="/Resources/ViewModels.xaml"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

View File

@@ -16,9 +16,18 @@
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<views:SearchView></views:SearchView> <Grid x:Name="AppTitleBar" Grid.Row="0" VerticalAlignment="Center" Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" x:Name="TitleBarIcon" Source="ms-appx:///Assets/Images/MangaReader.png" Width="20" Height="20" Margin="0 0 10 0" />
<TextBlock Grid.Column="1" x:Name="AppTitle" Text="{x:Bind Title, Mode=OneWay}" Style="{StaticResource CaptionTextBlockStyle}" VerticalAlignment="Center" />
</Grid>
<views:SearchView Grid.Row="1"></views:SearchView>
<!--<StackPanel Orientation="Vertical"> <!--<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Orientation="Horizontal" Spacing="10">
<TextBox Name="KeywordTextBox" Width="300"></TextBox> <TextBox Name="KeywordTextBox" Width="300"></TextBox>

View File

@@ -1,62 +1,19 @@
using MangaReader.Core.Search; using MangaReader.Core.Search;
using MangaReader.Core.Sources.MangaDex.Api; using MangaReader.Core.Sources.MangaDex.Api;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure, namespace MangaReader.WinUI;
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace MangaReader.WinUI
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : Window public sealed partial class MainWindow : Window
{ {
private readonly IMangaSearchCoordinator _mangaSearchCoordinator; private const string ApplicationTitle = "Manga Reader";
private readonly IMangaDexClient _mangaDexClient;
private CancellationTokenSource? _cancellationTokenSource;
public MainWindow(IMangaSearchCoordinator mangaSearchCoordinator, IMangaDexClient mangaDexClient) public MainWindow(IMangaSearchCoordinator mangaSearchCoordinator, IMangaDexClient mangaDexClient)
{ {
InitializeComponent(); InitializeComponent();
_mangaSearchCoordinator = mangaSearchCoordinator; Title = ApplicationTitle;
_mangaDexClient = mangaDexClient; ExtendsContentIntoTitleBar = true; // enable custom titlebar
} SetTitleBar(AppTitleBar); // set user ui element as titlebar
//private async void Button_Click(object sender, RoutedEventArgs e)
//{
// if (string.IsNullOrWhiteSpace(KeywordTextBox.Text))
// return;
// _cancellationTokenSource?.Cancel();
// _cancellationTokenSource = new();
// var result = await _mangaSearchCoordinator.SearchAsync(KeywordTextBox.Text, _cancellationTokenSource.Token);
// //Guid mangaGuid = new("a920060c-7e39-4ac1-980c-f0e605a40ae4");
// //var coverArtResult = await _mangaDexClient.GetCoverArtAsync(mangaGuid, _cancellationTokenSource.Token);
// // if ( (coverArtResult is MangaDexC)
// // {
// // }
// // if (coverArtResult.)
//}
} }
} }

View File

@@ -18,6 +18,7 @@
<None Remove="Assets\Fonts\Poppins-Medium.otf" /> <None Remove="Assets\Fonts\Poppins-Medium.otf" />
<None Remove="Assets\Fonts\Poppins-Regular.otf" /> <None Remove="Assets\Fonts\Poppins-Regular.otf" />
<None Remove="Assets\Fonts\Poppins-SemiBold.otf" /> <None Remove="Assets\Fonts\Poppins-SemiBold.otf" />
<None Remove="Assets\Images\MangaReader.png" />
<None Remove="Resources\ViewModels.xaml" /> <None Remove="Resources\ViewModels.xaml" />
</ItemGroup> </ItemGroup>
@@ -29,6 +30,7 @@
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" /> <Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" /> <Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" /> <Content Include="Assets\Wide310x150Logo.scale-200.png" />
<Content Include="MangaReader.ico" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -45,6 +47,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<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.250513003" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
@@ -53,6 +56,9 @@
<ProjectReference Include="..\MangaReader.Core\MangaReader.Core.csproj" /> <ProjectReference Include="..\MangaReader.Core\MangaReader.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Resources\Converters.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\Styles.xaml"> <Page Update="Resources\Styles.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>
@@ -84,5 +90,6 @@
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun> <PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed> <PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed> <PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<ApplicationIcon>MangaReader.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:MangaReader.WinUI.Converters">
<converters:UppercaseConverter x:Key="UppercaseConverter" />
</ResourceDictionary>

View File

@@ -7,47 +7,69 @@
xmlns:vm="using:MangaReader.WinUI.ViewModels" xmlns:vm="using:MangaReader.WinUI.ViewModels"
xmlns:search="using:MangaReader.Core.Search" xmlns:search="using:MangaReader.Core.Search"
xmlns:UI="using:CommunityToolkit.WinUI" 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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding Source={StaticResource Locator}, Path=SearchViewModel}" DataContext="{Binding Source={StaticResource Locator}, Path=SearchViewModel}"
d:DataContext="{d:DesignInstance Type=vm:SearchViewModel, IsDesignTimeCreatable=True}" d:DataContext="{d:DesignInstance Type=vm:SearchViewModel, IsDesignTimeCreatable=True}"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<Media:AttachedCardShadow x:Key="CommonShadow" Offset="5" BlurRadius="10" Opacity=".4" />
<DataTemplate x:Key="MangaSearchResultTemplate" x:DataType="search:MangaSearchResult"> <DataTemplate x:Key="MangaSearchResultTemplate" x:DataType="search:MangaSearchResult">
<Grid Padding="20" ColumnSpacing="20" MaxHeight="600" VerticalAlignment="Top" Background="{StaticResource CardBackgroundFillColorDefault}" CornerRadius="8"> <Grid Padding="20" ColumnSpacing="20" MaxHeight="400" VerticalAlignment="Stretch" Background="{StaticResource CardBackgroundFillColorDefault}" CornerRadius="8">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="300"> <Border Grid.Column="0" MaxWidth="300" UI:Effects.Shadow="{StaticResource CommonShadow}" VerticalAlignment="Top" HorizontalAlignment="Left">
<Grid VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="8"> <Grid VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="8">
<Image Source="{x:Bind Thumbnail, Mode=OneWay}" MaxWidth="300" UI:Effects.Shadow="{StaticResource CommonShadow}"></Image> <Image Source="{x:Bind Thumbnail, Mode=OneWay}" MaxWidth="300"></Image>
<Canvas Background="#19000000"></Canvas> <Canvas Background="#19000000"></Canvas>
</Grid> </Grid>
</Border> </Border>
<Grid Grid.Column="1" RowSpacing="20"> <Grid Grid.Column="1" RowSpacing="20">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{x:Bind Title}" FontSize="24" FontFamily="{StaticResource PoppinsSemiBold}" TextWrapping="Wrap"></TextBlock> <TextBlock Grid.Row="0" Text="{x:Bind Title}" FontSize="24" FontFamily="{StaticResource PoppinsSemiBold}" TextWrapping="Wrap"></TextBlock>
<ScrollViewer Grid.Row="1"> <ItemsControl Grid.Row="1" ItemsSource="{x:Bind Genres, Mode=OneWay}" ItemTemplate="{StaticResource GenreTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Controls:WrapPanel Orientation="Horizontal" HorizontalSpacing="10" VerticalSpacing="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<ScrollViewer Grid.Row="3">
<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>
</Grid> </Grid>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="GenreTemplate" x:DataType="x:String">
<Border>
<TextBlock FontSize="12" Foreground="{StaticResource TextFillColorTertiary}" Text="{x:Bind Mode=OneWay, Converter={StaticResource UppercaseConverter}}" FontFamily="{StaticResource PoppinsSemiBold}"></TextBlock>
</Border>
</DataTemplate>
</UserControl.Resources> </UserControl.Resources>
<Grid Padding="0" RowSpacing="20"> <Grid Padding="0" RowSpacing="20">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Padding="20"> <Border HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Padding="10">
<TextBox Text="{Binding Keyword, Mode=TwoWay}" Width="300"></TextBox> <TextBox Text="{Binding Keyword, Mode=TwoWay}" Width="300"></TextBox>
<Button Content="Search" Command="{Binding SearchCommand}"></Button> <Button Content="Search" Command="{Binding SearchCommand}"></Button>
</StackPanel> </StackPanel>
</Border>
<ScrollViewer Grid.Row="1" RenderTransformOrigin=".5,.5" Padding="50"> <ScrollViewer Grid.Row="1" RenderTransformOrigin=".5,.5" Padding="50">
<ScrollViewer.RenderTransform> <ScrollViewer.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1" /> <ScaleTransform ScaleX="1" ScaleY="1" />