feat: Implement book filtering and retrieval features

- Added BooksListFilterContext to manage book filtering criteria.
- Enhanced BooksService with methods to get book counts, minimum and maximum publication years, and retrieve books by letter and year.
- Created new API endpoints in BooksController for counting books, fetching minimum and maximum years, and retrieving books by letter and year.
- Introduced new request builders for handling API requests related to books by letter and year.
- Updated kiota-lock.json to reflect new API description location.
- Added localized strings for new book-related UI elements.
This commit is contained in:
2026-03-23 00:42:37 +00:00
parent 3f0cf08417
commit 16151a50c5
32 changed files with 2839 additions and 4 deletions

View File

@@ -189,6 +189,7 @@ public partial class App : PrismApplication
containerRegistry.RegisterSingleton<SoftwareReleasesService>();
containerRegistry.RegisterSingleton<IComputersListFilterContext, ComputersListFilterContext>();
containerRegistry.RegisterSingleton<IConsolesListFilterContext, ConsolesListFilterContext>();
containerRegistry.RegisterSingleton<IBooksListFilterContext, BooksListFilterContext>();
// Register ViewModels explicitly — RegisterForNavigation creates conditional
// (keyed) registrations that the ViewModelLocator can't resolve by type alone
@@ -201,6 +202,9 @@ public partial class App : PrismApplication
containerRegistry.Register<ConsolesListViewModel>();
containerRegistry.Register<CompaniesViewModel>();
containerRegistry.Register<CompanyDetailViewModel>();
containerRegistry.Register<BooksViewModel>();
containerRegistry.Register<BooksListViewModel>();
containerRegistry.Register<BookViewViewModel>();
containerRegistry.Register<MachineViewViewModel>();
containerRegistry.Register<PhotoDetailViewModel>();
containerRegistry.Register<GpusListViewModel>();
@@ -245,6 +249,9 @@ public partial class App : PrismApplication
containerRegistry.RegisterForNavigation<ConsolesListPage, ConsolesListViewModel>();
containerRegistry.RegisterForNavigation<CompaniesPage, CompaniesViewModel>();
containerRegistry.RegisterForNavigation<CompanyDetailPage, CompanyDetailViewModel>();
containerRegistry.RegisterForNavigation<BooksPage, BooksViewModel>();
containerRegistry.RegisterForNavigation<BooksListPage, BooksListViewModel>();
containerRegistry.RegisterForNavigation<BookViewPage, BookViewViewModel>();
containerRegistry.RegisterForNavigation<MachineViewPage, MachineViewViewModel>();
containerRegistry.RegisterForNavigation<PhotoDetailPage, PhotoDetailViewModel>();
containerRegistry.RegisterForNavigation<GpuListPage, GpusListViewModel>();

View File

@@ -0,0 +1,18 @@
using System;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
namespace Marechai.App.Presentation.Models;
[Bindable]
public partial class BookListItem : ObservableObject
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public int? Year { get; set; }
public string? Authors { get; set; }
public Guid? CoverGuid { get; set; }
[ObservableProperty]
private ImageSource? _coverThumbnailSource;
}

View File

@@ -0,0 +1,385 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.App.Navigation;
using Marechai.App.Presentation.Views;
using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
namespace Marechai.App.Presentation.ViewModels;
[Bindable]
public partial class BookViewViewModel : ObservableObject, IRegionAware
{
private readonly BookCoverCache _coverCache;
private readonly BooksService _booksService;
private readonly ImageSourceFactory _imageSourceFactory;
private readonly IStringLocalizer _localizer;
private readonly ILogger<BookViewViewModel> _logger;
private readonly IRegionManager _regionManager;
private string? _navigationSource;
private long _currentBookId;
private long? _sourceNavigationBookId;
[ObservableProperty]
private string _bookTitle = string.Empty;
[ObservableProperty]
private string? _nativeTitle;
[ObservableProperty]
private string? _publishedDisplay;
[ObservableProperty]
private string? _country;
[ObservableProperty]
private string? _isbn;
[ObservableProperty]
private short? _pages;
[ObservableProperty]
private int? _edition;
[ObservableProperty]
private ImageSource? _coverImageSource;
[ObservableProperty]
private bool _hasCover;
[ObservableProperty]
private string? _synopsisText;
[ObservableProperty]
private string? _previousBookTitle;
[ObservableProperty]
private long? _previousBookId;
[ObservableProperty]
private string? _sourceBookTitle;
[ObservableProperty]
private long? _sourceBookId;
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
// Visibility flags
[ObservableProperty]
private Visibility _showNativeTitle = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showPublished = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showCountry = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showIsbn = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showPages = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showEdition = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showSynopsis = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showPeople = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showCompanies = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showMachines = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showMachineFamilies = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showPreviousBook = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showSourceBook = Visibility.Collapsed;
public BookViewViewModel(ILogger<BookViewViewModel> logger, IRegionManager regionManager,
BooksService booksService, BookCoverCache coverCache,
IStringLocalizer localizer, ImageSourceFactory imageSourceFactory)
{
_logger = logger;
_regionManager = regionManager;
_booksService = booksService;
_coverCache = coverCache;
_localizer = localizer;
_imageSourceFactory = imageSourceFactory;
}
public ObservableCollection<string> People { get; } = [];
public ObservableCollection<string> Companies { get; } = [];
public ObservableCollection<string> Machines { get; } = [];
public ObservableCollection<string> MachineFamilies { get; } = [];
public bool IsNavigationTarget(NavigationContext navigationContext) => false;
public void OnNavigatedFrom(NavigationContext navigationContext) { }
public void OnNavigatedTo(NavigationContext navigationContext)
{
if(navigationContext.Parameters.TryGetValue<string>(NavParamKeys.NavigationSource, out string? source))
_navigationSource = source;
if(navigationContext.Parameters.TryGetValue<long>("SourceNavigationBookId", out long sourceBookNavId))
_sourceNavigationBookId = sourceBookNavId;
if(navigationContext.Parameters.TryGetValue<long>(NavParamKeys.BookId, out long bookId))
{
_currentBookId = bookId;
_ = LoadBookAsync(bookId);
}
}
[RelayCommand]
public Task GoBack()
{
switch(_navigationSource)
{
case nameof(BooksListViewModel):
_regionManager.RequestNavigate(RegionNames.Content, nameof(BooksListPage));
break;
case nameof(BookViewViewModel):
{
// Going back from a book-to-book navigation — return to the source book
if(_sourceNavigationBookId.HasValue)
{
var parameters = new NavigationParameters
{
{ NavParamKeys.BookId, _sourceNavigationBookId.Value },
{ NavParamKeys.NavigationSource, nameof(BooksListViewModel) }
};
_regionManager.RequestNavigate(RegionNames.Content, nameof(BookViewPage), parameters);
}
else
_regionManager.RequestNavigate(RegionNames.Content, nameof(BooksListPage));
break;
}
default:
_regionManager.RequestNavigate(RegionNames.Content, nameof(BooksPage));
break;
}
return Task.CompletedTask;
}
[RelayCommand]
public Task NavigateToPreviousBook()
{
if(PreviousBookId is null) return Task.CompletedTask;
var parameters = new NavigationParameters
{
{ NavParamKeys.BookId, PreviousBookId.Value },
{ NavParamKeys.NavigationSource, nameof(BookViewViewModel) },
{ "SourceNavigationBookId", _currentBookId }
};
_regionManager.RequestNavigate(RegionNames.Content, nameof(BookViewPage), parameters);
return Task.CompletedTask;
}
[RelayCommand]
public Task NavigateToSourceBook()
{
if(SourceBookId is null) return Task.CompletedTask;
var parameters = new NavigationParameters
{
{ NavParamKeys.BookId, SourceBookId.Value },
{ NavParamKeys.NavigationSource, nameof(BookViewViewModel) },
{ "SourceNavigationBookId", _currentBookId }
};
_regionManager.RequestNavigate(RegionNames.Content, nameof(BookViewPage), parameters);
return Task.CompletedTask;
}
[RelayCommand]
public Task LoadData()
{
HasError = false;
ErrorMessage = string.Empty;
return Task.CompletedTask;
}
public async Task LoadBookAsync(long bookId)
{
try
{
IsLoading = true;
IsDataLoaded = false;
HasError = false;
ErrorMessage = string.Empty;
People.Clear();
Companies.Clear();
Machines.Clear();
MachineFamilies.Clear();
BookDto? book = await _booksService.GetBookAsync(bookId);
if(book is null)
{
HasError = true;
ErrorMessage = _localizer["Book not found"];
IsLoading = false;
return;
}
// Populate basic information (no SortTitle)
BookTitle = book.Title ?? string.Empty;
NativeTitle = book.NativeTitle;
Country = book.Country;
Isbn = book.Isbn;
Pages = (short?)book.Pages;
Edition = book.Edition;
if(book.Published.HasValue)
PublishedDisplay = book.Published.Value.ToString("MMMM d, yyyy");
// Load cover image
HasCover = book.CoverGuid.HasValue;
if(book.CoverGuid.HasValue)
_ = LoadCoverAsync(book.CoverGuid.Value);
// Load synopsis
DocumentSynopsisDto? synopsis = await _booksService.GetBookSynopsisAsync(bookId);
if(synopsis is not null)
SynopsisText = synopsis.Text;
// Load people (by role)
List<PersonByBookDto> people = await _booksService.GetPeopleByBookAsync(bookId);
foreach(PersonByBookDto person in people)
{
string name = person.DisplayName ?? person.Alias ?? $"{person.Name} {person.Surname}".Trim();
string? roleStr = person.Role;
string display = !string.IsNullOrEmpty(roleStr) ? $"{name} ({roleStr})" : name;
People.Add(display);
}
// Load companies (by role)
List<CompanyByBookDto> companies = await _booksService.GetCompaniesByBookAsync(bookId);
foreach(CompanyByBookDto company in companies)
{
string name = company.Company ?? string.Empty;
string? roleStr = company.Role;
string display = !string.IsNullOrEmpty(roleStr) ? $"{name} ({roleStr})" : name;
Companies.Add(display);
}
// Load machines
List<BookByMachineDto> machines = await _booksService.GetMachinesByBookAsync(bookId);
foreach(BookByMachineDto machine in machines)
Machines.Add(machine.Machine ?? string.Empty);
// Load machine families
List<BookByMachineFamilyDto> families = await _booksService.GetMachineFamiliesByBookAsync(bookId);
foreach(BookByMachineFamilyDto family in families)
MachineFamilies.Add(family.MachineFamily ?? string.Empty);
// Load previous book title
PreviousBookId = book.PreviousId;
if(book.PreviousId.HasValue)
{
BookDto? prev = await _booksService.GetBookAsync(book.PreviousId.Value);
PreviousBookTitle = prev?.Title;
}
// Load source book title
SourceBookId = book.SourceId;
if(book.SourceId.HasValue)
{
BookDto? src = await _booksService.GetBookAsync(book.SourceId.Value);
SourceBookTitle = src?.Title;
}
UpdateVisibilities();
IsDataLoaded = true;
IsLoading = false;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading book {BookId}", bookId);
HasError = true;
ErrorMessage = ex.Message;
IsLoading = false;
}
}
private void UpdateVisibilities()
{
ShowNativeTitle = !string.IsNullOrEmpty(NativeTitle) ? Visibility.Visible : Visibility.Collapsed;
ShowPublished = !string.IsNullOrEmpty(PublishedDisplay) ? Visibility.Visible : Visibility.Collapsed;
ShowCountry = !string.IsNullOrEmpty(Country) ? Visibility.Visible : Visibility.Collapsed;
ShowIsbn = !string.IsNullOrEmpty(Isbn) ? Visibility.Visible : Visibility.Collapsed;
ShowPages = Pages.HasValue ? Visibility.Visible : Visibility.Collapsed;
ShowEdition = Edition.HasValue ? Visibility.Visible : Visibility.Collapsed;
ShowSynopsis = !string.IsNullOrEmpty(SynopsisText) ? Visibility.Visible : Visibility.Collapsed;
ShowPeople = People.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowCompanies = Companies.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowMachines = Machines.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowMachineFamilies = MachineFamilies.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowPreviousBook = PreviousBookId.HasValue ? Visibility.Visible : Visibility.Collapsed;
ShowSourceBook = SourceBookId.HasValue ? Visibility.Visible : Visibility.Collapsed;
}
private async Task LoadCoverAsync(Guid coverGuid)
{
try
{
Stream stream = await _coverCache.GetCoverAsync(coverGuid);
CoverImageSource = await _imageSourceFactory.CreateBitmapImageSourceAsync(stream);
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading cover for book {BookId}", _currentBookId);
}
}
}

View File

@@ -0,0 +1,224 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.App.Navigation;
using Marechai.App.Presentation.Models;
using Marechai.App.Presentation.Views;
using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Microsoft.UI.Xaml.Data;
namespace Marechai.App.Presentation.ViewModels;
[Bindable]
public partial class BooksListViewModel : ObservableObject
{
private readonly BookCoverCache _coverCache;
private readonly BooksService _booksService;
private readonly IBooksListFilterContext _filterContext;
private readonly ImageSourceFactory _imageSourceFactory;
private readonly IStringLocalizer _localizer;
private readonly ILogger<BooksListViewModel> _logger;
private readonly IRegionManager _regionManager;
[ObservableProperty]
private ObservableCollection<BookListItem> _booksList = [];
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private string _filterDescription = string.Empty;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _pageTitle = string.Empty;
public BooksListViewModel(BooksService booksService, IStringLocalizer localizer,
ILogger<BooksListViewModel> logger, IRegionManager regionManager,
IBooksListFilterContext filterContext,
BookCoverCache coverCache,
ImageSourceFactory imageSourceFactory)
{
_booksService = booksService;
_localizer = localizer;
_logger = logger;
_regionManager = regionManager;
_filterContext = filterContext;
_coverCache = coverCache;
_imageSourceFactory = imageSourceFactory;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
NavigateToBookCommand = new AsyncRelayCommand<BookListItem>(NavigateToBookAsync);
}
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<BookListItem> NavigateToBookCommand { get; }
public BookListFilterType FilterType
{
get => _filterContext.FilterType;
set => _filterContext.FilterType = value;
}
public string FilterValue
{
get => _filterContext.FilterValue;
set => _filterContext.FilterValue = value;
}
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
BooksList.Clear();
UpdateFilterDescription();
await LoadBooksFromApiAsync();
if(BooksList.Count == 0)
{
ErrorMessage = _localizer["No books found for this filter"].Value;
HasError = true;
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading books: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load books. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
private void UpdateFilterDescription()
{
switch(FilterType)
{
case BookListFilterType.All:
PageTitle = _localizer["All Books"];
FilterDescription = _localizer["Browsing all books in the database"];
break;
case BookListFilterType.Letter:
if(!string.IsNullOrEmpty(FilterValue) && FilterValue.Length == 1)
{
PageTitle = $"{_localizer["Books Starting with"]} {FilterValue}";
FilterDescription = $"{_localizer["Showing books that start with"]} {FilterValue}";
}
break;
case BookListFilterType.Year:
if(!string.IsNullOrEmpty(FilterValue) && int.TryParse(FilterValue, out int year))
{
PageTitle = $"{_localizer["Books from"]} {year}";
FilterDescription = $"{_localizer["Showing books published in"]} {year}";
}
break;
}
}
private async Task LoadBooksFromApiAsync()
{
try
{
List<BookDto> books = FilterType switch
{
BookListFilterType.Letter when FilterValue.Length == 1 =>
await _booksService.GetBooksByLetterAsync(FilterValue[0]),
BookListFilterType.Year when int.TryParse(FilterValue, out int year) =>
await _booksService.GetBooksByYearAsync(year),
_ => await _booksService.GetAllBooksAsync()
};
foreach(BookDto book in books)
{
long id = book.Id ?? 0;
int? year = book.Published?.Year;
var item = new BookListItem
{
Id = id,
Title = book.Title ?? string.Empty,
Year = year,
CoverGuid = book.CoverGuid
};
// Fire-and-forget cover thumbnail loading
if(book.CoverGuid.HasValue)
_ = LoadCoverThumbnailAsync(item, book.CoverGuid.Value);
BooksList.Add(item);
}
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading books from API");
}
}
private async Task LoadCoverThumbnailAsync(BookListItem item, Guid coverGuid)
{
try
{
Stream stream = await _coverCache.GetThumbnailAsync(coverGuid);
item.CoverThumbnailSource = await _imageSourceFactory.CreateBitmapImageSourceAsync(stream);
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading cover thumbnail for book {BookId}", item.Id);
}
}
private Task GoBackAsync()
{
_regionManager.RequestNavigate(RegionNames.Content, nameof(BooksPage));
return Task.CompletedTask;
}
private Task NavigateToBookAsync(BookListItem? book)
{
if(book is null) return Task.CompletedTask;
var parameters = new NavigationParameters
{
{ NavParamKeys.BookId, book.Id },
{ NavParamKeys.NavigationSource, nameof(BooksListViewModel) }
};
_regionManager.RequestNavigate(RegionNames.Content, nameof(BookViewPage), parameters);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,194 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.App.Navigation;
using Marechai.App.Presentation.Views;
using Marechai.App.Services;
namespace Marechai.App.Presentation.ViewModels;
public partial class BooksViewModel : ObservableObject
{
private readonly BooksService _booksService;
private readonly IBooksListFilterContext _filterContext;
private readonly IStringLocalizer _localizer;
private readonly ILogger<BooksViewModel> _logger;
private readonly IRegionManager _regionManager;
[ObservableProperty]
private int _bookCount;
[ObservableProperty]
private string _bookCountText = string.Empty;
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private ObservableCollection<char> _lettersList = [];
[ObservableProperty]
private int _maximumYear;
[ObservableProperty]
private int _minimumYear;
[ObservableProperty]
private string _yearsGridTitle = string.Empty;
[ObservableProperty]
private ObservableCollection<int> _yearsList = [];
public BooksViewModel(BooksService booksService, IStringLocalizer localizer,
ILogger<BooksViewModel> logger, IRegionManager regionManager,
IBooksListFilterContext filterContext)
{
_booksService = booksService;
_localizer = localizer;
_logger = logger;
_regionManager = regionManager;
_filterContext = filterContext;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
NavigateByLetterCommand = new AsyncRelayCommand<char>(NavigateByLetterAsync);
NavigateByYearCommand = new AsyncRelayCommand<int>(NavigateByYearAsync);
NavigateAllBooksCommand = new AsyncRelayCommand(NavigateAllBooksAsync);
Title = _localizer["Books"];
InitializeLetters();
}
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<char> NavigateByLetterCommand { get; }
public IAsyncRelayCommand<int> NavigateByYearCommand { get; }
public IAsyncRelayCommand NavigateAllBooksCommand { get; }
public string Title { get; }
private void InitializeLetters()
{
LettersList.Clear();
for(var c = 'A'; c <= 'Z'; c++) LettersList.Add(c);
}
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
YearsList.Clear();
Task<int> countTask = _booksService.GetBooksCountAsync();
Task<int> minYearTask = _booksService.GetMinimumYearAsync();
Task<int> maxYearTask = _booksService.GetMaximumYearAsync();
await Task.WhenAll(countTask, minYearTask, maxYearTask);
BookCount = countTask.Result;
MinimumYear = minYearTask.Result;
MaximumYear = maxYearTask.Result;
BookCountText = _localizer["Books in the database"];
if(MinimumYear > 0 && MaximumYear > 0)
{
for(int year = MinimumYear; year <= MaximumYear; year++) YearsList.Add(year);
YearsGridTitle = string.Format(_localizer["Browse by Year ({0} - {1})"], MinimumYear, MaximumYear);
}
if(BookCount == 0)
{
ErrorMessage = _localizer["No books found"].Value;
HasError = true;
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError("Error loading books data: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load books data. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
private async Task GoBackAsync()
{
_regionManager.RequestNavigate(RegionNames.Content, nameof(NewsPage));
}
private Task NavigateByLetterAsync(char letter)
{
try
{
_logger.LogInformation("Navigating to books by letter: {Letter}", letter);
_filterContext.FilterType = BookListFilterType.Letter;
_filterContext.FilterValue = letter.ToString();
_regionManager.RequestNavigate(RegionNames.Content, nameof(BooksListPage));
}
catch(Exception ex)
{
_logger.LogError("Error navigating to letter books: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
return Task.CompletedTask;
}
private Task NavigateByYearAsync(int year)
{
try
{
_logger.LogInformation("Navigating to books by year: {Year}", year);
_filterContext.FilterType = BookListFilterType.Year;
_filterContext.FilterValue = year.ToString();
_regionManager.RequestNavigate(RegionNames.Content, nameof(BooksListPage));
}
catch(Exception ex)
{
_logger.LogError("Error navigating to year books: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
return Task.CompletedTask;
}
private Task NavigateAllBooksAsync()
{
try
{
_logger.LogInformation("Navigating to all books");
_filterContext.FilterType = BookListFilterType.All;
_filterContext.FilterValue = string.Empty;
_regionManager.RequestNavigate(RegionNames.Content, nameof(BooksListPage));
}
catch(Exception ex)
{
_logger.LogError("Error navigating to all books: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
return Task.CompletedTask;
}
}

View File

@@ -90,7 +90,7 @@ public partial class MainViewModel : ObservableObject
// Initialize commands
NavigateToNewsCommand = new RelayCommand(() => NavigateTo(nameof(NewsPage)));
NavigateToBooksCommand = new RelayCommand(() => NavigateTo("books"));
NavigateToBooksCommand = new RelayCommand(() => NavigateTo(nameof(BooksPage)));
NavigateToCompaniesCommand = new RelayCommand(() => NavigateTo(nameof(CompaniesPage)));
NavigateToComputersCommand = new RelayCommand(() => NavigateTo(nameof(ComputersPage)));
NavigateToConsolesCommand = new RelayCommand(() => NavigateTo(nameof(ConsolesPage)));

View File

@@ -0,0 +1,425 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.BookViewPage"
x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="using:Marechai.App.Presentation.Converters"
NavigationCacheMode="Disabled"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Back Button -->
<Grid Grid.Row="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="12,12,16,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{Binding GoBackCommand}"
Style="{ThemeResource AlternateButtonStyle}"
ToolTipService.ToolTip="Go back"
Padding="8"
MinWidth="44"
MinHeight="44"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE72B;"
FontSize="16" />
</Button>
<StackPanel Grid.Column="1"
Margin="12,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding BookTitle}"
FontSize="20"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</Grid>
<!-- Main Content -->
<Grid Grid.Row="1">
<!-- Loading State -->
<StackPanel Visibility="{Binding IsLoading}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="64"
Width="64"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=LoadingText}"
FontSize="14"
TextAlignment="Center"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
<!-- Error State -->
<StackPanel Visibility="{Binding HasError}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="24"
Spacing="16"
MaxWidth="400">
<InfoBar IsOpen="True"
Severity="Error"
Title="{Binding Source={StaticResource Strings}, Path=BooksErrorInfoBar_Title}"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button
Content="{Binding Source={StaticResource Strings}, Path=RetryButton}"
Command="{Binding LoadDataCommand}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- Book Details -->
<ScrollViewer Visibility="{Binding IsDataLoaded}"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Padding="16"
Spacing="24">
<!-- Cover Image or Placeholder -->
<Grid HorizontalAlignment="Center"
MaxWidth="300"
MaxHeight="400">
<!-- Placeholder FontIcon (behind image) -->
<FontIcon Glyph="&#xE736;"
FontFamily="Segoe MDL2 Assets"
FontSize="96"
Foreground="{ThemeResource SystemBaseMediumColor}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<!-- Cover image overlays when loaded -->
<Image Source="{Binding CoverImageSource}"
Stretch="Uniform"
MaxWidth="300"
MaxHeight="400" />
</Grid>
<!-- Native Title -->
<StackPanel Visibility="{Binding ShowNativeTitle}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookNativeTitleLabel}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding NativeTitle}"
FontSize="16"
Foreground="{ThemeResource TextControlForeground}"
TextWrapping="Wrap" />
</Border>
</StackPanel>
<!-- Published Date and Country -->
<Grid ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Visibility="{Binding ShowPublished}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookPublishedLabel}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding PublishedDisplay}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1"
Visibility="{Binding ShowCountry}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookCountryLabel}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding Country}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</StackPanel>
</Grid>
<!-- ISBN, Pages, Edition -->
<Grid ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Visibility="{Binding ShowIsbn}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookIsbnLabel}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding Isbn}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1"
Visibility="{Binding ShowPages}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookPagesLabel}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding Pages}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="2"
Visibility="{Binding ShowEdition}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookEditionLabel}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding Edition}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</StackPanel>
</Grid>
<!-- Synopsis -->
<StackPanel Visibility="{Binding ShowSynopsis}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookSynopsisLabel}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding SynopsisText}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}"
TextWrapping="Wrap" />
</Border>
</StackPanel>
<!-- People / Authors -->
<StackPanel Visibility="{Binding ShowPeople}"
Spacing="12">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookAuthorsLabel}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding People}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,8">
<TextBlock Text="{Binding}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Companies / Publishers -->
<StackPanel Visibility="{Binding ShowCompanies}"
Spacing="12">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookCompaniesLabel}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding Companies}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,8">
<TextBlock Text="{Binding}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Related Machines -->
<StackPanel Visibility="{Binding ShowMachines}"
Spacing="12">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookMachinesLabel}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding Machines}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,8">
<TextBlock Text="{Binding}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Related Machine Families -->
<StackPanel Visibility="{Binding ShowMachineFamilies}"
Spacing="12">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookMachineFamiliesLabel}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding MachineFamilies}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,8">
<TextBlock Text="{Binding}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Previous Edition -->
<StackPanel Visibility="{Binding ShowPreviousBook}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookPreviousEditionLabel}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<HyperlinkButton Content="{Binding PreviousBookTitle}"
Command="{Binding NavigateToPreviousBookCommand}"
FontSize="14" />
</StackPanel>
<!-- Source Book -->
<StackPanel Visibility="{Binding ShowSourceBook}"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BookSourceLabel}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<HyperlinkButton Content="{Binding SourceBookTitle}"
Command="{Binding NavigateToSourceBookCommand}"
FontSize="14" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,13 @@
#nullable enable
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation.Views;
public sealed partial class BookViewPage : Page
{
public BookViewPage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,257 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.BooksListPage"
x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="using:Marechai.App.Presentation.Converters"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Back Button and Title -->
<Grid Grid.Row="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="16,12,16,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{Binding GoBackCommand}"
Style="{ThemeResource AlternateButtonStyle}"
ToolTipService.ToolTip="Go back"
Padding="8"
MinWidth="44"
MinHeight="44"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE72B;"
FontSize="16" />
</Button>
<StackPanel Grid.Column="1"
Margin="16,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding PageTitle}"
FontSize="20"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<TextBlock Text="{Binding FilterDescription}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
Margin="0,4,0,0" />
</StackPanel>
</Grid>
<!-- Main Content -->
<Grid Grid.Row="1">
<!-- Loading State -->
<StackPanel Visibility="{Binding IsLoading}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="64"
Width="64"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock Text="{Binding Source={StaticResource Strings}, Path=LoadingText}"
FontSize="14"
TextAlignment="Center"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
<!-- Error State -->
<StackPanel Visibility="{Binding HasError}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="24"
Spacing="16"
MaxWidth="400">
<InfoBar IsOpen="True"
Severity="Error"
Title="{Binding Source={StaticResource Strings}, Path=BooksErrorInfoBar_Title}"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="{Binding Source={StaticResource Strings}, Path=RetryButton}"
Command="{Binding LoadData}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- Books List -->
<Grid Visibility="{Binding IsDataLoaded}">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<Grid Padding="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Count Header -->
<StackPanel Grid.Row="0"
Padding="16,12"
Orientation="Horizontal"
Spacing="4">
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="{Binding Source={StaticResource Strings}, Path=ResultsText}" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="{Binding BooksList.Count}" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="{Binding Source={StaticResource Strings}, Path=BooksLabel}" />
</StackPanel>
<!-- Books List -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding BooksList}"
Padding="0"
Margin="0,8,0,0"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="0"
HorizontalAlignment="Stretch" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Padding="0"
Margin="0,0,0,8"
MinHeight="80"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
HorizontalAlignment="Stretch"
Command="{Binding DataContext.NavigateToBookCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Background="Transparent"
BorderThickness="0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid MinHeight="80"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border x:Name="ShadowBorder"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12"
Translation="0, 0, 4"
VerticalAlignment="Stretch">
<Border.Shadow>
<ThemeShadow />
</Border.Shadow>
<Grid ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Cover Thumbnail or Book Icon Placeholder -->
<Grid Grid.Column="0"
Width="48"
Height="64"
VerticalAlignment="Center">
<!-- Placeholder shown behind image -->
<FontIcon Glyph="&#xE736;"
FontFamily="Segoe MDL2 Assets"
FontSize="32"
Foreground="{ThemeResource SystemBaseMediumColor}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<!-- Cover thumbnail overlays placeholder when loaded -->
<Image Source="{Binding CoverThumbnailSource}"
Stretch="Uniform"
MaxWidth="48"
MaxHeight="64" />
</Grid>
<!-- Book Info -->
<StackPanel Grid.Column="1"
Spacing="8"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Title}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}"
TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE787;"
FontSize="14"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock FontSize="13"
Foreground="{ThemeResource SystemBaseMediumColor}">
<Run Text="{Binding Year}" />
</TextBlock>
</StackPanel>
</StackPanel>
<!-- Navigation Arrow -->
<StackPanel Grid.Column="2"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE72A;"
FontSize="18"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
</Grid>
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="ShadowBorder.Background"
Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
<Setter Target="ShadowBorder.Translation"
Value="0, -2, 8" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="ShadowBorder.Background"
Value="{ThemeResource CardBackgroundFillColorTertiaryBrush}" />
<Setter Target="ShadowBorder.Translation"
Value="0, 0, 2" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,20 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation.Views;
public sealed partial class BooksListPage : Page
{
public BooksListPage()
{
InitializeComponent();
DataContextChanged += BooksListPage_DataContextChanged;
}
private void BooksListPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(args.NewValue is BooksListViewModel vm)
vm.LoadData.Execute(null);
}
}

View File

@@ -0,0 +1,311 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.BooksPage"
x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="using:Marechai.App.Presentation.Converters"
xmlns:utu="using:Uno.Toolkit.UI"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header -->
<utu:NavigationBar Grid.Row="0"
Content="{Binding Path=Title}">
<utu:NavigationBar.MainCommand>
<AppBarButton Label="{Binding Source={StaticResource Strings}, Path=BackButton}" Icon="Back"
Command="{Binding GoBackCommand}"
AutomationProperties.Name="Go back" />
</utu:NavigationBar.MainCommand>
</utu:NavigationBar>
<!-- Content -->
<ScrollViewer Grid.Row="1">
<StackPanel Padding="16"
Spacing="24">
<!-- Book Count Display -->
<StackPanel HorizontalAlignment="Center"
Spacing="8">
<TextBlock Text="{Binding BookCountText}"
TextAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding BookCount}"
TextAlignment="Center"
FontSize="48"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- Loading State -->
<StackPanel Visibility="{Binding IsLoading}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="48"
Width="48" />
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=LoadingText}"
TextAlignment="Center"
FontSize="14" />
</StackPanel>
<!-- Error State -->
<StackPanel Visibility="{Binding HasError}"
Padding="16"
Background="{ThemeResource SystemErrorBackgroundColor}"
CornerRadius="8"
Spacing="8">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=ErrorTitle}"
FontWeight="Bold"
Foreground="{ThemeResource SystemErrorTextForegroundColor}" />
<TextBlock Text="{Binding ErrorMessage}"
Foreground="{ThemeResource SystemErrorTextForegroundColor}"
TextWrapping="Wrap" />
</StackPanel>
<!-- Main Content (visible when loaded and no error) -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="24">
<!-- Letters Grid Section -->
<StackPanel Spacing="12">
<TextBlock
Text="{Binding Source={StaticResource Strings}, Path=BrowseByLetterText}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<ItemsRepeater ItemsSource="{Binding LettersList}"
Layout="{StaticResource LettersGridLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.NavigateByLetterCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{StaticResource KeyboardKeyButtonStyle}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
<!-- Years Grid Section -->
<StackPanel Spacing="12">
<TextBlock Text="{Binding YearsGridTitle}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<ItemsRepeater ItemsSource="{Binding YearsList}"
Layout="{StaticResource YearsGridLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.NavigateByYearCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{StaticResource KeyboardKeyButtonStyle}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
<!-- All Books Button -->
<StackPanel Spacing="12">
<Button
Content="{Binding Source={StaticResource Strings}, Path=AllBooksButton}"
Padding="16,12"
HorizontalAlignment="Stretch"
FontSize="16"
FontWeight="SemiBold"
Command="{Binding NavigateAllBooksCommand}"
Style="{StaticResource AccentButtonStyle}" />
<!-- Search Field (placeholder for future implementation) -->
<TextBox
PlaceholderText="{Binding Source={StaticResource Strings}, Path=BooksSearchBox_Placeholder}"
Padding="12"
IsEnabled="False"
Opacity="0.5" />
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
<Page.Resources>
<!-- Keyboard Key Button Style -->
<Style x:Key="KeyboardKeyButtonStyle"
TargetType="Button">
<Setter Property="Foreground"
Value="#1A1A1A" />
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#D6D6D6"
Offset="0" />
<GradientStop Color="#C2C2C2"
Offset="0.55" />
<GradientStop Color="#B0B0B0"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush"
Value="#7A7A7A" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="CornerRadius"
Value="6" />
<Setter Property="Padding"
Value="14,12" />
<Setter Property="Margin"
Value="4" />
<Setter Property="FontFamily"
Value="Segoe UI" />
<Setter Property="FontWeight"
Value="SemiBold" />
<Setter Property="FontSize"
Value="15" />
<Setter Property="HorizontalAlignment"
Value="Stretch" />
<Setter Property="VerticalAlignment"
Value="Stretch" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="MinWidth"
Value="52" />
<Setter Property="MinHeight"
Value="52" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Border x:Name="Shadow"
CornerRadius="6"
Background="#33000000"
Margin="2,4,4,2" />
<Border x:Name="KeyBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid>
<Border CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="#60FFFFFF"
BorderThickness="1,1,0,0" />
<Border CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="#30000000"
BorderThickness="0,0,1,1" />
<ContentPresenter x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="0"
TextWrapping="NoWrap" />
</Grid>
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="KeyBorder.Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#E0E0E0"
Offset="0" />
<GradientStop Color="#CFCFCF"
Offset="0.55" />
<GradientStop Color="#BDBDBD"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Target="KeyBorder.BorderBrush"
Value="#5F5F5F" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="KeyBorder.Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#9C9C9C"
Offset="0" />
<GradientStop Color="#A8A8A8"
Offset="0.55" />
<GradientStop Color="#B4B4B4"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Target="KeyBorder.BorderBrush"
Value="#4A4A4A" />
<Setter Target="KeyBorder.RenderTransform">
<Setter.Value>
<TranslateTransform Y="2" />
</Setter.Value>
</Setter>
<Setter Target="Shadow.Opacity"
Value="0.15" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="KeyBorder.Opacity"
Value="0.45" />
<Setter Target="ContentPresenter.Foreground"
Value="#777777" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<VisualState.Setters>
<Setter Target="KeyBorder.BorderBrush"
Value="#3A7AFE" />
<Setter Target="KeyBorder.BorderThickness"
Value="2" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unfocused" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<UniformGridLayout x:Key="LettersGridLayout"
ItemsStretch="Fill"
MinItemWidth="44"
MinItemHeight="44"
MaximumRowsOrColumns="13" />
<UniformGridLayout x:Key="YearsGridLayout"
ItemsStretch="Fill"
MinItemWidth="54"
MinItemHeight="44"
MaximumRowsOrColumns="10" />
</Page.Resources>
</Page>

View File

@@ -0,0 +1,37 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class BooksPage : Page
{
public BooksPage()
{
InitializeComponent();
DataContextChanged += BooksPage_DataContextChanged;
Loaded += BooksPage_Loaded;
}
private void BooksPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is not BooksViewModel viewModel) return;
_ = viewModel.LoadData.ExecuteAsync(null);
}
private void BooksPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(args.NewValue is BooksViewModel viewModel)
_ = viewModel.LoadData.ExecuteAsync(null);
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(DataContext is BooksViewModel viewModel)
_ = viewModel.LoadData.ExecuteAsync(null);
}
}

View File

@@ -1752,4 +1752,27 @@
<data name="FailedToLoadSoftwareReleases" xml:space="preserve"><value>Software-Veröffentlichungen konnten nicht geladen werden.</value></data>
<data name="FailedToDeleteSoftwareRelease" xml:space="preserve"><value>Software-Veröffentlichung konnte nicht gelöscht werden.</value></data>
<data name="FailedToSaveSoftwareRelease" xml:space="preserve"><value>Software-Veröffentlichung konnte nicht gespeichert werden.</value></data>
<data name="AllBooksButton" xml:space="preserve"><value>Alle Bücher</value></data>
<data name="BooksSearchBox_Placeholder" xml:space="preserve"><value>Bücher suchen...</value></data>
<data name="BooksErrorInfoBar_Title" xml:space="preserve"><value>Fehler beim Laden der Bücher</value></data>
<data name="BooksLabel" xml:space="preserve"><value>Bücher</value></data>
<data name="BookSynopsisLabel" xml:space="preserve"><value>Zusammenfassung</value></data>
<data name="BookAuthorsLabel" xml:space="preserve"><value>Autoren und Mitwirkende</value></data>
<data name="BookCompaniesLabel" xml:space="preserve"><value>Verlage und Unternehmen</value></data>
<data name="BookMachinesLabel" xml:space="preserve"><value>Zugehörige Maschinen</value></data>
<data name="BookMachineFamiliesLabel" xml:space="preserve"><value>Zugehörige Maschinenfamilien</value></data>
<data name="BookPreviousEditionLabel" xml:space="preserve"><value>Vorherige Ausgabe</value></data>
<data name="Books" xml:space="preserve"><value>Bücher</value></data>
<data name="Books in the database" xml:space="preserve"><value>Bücher in der Datenbank</value></data>
<data name="No books found" xml:space="preserve"><value>Keine Bücher gefunden</value></data>
<data name="Failed to load books data. Please try again later." xml:space="preserve"><value>Fehler beim Laden der Bücherdaten. Bitte versuchen Sie es später erneut.</value></data>
<data name="All Books" xml:space="preserve"><value>Alle Bücher</value></data>
<data name="Browsing all books in the database" xml:space="preserve"><value>Alle Bücher in der Datenbank durchsuchen</value></data>
<data name="Books Starting with" xml:space="preserve"><value>Bücher beginnend mit</value></data>
<data name="Showing books that start with" xml:space="preserve"><value>Bücher anzeigen, die beginnen mit</value></data>
<data name="Books from" xml:space="preserve"><value>Bücher aus</value></data>
<data name="Showing books published in" xml:space="preserve"><value>Bücher anzeigen, veröffentlicht in</value></data>
<data name="No books found for this filter" xml:space="preserve"><value>Keine Bücher für diesen Filter gefunden</value></data>
<data name="Failed to load books. Please try again later." xml:space="preserve"><value>Fehler beim Laden der Bücher. Bitte versuchen Sie es später erneut.</value></data>
<data name="Book not found" xml:space="preserve"><value>Buch nicht gefunden</value></data>
</root>

View File

@@ -1892,4 +1892,27 @@
<data name="FailedToLoadSoftwareReleases" xml:space="preserve"><value>Error al cargar lanzamientos de software.</value></data>
<data name="FailedToDeleteSoftwareRelease" xml:space="preserve"><value>Error al eliminar lanzamiento de software.</value></data>
<data name="FailedToSaveSoftwareRelease" xml:space="preserve"><value>Error al guardar lanzamiento de software.</value></data>
<data name="AllBooksButton" xml:space="preserve"><value>Todos los libros</value></data>
<data name="BooksSearchBox_Placeholder" xml:space="preserve"><value>Buscar libros...</value></data>
<data name="BooksErrorInfoBar_Title" xml:space="preserve"><value>Error al cargar libros</value></data>
<data name="BooksLabel" xml:space="preserve"><value>libros</value></data>
<data name="BookSynopsisLabel" xml:space="preserve"><value>Sinopsis</value></data>
<data name="BookAuthorsLabel" xml:space="preserve"><value>Autores y colaboradores</value></data>
<data name="BookCompaniesLabel" xml:space="preserve"><value>Editoriales y empresas</value></data>
<data name="BookMachinesLabel" xml:space="preserve"><value>Máquinas relacionadas</value></data>
<data name="BookMachineFamiliesLabel" xml:space="preserve"><value>Familias de máquinas relacionadas</value></data>
<data name="BookPreviousEditionLabel" xml:space="preserve"><value>Edición anterior</value></data>
<data name="Books" xml:space="preserve"><value>Libros</value></data>
<data name="Books in the database" xml:space="preserve"><value>Libros en la base de datos</value></data>
<data name="No books found" xml:space="preserve"><value>No se encontraron libros</value></data>
<data name="Failed to load books data. Please try again later." xml:space="preserve"><value>Error al cargar los datos de libros. Por favor, inténtelo de nuevo más tarde.</value></data>
<data name="All Books" xml:space="preserve"><value>Todos los libros</value></data>
<data name="Browsing all books in the database" xml:space="preserve"><value>Explorando todos los libros en la base de datos</value></data>
<data name="Books Starting with" xml:space="preserve"><value>Libros que empiezan con</value></data>
<data name="Showing books that start with" xml:space="preserve"><value>Mostrando libros que empiezan con</value></data>
<data name="Books from" xml:space="preserve"><value>Libros de</value></data>
<data name="Showing books published in" xml:space="preserve"><value>Mostrando libros publicados en</value></data>
<data name="No books found for this filter" xml:space="preserve"><value>No se encontraron libros para este filtro</value></data>
<data name="Failed to load books. Please try again later." xml:space="preserve"><value>Error al cargar los libros. Por favor, inténtelo de nuevo más tarde.</value></data>
<data name="Book not found" xml:space="preserve"><value>Libro no encontrado</value></data>
</root>

View File

@@ -1856,4 +1856,27 @@
<data name="FailedToLoadSoftwareReleases" xml:space="preserve"><value>Échec du chargement des sorties de logiciels.</value></data>
<data name="FailedToDeleteSoftwareRelease" xml:space="preserve"><value>Échec de la suppression de la sortie du logiciel.</value></data>
<data name="FailedToSaveSoftwareRelease" xml:space="preserve"><value>Échec de l'enregistrement de la sortie du logiciel.</value></data>
<data name="AllBooksButton" xml:space="preserve"><value>Tous les livres</value></data>
<data name="BooksSearchBox_Placeholder" xml:space="preserve"><value>Rechercher des livres...</value></data>
<data name="BooksErrorInfoBar_Title" xml:space="preserve"><value>Erreur de chargement des livres</value></data>
<data name="BooksLabel" xml:space="preserve"><value>livres</value></data>
<data name="BookSynopsisLabel" xml:space="preserve"><value>Synopsis</value></data>
<data name="BookAuthorsLabel" xml:space="preserve"><value>Auteurs et contributeurs</value></data>
<data name="BookCompaniesLabel" xml:space="preserve"><value>Éditeurs et entreprises</value></data>
<data name="BookMachinesLabel" xml:space="preserve"><value>Machines associées</value></data>
<data name="BookMachineFamiliesLabel" xml:space="preserve"><value>Familles de machines associées</value></data>
<data name="BookPreviousEditionLabel" xml:space="preserve"><value>Édition précédente</value></data>
<data name="Books" xml:space="preserve"><value>Livres</value></data>
<data name="Books in the database" xml:space="preserve"><value>Livres dans la base de données</value></data>
<data name="No books found" xml:space="preserve"><value>Aucun livre trouvé</value></data>
<data name="Failed to load books data. Please try again later." xml:space="preserve"><value>Erreur de chargement des données des livres. Veuillez réessayer plus tard.</value></data>
<data name="All Books" xml:space="preserve"><value>Tous les livres</value></data>
<data name="Browsing all books in the database" xml:space="preserve"><value>Parcourir tous les livres dans la base de données</value></data>
<data name="Books Starting with" xml:space="preserve"><value>Livres commençant par</value></data>
<data name="Showing books that start with" xml:space="preserve"><value>Affichage des livres commençant par</value></data>
<data name="Books from" xml:space="preserve"><value>Livres de</value></data>
<data name="Showing books published in" xml:space="preserve"><value>Affichage des livres publiés en</value></data>
<data name="No books found for this filter" xml:space="preserve"><value>Aucun livre trouvé pour ce filtre</value></data>
<data name="Failed to load books. Please try again later." xml:space="preserve"><value>Erreur de chargement des livres. Veuillez réessayer plus tard.</value></data>
<data name="Book not found" xml:space="preserve"><value>Livre non trouvé</value></data>
</root>

View File

@@ -1752,4 +1752,27 @@
<data name="FailedToLoadSoftwareReleases" xml:space="preserve"><value>Editiones programmatum onerare non potuit.</value></data>
<data name="FailedToDeleteSoftwareRelease" xml:space="preserve"><value>Editionem programmatis delere non potuit.</value></data>
<data name="FailedToSaveSoftwareRelease" xml:space="preserve"><value>Editionem programmatis servare non potuit.</value></data>
<data name="AllBooksButton" xml:space="preserve"><value>Omnes libri</value></data>
<data name="BooksSearchBox_Placeholder" xml:space="preserve"><value>Libros quaerere...</value></data>
<data name="BooksErrorInfoBar_Title" xml:space="preserve"><value>Error in libris onerandis</value></data>
<data name="BooksLabel" xml:space="preserve"><value>libri</value></data>
<data name="BookSynopsisLabel" xml:space="preserve"><value>Synopsis</value></data>
<data name="BookAuthorsLabel" xml:space="preserve"><value>Auctores et contributores</value></data>
<data name="BookCompaniesLabel" xml:space="preserve"><value>Editores et societates</value></data>
<data name="BookMachinesLabel" xml:space="preserve"><value>Machinae pertinentes</value></data>
<data name="BookMachineFamiliesLabel" xml:space="preserve"><value>Familiae machinarum pertinentes</value></data>
<data name="BookPreviousEditionLabel" xml:space="preserve"><value>Editio prior</value></data>
<data name="Books" xml:space="preserve"><value>Libri</value></data>
<data name="Books in the database" xml:space="preserve"><value>Libri in basi datorum</value></data>
<data name="No books found" xml:space="preserve"><value>Nulli libri inventi</value></data>
<data name="Failed to load books data. Please try again later." xml:space="preserve"><value>Error in datis librorum onerandis. Quaeso, iterum posterius tempta.</value></data>
<data name="All Books" xml:space="preserve"><value>Omnes libri</value></data>
<data name="Browsing all books in the database" xml:space="preserve"><value>Omnes libros in basi datorum perlustrare</value></data>
<data name="Books Starting with" xml:space="preserve"><value>Libri incipientes ab</value></data>
<data name="Showing books that start with" xml:space="preserve"><value>Libri ostenduntur qui incipiunt ab</value></data>
<data name="Books from" xml:space="preserve"><value>Libri ab anno</value></data>
<data name="Showing books published in" xml:space="preserve"><value>Libri ostenduntur editi anno</value></data>
<data name="No books found for this filter" xml:space="preserve"><value>Nulli libri inventi pro hoc filtro</value></data>
<data name="Failed to load books. Please try again later." xml:space="preserve"><value>Error in libris onerandis. Quaeso, iterum posterius tempta.</value></data>
<data name="Book not found" xml:space="preserve"><value>Liber non inventus</value></data>
</root>

View File

@@ -1752,4 +1752,27 @@
<data name="FailedToLoadSoftwareReleases" xml:space="preserve"><value>Falha ao carregar lançamentos de software.</value></data>
<data name="FailedToDeleteSoftwareRelease" xml:space="preserve"><value>Falha ao excluir lançamento de software.</value></data>
<data name="FailedToSaveSoftwareRelease" xml:space="preserve"><value>Falha ao salvar lançamento de software.</value></data>
<data name="AllBooksButton" xml:space="preserve"><value>Todos os livros</value></data>
<data name="BooksSearchBox_Placeholder" xml:space="preserve"><value>Pesquisar livros...</value></data>
<data name="BooksErrorInfoBar_Title" xml:space="preserve"><value>Erro ao carregar livros</value></data>
<data name="BooksLabel" xml:space="preserve"><value>livros</value></data>
<data name="BookSynopsisLabel" xml:space="preserve"><value>Sinopse</value></data>
<data name="BookAuthorsLabel" xml:space="preserve"><value>Autores e colaboradores</value></data>
<data name="BookCompaniesLabel" xml:space="preserve"><value>Editoras e empresas</value></data>
<data name="BookMachinesLabel" xml:space="preserve"><value>Máquinas relacionadas</value></data>
<data name="BookMachineFamiliesLabel" xml:space="preserve"><value>Famílias de máquinas relacionadas</value></data>
<data name="BookPreviousEditionLabel" xml:space="preserve"><value>Edição anterior</value></data>
<data name="Books" xml:space="preserve"><value>Livros</value></data>
<data name="Books in the database" xml:space="preserve"><value>Livros no banco de dados</value></data>
<data name="No books found" xml:space="preserve"><value>Nenhum livro encontrado</value></data>
<data name="Failed to load books data. Please try again later." xml:space="preserve"><value>Erro ao carregar os dados dos livros. Por favor, tente novamente mais tarde.</value></data>
<data name="All Books" xml:space="preserve"><value>Todos os livros</value></data>
<data name="Browsing all books in the database" xml:space="preserve"><value>Navegando por todos os livros no banco de dados</value></data>
<data name="Books Starting with" xml:space="preserve"><value>Livros começando com</value></data>
<data name="Showing books that start with" xml:space="preserve"><value>Mostrando livros que começam com</value></data>
<data name="Books from" xml:space="preserve"><value>Livros de</value></data>
<data name="Showing books published in" xml:space="preserve"><value>Mostrando livros publicados em</value></data>
<data name="No books found for this filter" xml:space="preserve"><value>Nenhum livro encontrado para este filtro</value></data>
<data name="Failed to load books. Please try again later." xml:space="preserve"><value>Erro ao carregar os livros. Por favor, tente novamente mais tarde.</value></data>
<data name="Book not found" xml:space="preserve"><value>Livro não encontrado</value></data>
</root>

View File

@@ -2098,4 +2098,27 @@
<data name="FailedToLoadSoftwareReleases" xml:space="preserve"><value>Failed to load software releases.</value></data>
<data name="FailedToDeleteSoftwareRelease" xml:space="preserve"><value>Failed to delete software release.</value></data>
<data name="FailedToSaveSoftwareRelease" xml:space="preserve"><value>Failed to save software release.</value></data>
<data name="AllBooksButton" xml:space="preserve"><value>All Books</value></data>
<data name="BooksSearchBox_Placeholder" xml:space="preserve"><value>Search books...</value></data>
<data name="BooksErrorInfoBar_Title" xml:space="preserve"><value>Error loading books</value></data>
<data name="BooksLabel" xml:space="preserve"><value>books</value></data>
<data name="BookSynopsisLabel" xml:space="preserve"><value>Synopsis</value></data>
<data name="BookAuthorsLabel" xml:space="preserve"><value>Authors &amp; Contributors</value></data>
<data name="BookCompaniesLabel" xml:space="preserve"><value>Publishers &amp; Companies</value></data>
<data name="BookMachinesLabel" xml:space="preserve"><value>Related Machines</value></data>
<data name="BookMachineFamiliesLabel" xml:space="preserve"><value>Related Machine Families</value></data>
<data name="BookPreviousEditionLabel" xml:space="preserve"><value>Previous Edition</value></data>
<data name="Books" xml:space="preserve"><value>Books</value></data>
<data name="Books in the database" xml:space="preserve"><value>Books in the database</value></data>
<data name="No books found" xml:space="preserve"><value>No books found</value></data>
<data name="Failed to load books data. Please try again later." xml:space="preserve"><value>Failed to load books data. Please try again later.</value></data>
<data name="All Books" xml:space="preserve"><value>All Books</value></data>
<data name="Browsing all books in the database" xml:space="preserve"><value>Browsing all books in the database</value></data>
<data name="Books Starting with" xml:space="preserve"><value>Books Starting with</value></data>
<data name="Showing books that start with" xml:space="preserve"><value>Showing books that start with</value></data>
<data name="Books from" xml:space="preserve"><value>Books from</value></data>
<data name="Showing books published in" xml:space="preserve"><value>Showing books published in</value></data>
<data name="No books found for this filter" xml:space="preserve"><value>No books found for this filter</value></data>
<data name="Failed to load books. Please try again later." xml:space="preserve"><value>Failed to load books. Please try again later.</value></data>
<data name="Book not found" xml:space="preserve"><value>Book not found</value></data>
</root>

View File

@@ -0,0 +1,20 @@
namespace Marechai.App.Services;
public interface IBooksListFilterContext
{
BookListFilterType FilterType { get; set; }
string FilterValue { get; set; }
}
public class BooksListFilterContext : IBooksListFilterContext
{
public BookListFilterType FilterType { get; set; } = BookListFilterType.All;
public string FilterValue { get; set; } = string.Empty;
}
public enum BookListFilterType
{
All,
Letter,
Year
}

View File

@@ -19,6 +19,104 @@ public class BooksService
_logger = logger;
}
// --- User-facing browsing methods ---
public async Task<int> GetBooksCountAsync()
{
try
{
int? result = await _apiClient.Books.Count.GetAsync();
return result ?? 0;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching books count");
return 0;
}
}
public async Task<int> GetMinimumYearAsync()
{
try
{
int? result = await _apiClient.Books.MinimumYear.GetAsync();
return result ?? 0;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching minimum year");
return 0;
}
}
public async Task<int> GetMaximumYearAsync()
{
try
{
int? result = await _apiClient.Books.MaximumYear.GetAsync();
return result ?? 0;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching maximum year");
return 0;
}
}
public async Task<List<BookDto>> GetBooksByLetterAsync(char letter)
{
try
{
List<BookDto>? books = await _apiClient.Books.ByLetter[letter.ToString()].GetAsync();
return books ?? [];
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching books by letter '{Letter}'", letter);
return [];
}
}
public async Task<List<BookDto>> GetBooksByYearAsync(int year)
{
try
{
List<BookDto>? books = await _apiClient.Books.ByYear[year].GetAsync();
return books ?? [];
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching books by year {Year}", year);
return [];
}
}
public async Task<DocumentSynopsisDto?> GetBookSynopsisAsync(long bookId)
{
try
{
return await _apiClient.Books[bookId].Synopsis.GetAsync();
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching synopsis for book {Id}", bookId);
return null;
}
}
// --- CRUD ---
public async Task<List<BookDto>> GetAllBooksAsync()
{
try

View File

@@ -285,7 +285,7 @@ namespace Marechai.App
ApiClientBuilder.RegisterDefaultDeserializer<FormParseNodeFactory>();
if (string.IsNullOrEmpty(RequestAdapter.BaseUrl))
{
RequestAdapter.BaseUrl = "http://localhost:5099";
RequestAdapter.BaseUrl = "http://localhost:5023";
}
PathParameters.TryAdd("baseurl", RequestAdapter.BaseUrl);
}

View File

@@ -1,7 +1,12 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Books.ByLetter;
using Marechai.App.Books.ByYear;
using Marechai.App.Books.Companies;
using Marechai.App.Books.Count;
using Marechai.App.Books.Item;
using Marechai.App.Books.MaximumYear;
using Marechai.App.Books.MinimumYear;
using Marechai.App.Books.Scans;
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
@@ -20,11 +25,36 @@ namespace Marechai.App.Books
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class BooksRequestBuilder : BaseRequestBuilder
{
/// <summary>The byLetter property</summary>
public global::Marechai.App.Books.ByLetter.ByLetterRequestBuilder ByLetter
{
get => new global::Marechai.App.Books.ByLetter.ByLetterRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The byYear property</summary>
public global::Marechai.App.Books.ByYear.ByYearRequestBuilder ByYear
{
get => new global::Marechai.App.Books.ByYear.ByYearRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The companies property</summary>
public global::Marechai.App.Books.Companies.CompaniesRequestBuilder Companies
{
get => new global::Marechai.App.Books.Companies.CompaniesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The count property</summary>
public global::Marechai.App.Books.Count.CountRequestBuilder Count
{
get => new global::Marechai.App.Books.Count.CountRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The maximumYear property</summary>
public global::Marechai.App.Books.MaximumYear.MaximumYearRequestBuilder MaximumYear
{
get => new global::Marechai.App.Books.MaximumYear.MaximumYearRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The minimumYear property</summary>
public global::Marechai.App.Books.MinimumYear.MinimumYearRequestBuilder MinimumYear
{
get => new global::Marechai.App.Books.MinimumYear.MinimumYearRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The scans property</summary>
public global::Marechai.App.Books.Scans.ScansRequestBuilder Scans
{

View File

@@ -0,0 +1,48 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Books.ByLetter.Item;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System;
namespace Marechai.App.Books.ByLetter
{
/// <summary>
/// Builds and executes requests for operations under \books\by-letter
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class ByLetterRequestBuilder : BaseRequestBuilder
{
/// <summary>Gets an item from the Marechai.App.books.byLetter.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::Marechai.App.Books.ByLetter.Item.WithCItemRequestBuilder"/></returns>
public global::Marechai.App.Books.ByLetter.Item.WithCItemRequestBuilder this[string position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
urlTplParams.Add("c", position);
return new global::Marechai.App.Books.ByLetter.Item.WithCItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.ByLetter.ByLetterRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public ByLetterRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/by-letter", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.ByLetter.ByLetterRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public ByLetterRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/by-letter", rawUrl)
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,92 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.ByLetter.Item
{
/// <summary>
/// Builds and executes requests for operations under \books\by-letter\{c}
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class WithCItemRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.ByLetter.Item.WithCItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public WithCItemRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/by-letter/{c}", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.ByLetter.Item.WithCItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public WithCItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/by-letter/{c}", rawUrl)
{
}
/// <returns>A List&lt;global::Marechai.App.Models.BookDto&gt;</returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<List<global::Marechai.App.Models.BookDto>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::Marechai.App.Models.BookDto>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
var collectionResult = await RequestAdapter.SendCollectionAsync<global::Marechai.App.Models.BookDto>(requestInfo, global::Marechai.App.Models.BookDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
return collectionResult?.AsList();
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.ByLetter.Item.WithCItemRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.ByLetter.Item.WithCItemRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.ByLetter.Item.WithCItemRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class WithCItemRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,61 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Books.ByYear.Item;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System;
namespace Marechai.App.Books.ByYear
{
/// <summary>
/// Builds and executes requests for operations under \books\by-year
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class ByYearRequestBuilder : BaseRequestBuilder
{
/// <summary>Gets an item from the Marechai.App.books.byYear.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder"/></returns>
public global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder this[int position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
urlTplParams.Add("year", position);
return new global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>Gets an item from the Marechai.App.books.byYear.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder"/></returns>
[Obsolete("This indexer is deprecated and will be removed in the next major version. Use the one with the typed parameter instead.")]
public global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder this[string position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
if (!string.IsNullOrWhiteSpace(position)) urlTplParams.Add("year", position);
return new global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.ByYear.ByYearRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public ByYearRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/by-year", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.ByYear.ByYearRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public ByYearRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/by-year", rawUrl)
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,92 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.ByYear.Item
{
/// <summary>
/// Builds and executes requests for operations under \books\by-year\{year}
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class WithYearItemRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public WithYearItemRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/by-year/{year}", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public WithYearItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/by-year/{year}", rawUrl)
{
}
/// <returns>A List&lt;global::Marechai.App.Models.BookDto&gt;</returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<List<global::Marechai.App.Models.BookDto>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::Marechai.App.Models.BookDto>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
var collectionResult = await RequestAdapter.SendCollectionAsync<global::Marechai.App.Models.BookDto>(requestInfo, global::Marechai.App.Models.BookDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
return collectionResult?.AsList();
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.ByYear.Item.WithYearItemRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class WithYearItemRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,91 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.Count
{
/// <summary>
/// Builds and executes requests for operations under \books\count
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CountRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Count.CountRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CountRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/count", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Count.CountRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CountRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/count", rawUrl)
{
}
/// <returns>A <see cref="int"/></returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<int?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<int?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendPrimitiveAsync<int?>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.Count.CountRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.Count.CountRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.Count.CountRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CountRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,91 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.MaximumYear
{
/// <summary>
/// Builds and executes requests for operations under \books\maximum-year
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class MaximumYearRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.MaximumYear.MaximumYearRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public MaximumYearRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/maximum-year", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.MaximumYear.MaximumYearRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public MaximumYearRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/maximum-year", rawUrl)
{
}
/// <returns>A <see cref="int"/></returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<int?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<int?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendPrimitiveAsync<int?>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.MaximumYear.MaximumYearRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.MaximumYear.MaximumYearRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.MaximumYear.MaximumYearRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class MaximumYearRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,91 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.MinimumYear
{
/// <summary>
/// Builds and executes requests for operations under \books\minimum-year
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class MinimumYearRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.MinimumYear.MinimumYearRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public MinimumYearRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/minimum-year", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.MinimumYear.MinimumYearRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public MinimumYearRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/minimum-year", rawUrl)
{
}
/// <returns>A <see cref="int"/></returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<int?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<int?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendPrimitiveAsync<int?>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.MinimumYear.MinimumYearRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.MinimumYear.MinimumYearRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.MinimumYear.MinimumYearRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class MinimumYearRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -1,6 +1,6 @@
{
"descriptionHash": "F3FC5617E7A71665428E4AA0614ADDFC285D8A654413C717584235C9DD36005081E280A8CD61559BA44BB138DB08B09307865C9C8B9D31241C7BD258673BD7B5",
"descriptionLocation": "http://localhost:5099/openapi/v1.json",
"descriptionHash": "724AE57924BB426C5D5FB34A738D07B85D6E2F86CBAB30EABF70114DD8FF3E4A103AE0A15C6BE3E19484B0557343A4E7B62A6580D4C3A1ECAC79793C6938D28A",
"descriptionLocation": "http://localhost:5023/openapi/v1.json",
"lockFileVersion": "1.0.0",
"kiotaVersion": "1.29.0",
"clientClassName": "ApiClient",

View File

@@ -608,4 +608,14 @@ public class LocalizedStrings
public string RegionLabel => _l["RegionLabel"];
public string PublisherLabel => _l["PublisherLabel"];
public string ReleaseDateLabel => _l["ReleaseDateLabel"];
public string AllBooksButton => _l["AllBooksButton"];
public string BooksSearchBox_Placeholder => _l["BooksSearchBox_Placeholder"];
public string BooksErrorInfoBar_Title => _l["BooksErrorInfoBar_Title"];
public string BooksLabel => _l["BooksLabel"];
public string BookSynopsisLabel => _l["BookSynopsisLabel"];
public string BookAuthorsLabel => _l["BookAuthorsLabel"];
public string BookCompaniesLabel => _l["BookCompaniesLabel"];
public string BookMachinesLabel => _l["BookMachinesLabel"];
public string BookMachineFamiliesLabel => _l["BookMachineFamiliesLabel"];
public string BookPreviousEditionLabel => _l["BookPreviousEditionLabel"];
}

View File

@@ -52,6 +52,88 @@ public class BooksController(MarechaiContext context, IConfiguration configurati
];
readonly string _assetRootPath = configuration["AssetRootPath"]!;
[HttpGet("count")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public Task<int> GetBooksCountAsync() => context.Books.CountAsync();
[HttpGet("minimum-year")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public Task<int> GetMinimumYearAsync() => context.Books
.Where(b => b.Published.HasValue &&
b.Published.Value.Year > 1000)
.MinAsync(b => b.Published.Value.Year);
[HttpGet("maximum-year")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public Task<int> GetMaximumYearAsync() => context.Books
.Where(b => b.Published.HasValue &&
b.Published.Value.Year > 1000)
.MaxAsync(b => b.Published.Value.Year);
[HttpGet("by-letter/{c}")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public Task<List<BookDto>> GetBooksByLetterAsync(char c) => context.Books
.Where(b =>
(b.SortTitle != null &&
EF.Functions.Like(b.SortTitle, $"{c}%")) ||
(b.SortTitle == null &&
EF.Functions.Like(b.Title, $"{c}%")))
.OrderBy(b => b.SortTitle)
.ThenBy(b => b.Title)
.ThenBy(b => b.Published)
.Select(b => new BookDto
{
Id = b.Id,
Title = b.Title,
NativeTitle = b.NativeTitle,
SortTitle = b.SortTitle,
Published = b.Published,
Isbn = b.Isbn,
CountryId = b.CountryId,
Pages = b.Pages,
Country = b.Country.Name,
CoverGuid = b.CoverGuid,
OriginalCoverExtension =
b.OriginalCoverExtension
})
.ToListAsync();
[HttpGet("by-year/{year:int}")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public Task<List<BookDto>> GetBooksByYearAsync(int year) => context.Books
.Where(b => b.Published != null &&
b.Published.Value.Year == year)
.OrderBy(b => b.SortTitle)
.ThenBy(b => b.Title)
.ThenBy(b => b.Published)
.Select(b => new BookDto
{
Id = b.Id,
Title = b.Title,
NativeTitle = b.NativeTitle,
SortTitle = b.SortTitle,
Published = b.Published,
Isbn = b.Isbn,
CountryId = b.CountryId,
Pages = b.Pages,
Country = b.Country.Name,
CoverGuid = b.CoverGuid,
OriginalCoverExtension =
b.OriginalCoverExtension
})
.ToListAsync();
[HttpGet]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]