Browse Source

tv ui update (#56)

* update tv show ui

* update tv season ui

* list episode details on tv season page

* remove episode page

* remove breadcrumbs

* move home link

* code cleanup

* update screenshots
pull/57/head
Jason Dove 5 years ago committed by GitHub
parent
commit
b1ecbafb6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      ErsatzTV.Application/MediaCards/Mapper.cs
  2. 13
      ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs
  3. 18
      ErsatzTV.Application/Television/Mapper.cs
  4. 9
      ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs
  5. 2
      ErsatzTV.Application/Television/TelevisionShowViewModel.cs
  6. 32
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  7. 27
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  8. 1
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  9. 22
      ErsatzTV/Extensions/NavigationManagerExtensions.cs
  10. 17
      ErsatzTV/Pages/CollectionItems.razor
  11. 15
      ErsatzTV/Pages/Movie.razor
  12. 84
      ErsatzTV/Pages/TelevisionEpisode.razor
  13. 139
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  14. 100
      ErsatzTV/Pages/TelevisionSeasonList.razor
  15. 12
      ErsatzTV/Pages/_Host.cshtml
  16. 43
      ErsatzTV/Shared/MainLayout.razor
  17. 4
      ErsatzTV/wwwroot/css/site.css
  18. BIN
      docs/media-collection.png
  19. BIN
      docs/television-show.png

9
ErsatzTV.Application/MediaCards/Mapper.cs

@ -33,11 +33,12 @@ namespace ErsatzTV.Application.MediaCards @@ -33,11 +33,12 @@ namespace ErsatzTV.Application.MediaCards
episodeMetadata.EpisodeId,
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title).IfNone(string.Empty),
episodeMetadata.Episode.Season.ShowId,
episodeMetadata.Episode.SeasonId,
episodeMetadata.Episode.EpisodeNumber,
episodeMetadata.Title,
$"Episode {episodeMetadata.Episode.EpisodeNumber}",
episodeMetadata.Episode.EpisodeNumber.ToString(),
GetThumbnail(episodeMetadata),
episodeMetadata.Episode.EpisodeNumber.ToString());
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Map(em => em.Plot).IfNone(string.Empty),
GetThumbnail(episodeMetadata));
internal static MovieCardViewModel ProjectToViewModel(MovieMetadata movieMetadata) =>
new(

13
ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs

@ -7,14 +7,15 @@ namespace ErsatzTV.Application.MediaCards @@ -7,14 +7,15 @@ namespace ErsatzTV.Application.MediaCards
int EpisodeId,
DateTime Aired,
string ShowTitle,
int ShowId,
int SeasonId,
int Episode,
string Title,
string Subtitle,
string SortTitle,
string Poster,
string Placeholder) : MediaCardViewModel(
string Plot,
string Poster) : MediaCardViewModel(
Title,
Subtitle,
SortTitle,
$"Episode {Episode}",
$"Episode {Episode}",
Poster)
{
}

18
ErsatzTV.Application/Television/Mapper.cs

@ -12,7 +12,8 @@ namespace ErsatzTV.Application.Television @@ -12,7 +12,8 @@ namespace ErsatzTV.Application.Television
show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty));
show.ShowMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(GetFanArt).IfNone(string.Empty));
internal static TelevisionSeasonViewModel ProjectToViewModel(Season season) =>
new(
@ -21,7 +22,8 @@ namespace ErsatzTV.Application.Television @@ -21,7 +22,8 @@ namespace ErsatzTV.Application.Television
season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
season.Show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty),
season.SeasonNumber == 0 ? "Specials" : $"Season {season.SeasonNumber}",
season.SeasonMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty));
season.SeasonMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty),
season.Show.ShowMetadata.HeadOrNone().Map(GetFanArt).IfNone(string.Empty));
internal static TelevisionEpisodeViewModel ProjectToViewModel(Episode episode) =>
new(
@ -32,12 +34,14 @@ namespace ErsatzTV.Application.Television @@ -32,12 +34,14 @@ namespace ErsatzTV.Application.Television
episode.EpisodeMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
episode.EpisodeMetadata.HeadOrNone().Map(GetThumbnail).IfNone(string.Empty));
private static string GetPoster(Metadata metadata) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster))
.Match(a => a.Path, string.Empty);
private static string GetPoster(Metadata metadata) => GetArtwork(metadata, ArtworkKind.Poster);
private static string GetFanArt(Metadata metadata) => GetArtwork(metadata, ArtworkKind.FanArt);
private static string GetThumbnail(Metadata metadata) => GetArtwork(metadata, ArtworkKind.Thumbnail);
private static string GetThumbnail(Metadata metadata) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail))
private static string GetArtwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
}
}

9
ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs

@ -1,4 +1,11 @@ @@ -1,4 +1,11 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionSeasonViewModel(int Id, int ShowId, string Title, string Year, string Plot, string Poster);
public record TelevisionSeasonViewModel(
int Id,
int ShowId,
string Title,
string Year,
string Name,
string Poster,
string FanArt);
}

2
ErsatzTV.Application/Television/TelevisionShowViewModel.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionShowViewModel(int Id, string Title, string Year, string Plot, string Poster);
public record TelevisionShowViewModel(int Id, string Title, string Year, string Plot, string Poster, string FanArt);
}

32
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -80,8 +80,8 @@ namespace ErsatzTV.Core.Metadata @@ -80,8 +80,8 @@ namespace ErsatzTV.Core.Metadata
.GetOrAdd(libraryPath, file)
.BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie))
.BindT(UpdateMetadata)
.BindT(UpdatePoster)
.BindT(UpdateFanArt);
.BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster))
.BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt));
maybeMovie.IfLeft(
error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value));
@ -136,37 +136,15 @@ namespace ErsatzTV.Core.Metadata @@ -136,37 +136,15 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, Movie>> UpdatePoster(Movie movie)
private async Task<Either<BaseError, Movie>> UpdateArtwork(Movie movie, ArtworkKind artworkKind)
{
try
{
await LocateArtwork(movie, ArtworkKind.Poster).IfSomeAsync(
await LocateArtwork(movie, artworkKind).IfSomeAsync(
async posterFile =>
{
MovieMetadata metadata = movie.MovieMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Poster))
{
await _movieRepository.Update(movie);
}
});
return movie;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
private async Task<Either<BaseError, Movie>> UpdateFanArt(Movie movie)
{
try
{
await LocateArtwork(movie, ArtworkKind.FanArt).IfSomeAsync(
async posterFile =>
{
MovieMetadata metadata = movie.MovieMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.FanArt))
if (RefreshArtwork(posterFile, metadata, artworkKind))
{
await _movieRepository.Update(movie);
}

27
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -56,7 +56,8 @@ namespace ErsatzTV.Core.Metadata @@ -56,7 +56,8 @@ namespace ErsatzTV.Core.Metadata
Either<BaseError, Show> maybeShow =
await FindOrCreateShow(libraryPath.Id, showFolder)
.BindT(show => UpdateMetadataForShow(show, showFolder))
.BindT(show => UpdatePosterForShow(show, showFolder));
.BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Poster))
.BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.FanArt));
await maybeShow.Match(
show => ScanSeasons(libraryPath, ffprobePath, show, showFolder),
@ -212,17 +213,18 @@ namespace ErsatzTV.Core.Metadata @@ -212,17 +213,18 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, Show>> UpdatePosterForShow(
private async Task<Either<BaseError, Show>> UpdateArtworkForShow(
Show show,
string showFolder)
string showFolder,
ArtworkKind artworkKind)
{
try
{
await LocatePosterForShow(showFolder).IfSomeAsync(
await LocateArtworkForShow(showFolder, artworkKind).IfSomeAsync(
async posterFile =>
{
ShowMetadata metadata = show.ShowMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Poster))
if (RefreshArtwork(posterFile, metadata, artworkKind))
{
await _televisionRepository.Update(show);
}
@ -298,12 +300,21 @@ namespace ErsatzTV.Core.Metadata @@ -298,12 +300,21 @@ namespace ErsatzTV.Core.Metadata
.Filter(s => _localFileSystem.FileExists(s));
}
private Option<string> LocatePosterForShow(string showFolder) =>
ImageFileExtensions
.Map(ext => $"poster.{ext}")
private Option<string> LocateArtworkForShow(string showFolder, ArtworkKind artworkKind)
{
string segment = artworkKind switch
{
ArtworkKind.Poster => "poster",
ArtworkKind.FanArt => "fanart",
_ => throw new ArgumentOutOfRangeException(nameof(artworkKind))
};
return ImageFileExtensions
.Map(ext => $"{segment}.{ext}")
.Map(f => Path.Combine(showFolder, f))
.Filter(s => _localFileSystem.FileExists(s))
.HeadOrNone();
}
private Option<string> LocatePoster(Season season, string seasonFolder)
{

1
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -94,6 +94,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -94,6 +94,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(sm => sm.Artwork)
.Include(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.OrderBy(s => s.Id)
.SingleOrDefaultAsync(s => s.Id == seasonId)
.Map(Optional);

22
ErsatzTV/Extensions/NavigationManagerExtensions.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace ErsatzTV.Extensions
{
public static class NavigationManagerExtensions
{
public static ValueTask NavigateToFragmentAsync(this NavigationManager navigationManager, IJSRuntime jSRuntime)
{
Uri uri = navigationManager.ToAbsoluteUri(navigationManager.Uri);
if (uri.Fragment.Length == 0)
{
return default;
}
return jSRuntime.InvokeVoidAsync("blazorHelpers.scrollToFragment", uri.Fragment.Substring(1));
}
}
}

17
ErsatzTV/Pages/CollectionItems.razor

@ -69,12 +69,12 @@ @@ -69,12 +69,12 @@
@foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired))
{
<MediaCard Data="@card"
Link="@($"/media/tv/episodes/{card.EpisodeId}")"
Link="@($"/media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
Title="@card.ShowTitle"
Subtitle="@card.Title"
ContainerClass="media-card-episode-container mx-2"
CardClass="media-card-episode"
DeleteClicked="@RemoveEpisodeFromCollection"
DeleteClicked="@(_ => RemoveEpisodeFromCollection(card))"
ArtworkKind="@ArtworkKind.Thumbnail"/>
}
</MudContainer>
@ -139,17 +139,14 @@ @@ -139,17 +139,14 @@
}
}
private async Task RemoveEpisodeFromCollection(MediaCardViewModel vm)
private async Task RemoveEpisodeFromCollection(TelevisionEpisodeCardViewModel episode)
{
if (vm is TelevisionEpisodeCardViewModel episode)
var request = new RemoveItemsFromCollection(Id)
{
var request = new RemoveItemsFromCollection(Id)
{
MediaItemIds = new List<int> { episode.EpisodeId }
};
MediaItemIds = new List<int> { episode.EpisodeId }
};
await RemoveItemsWithConfirmation("episode", $"{episode.ShowTitle} - {episode.Title}", request);
}
await RemoveItemsWithConfirmation("episode", $"{episode.ShowTitle} - {episode.Title}", request);
}
private async Task RemoveItemsWithConfirmation(

15
ErsatzTV/Pages/Movie.razor

@ -52,21 +52,10 @@ @@ -52,21 +52,10 @@
private MovieViewModel _movie;
private List<BreadcrumbItem> _breadcrumbs;
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
{
await Mediator.Send(new GetMovieById(MovieId))
.IfSomeAsync(vm => _movie = vm);
_breadcrumbs = new List<BreadcrumbItem>
{
new("Movies", "/media/movies"),
new($"{_movie.Title} ({_movie.Year})", null, true)
};
}
private Task RefreshData() =>
Mediator.Send(new GetMovieById(MovieId)).IfSomeAsync(vm => _movie = vm);
private async Task AddToCollection()
{

84
ErsatzTV/Pages/TelevisionEpisode.razor

@ -1,84 +0,0 @@ @@ -1,84 +0,0 @@
@page "/media/tv/episodes/{EpisodeId:int}"
@using ErsatzTV.Application.Television
@using ErsatzTV.Application.Television.Queries
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands
@inject IMediator Mediator
@inject IDialogService Dialog
@inject NavigationManager NavigationManager
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudCard Class="mb-6">
<div style="display: flex; flex-direction: row;">
@if (!string.IsNullOrWhiteSpace(_episode.Poster))
{
<MudPaper style="display: flex; flex-direction: column">
<MudCardMedia Image="@($"/artwork/thumbnails/{_episode.Poster}")" Style="flex-grow: 1; height: 220px; width: 392px;"/>
</MudPaper>
}
<MudCardContent Class="ml-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h4">@_episode.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Plot</MudText>
<MudText Style="flex-grow: 1">@_episode.Plot</MudText>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
</div>
</div>
</MudCardContent>
</div>
</MudCard>
</MudContainer>
</MudContainer>
@code {
[Parameter]
public int EpisodeId { get; set; }
private TelevisionEpisodeViewModel _episode;
private TelevisionSeasonViewModel _season;
private List<BreadcrumbItem> _breadcrumbs;
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
{
await Mediator.Send(new GetTelevisionEpisodeById(EpisodeId))
.IfSomeAsync(vm => _episode = vm);
await Mediator.Send(new GetTelevisionSeasonById(_episode.SeasonId))
.IfSomeAsync(vm => _season = vm);
_breadcrumbs = new List<BreadcrumbItem>
{
new("TV Shows", "/media/tv/shows"),
new($"{_season.Title} ({_season.Year})", $"/media/tv/shows/{_season.ShowId}"),
new(_season.Plot, $"/media/tv/seasons/{_episode.SeasonId}"),
new($"Episode {_episode.Episode}", null, true)
};
}
private async Task AddToCollection()
{
var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", _episode.Title } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{
await Mediator.Send(new AddEpisodeToCollection(collection.Id, EpisodeId));
NavigationManager.NavigateTo($"/media/collections/{collection.Id}");
}
}
}

139
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
@page "/media/tv/seasons/{SeasonId:int}"
@using ErsatzTV.Extensions
@using ErsatzTV.Application.Television
@using ErsatzTV.Application.Television.Queries
@using ErsatzTV.Application.MediaCards
@ -14,56 +15,74 @@ @@ -14,56 +15,74 @@
@inject IDialogService Dialog
@inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> Channel
@inject IJSRuntime JsRuntime
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
<div class="fanart-tint"></div>
@if (!string.IsNullOrWhiteSpace(_season.FanArt))
{
<img src="@($"/artwork/fanart/{_season.FanArt}")" alt="fan art"/>
}
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px">
<div style="display: flex; flex-direction: row;">
@if (!string.IsNullOrWhiteSpace(_season.Poster))
{
<img class="mud-elevation-2 mr-6"
style="border-radius: 4px; max-height: 440px"
src="@($"/artwork/posters/{_season.Poster}")" alt="show poster"/>
}
<div style="display: flex; flex-direction: column; height: 100%">
<MudLink Href="@($"/media/tv/shows/{_season.ShowId}")">
<MudText Typo="Typo.h2" Class="media-item-title">@_season.Title</MudText>
</MudLink>
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_season.Name</MudText>
<div>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Schedule"
OnClick="@AddToSchedule">
Add To Schedule
</MudButton>
</div>
</div>
</div>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
@foreach (TelevisionEpisodeCardViewModel episode in _data.Cards)
{
<MudCard Class="mb-6">
<div style="display: flex; flex-direction: row;">
@if (!string.IsNullOrWhiteSpace(_season.Poster))
<div id="@($"episode-{episode.EpisodeId}")" style="display: flex; flex-direction: row; scroll-margin-top: 85px">
@if (!string.IsNullOrWhiteSpace(episode.Poster))
{
<MudPaper Style="flex-shrink: 0;">
<MudCardMedia Image="@($"/artwork/posters/{_season.Poster}")" Style="height: 440px; width: 304px;"/>
<MudPaper style="display: flex; flex-direction: column">
<MudCardMedia Image="@($"/artwork/thumbnails/{episode.Poster}")" Style="flex-grow: 1; height: 220px; width: 392px;"/>
</MudPaper>
}
<MudCardContent Class="mx-3 my-3">
<MudCardContent Class="ml-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h3">@_season.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Year</MudText>
<MudText Style="flex-grow: 1">@_season.Plot</MudText>
<MudText Typo="Typo.h4">@episode.Episode. @episode.Title</MudText>
<MudText Style="flex-grow: 1">@episode.Plot</MudText>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
OnClick="@(_ => AddEpisodeToCollection(episode))">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Schedule"
OnClick="@AddToSchedule">
Add To Schedule
</MudButton>
</div>
</div>
</MudCardContent>
</div>
</MudCard>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid">
@foreach (TelevisionEpisodeCardViewModel card in _data.Cards)
{
<MediaCard Data="@card"
Placeholder="@card.Placeholder"
Link="@($"/media/tv/episodes/{card.EpisodeId}")"
AddToCollectionClicked="@AddEpisodeToCollection"
ContainerClass="media-card-episode-container mx-2"
CardClass="media-card-episode"
ArtworkKind="@ArtworkKind.Thumbnail"/>
}
</MudContainer>
}
</MudContainer>
@code {
@ -78,7 +97,13 @@ @@ -78,7 +97,13 @@
private TelevisionEpisodeCardResultsViewModel _data;
private List<BreadcrumbItem> _breadcrumbs;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await NavigationManager.NavigateToFragmentAsync(JsRuntime);
}
}
protected override Task OnParametersSetAsync() => RefreshData();
@ -88,18 +113,11 @@ @@ -88,18 +113,11 @@
.IfSomeAsync(vm => _season = vm);
_data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, _pageNumber, _pageSize));
_breadcrumbs = new List<BreadcrumbItem>
{
new("TV Shows", "/media/tv/shows"),
new($"{_season.Title} ({_season.Year})", $"/media/tv/shows/{_season.ShowId}"),
new(_season.Plot, null, true)
};
}
private async Task AddToCollection()
{
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Plot}" } };
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Name}" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
@ -114,7 +132,7 @@ @@ -114,7 +132,7 @@
private async Task AddToSchedule()
{
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Plot}" } };
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Name}" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = Dialog.Show<AddToScheduleDialog>("Add To Schedule", parameters, options);
@ -126,27 +144,24 @@ @@ -126,27 +144,24 @@
}
}
private async Task AddEpisodeToCollection(MediaCardViewModel card)
private async Task AddEpisodeToCollection(TelevisionEpisodeCardViewModel episode)
{
if (card is TelevisionEpisodeCardViewModel episode)
var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.Title } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{
var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.Title } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await Mediator.Send(request);
addResult.Match(
Left: error =>
{
Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}");
Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value);
},
Right: _ => Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success));
}
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await Mediator.Send(request);
addResult.Match(
Left: error =>
{
Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}");
Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value);
},
Right: _ => Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success));
}
}

100
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -15,51 +15,57 @@ @@ -15,51 +15,57 @@
@inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudCard Class="mb-6">
<div style="display: flex; flex-direction: row;">
@if (!string.IsNullOrWhiteSpace(_show.Poster))
{
<MudPaper Style="flex-shrink: 0;">
<MudCardMedia Image="@($"/artwork/posters/{_show.Poster}")" Style="height: 440px; width: 304px;"/>
</MudPaper>
}
<MudCardContent Class="mx-3 my-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h3">@_show.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_show.Year</MudText>
<MudText Style="flex-grow: 1">@_show.Plot</MudText>
<div>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Schedule"
OnClick="@AddToSchedule">
Add To Schedule
</MudButton>
</div>
</div>
</MudCardContent>
</div>
</MudCard>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid">
@foreach (TelevisionSeasonCardViewModel card in _data.Cards)
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
<div class="fanart-tint"></div>
@if (!string.IsNullOrWhiteSpace(_show.FanArt))
{
<img src="@($"/artwork/fanart/{_show.FanArt}")" alt="fan art"/>
}
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px">
<div style="display: flex; flex-direction: row;">
@if (!string.IsNullOrWhiteSpace(_show.Poster))
{
<MediaCard Data="@card" Placeholder="@card.Placeholder"
Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")"
AddToCollectionClicked="@AddSeasonToCollection"/>
<img class="mud-elevation-2 mr-6"
style="border-radius: 4px; max-height: 440px"
src="@($"/artwork/posters/{_show.Poster}")" alt="show poster"/>
}
</MudContainer>
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h2" Class="media-item-title">@_show.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_show.Year</MudText>
@if (!string.IsNullOrWhiteSpace(_show.Plot))
{
<MudCard Elevation="2" Class="mb-6">
<MudCardContent Class="mx-3 my-3" Style="height: 100%">
<MudText Style="flex-grow: 1">@_show.Plot</MudText>
</MudCardContent>
</MudCard>
}
<div>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Schedule"
OnClick="@AddToSchedule">
Add To Schedule
</MudButton>
</div>
</div>
</div>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid mt-8">
@foreach (TelevisionSeasonCardViewModel card in _data.Cards)
{
<MediaCard Data="@card" Placeholder="@card.Placeholder"
Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")"
AddToCollectionClicked="@AddSeasonToCollection"/>
}
</MudContainer>
@code {
@ -74,8 +80,6 @@ @@ -74,8 +80,6 @@
private TelevisionSeasonCardResultsViewModel _data;
private List<BreadcrumbItem> _breadcrumbs;
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
@ -84,12 +88,6 @@ @@ -84,12 +88,6 @@
.IfSomeAsync(vm => _show = vm);
_data = await Mediator.Send(new GetTelevisionSeasonCards(ShowId, _pageNumber, _pageSize));
_breadcrumbs = new List<BreadcrumbItem>
{
new("TV Shows", "/media/tv/shows"),
new($"{_show.Title} ({_show.Year})", null, true)
};
}
private async Task AddToCollection()

12
ErsatzTV/Pages/_Host.cshtml

@ -35,5 +35,17 @@ @@ -35,5 +35,17 @@
<script src="_framework/blazor.server.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script type="text/javascript">
window.blazorHelpers = {
scrollToFragment: (elementId) => {
const element = document.getElementById(elementId);
if (element) {
element.scrollIntoView({
behavior: 'smooth'
});
}
}
};
</script>
</body>
</html>

43
ErsatzTV/Shared/MainLayout.razor

@ -10,7 +10,9 @@ @@ -10,7 +10,9 @@
<MudLayout>
<MudAppBar Elevation="1">
<div style="min-width: 240px">
<img src="/images/ersatztv.png" alt="ErsatzTV"/>
<a href="/">
<img src="/images/ersatztv.png" alt="ErsatzTV"/>
</a>
</div>
<MudTextField T="string"
@ref=" _textField"
@ -34,7 +36,6 @@ @@ -34,7 +36,6 @@
</MudAppBar>
<MudDrawer Open="true" Elevation="2" ClipMode="DrawerClipMode.Always">
<MudNavMenu>
<MudNavLink Href="/" Match="NavLinkMatch.All">Home</MudNavLink>
<MudNavLink Href="/channels">Channels</MudNavLink>
<MudNavLink Href="/ffmpeg">FFmpeg</MudNavLink>
@* <MudNavGroup Title="Media Sources" Expanded="true"> *@
@ -66,32 +67,24 @@ @@ -66,32 +67,24 @@
private MudTextField<string> _textField;
private MudTheme _ersatzTvTheme
private MudTheme _ersatzTvTheme => new()
{
get
Palette = new Palette
{
var current = new MudTheme();
return new MudTheme
{
Palette = new Palette
{
ActionDefault = "rgba(255,255,255, 0.80)",
Primary = "#009000",
AppbarBackground = "#121212",
Background = "#272727",
DrawerBackground = "#1f1f1f",
Surface = "#1f1f1f",
DrawerText = "rgba(255,255,255, 0.80)",
TextPrimary = "rgba(255,255,255, 0.80)",
TextSecondary = "rgba(255,255,255, 0.80)",
Info = "#00c0c0",
Tertiary = "#00c000",
White = Colors.Shades.White
}
};
ActionDefault = "rgba(255,255,255, 0.80)",
Primary = "#009000",
AppbarBackground = "#121212",
Background = "#272727",
DrawerBackground = "#1f1f1f",
Surface = "#1f1f1f",
DrawerText = "rgba(255,255,255, 0.80)",
TextPrimary = "rgba(255,255,255, 0.80)",
TextSecondary = "rgba(255,255,255, 0.80)",
Info = "#00c0c0",
Tertiary = "#00c000",
White = Colors.Shades.White
}
}
};
private void OnSearchKeyDown(KeyboardEventArgs args)
{

4
ErsatzTV/wwwroot/css/site.css

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
.mud-breadcrumb-separator > span { color: inherit !important; }
.media-card-grid {
.media-card-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;

BIN
docs/media-collection.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 220 KiB

BIN
docs/television-show.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 436 KiB

Loading…
Cancel
Save