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
episodeMetadata.EpisodeId, episodeMetadata.EpisodeId,
episodeMetadata.ReleaseDate ?? DateTime.MinValue, episodeMetadata.ReleaseDate ?? DateTime.MinValue,
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title).IfNone(string.Empty), 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, episodeMetadata.Title,
$"Episode {episodeMetadata.Episode.EpisodeNumber}", episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Map(em => em.Plot).IfNone(string.Empty),
episodeMetadata.Episode.EpisodeNumber.ToString(), GetThumbnail(episodeMetadata));
GetThumbnail(episodeMetadata),
episodeMetadata.Episode.EpisodeNumber.ToString());
internal static MovieCardViewModel ProjectToViewModel(MovieMetadata movieMetadata) => internal static MovieCardViewModel ProjectToViewModel(MovieMetadata movieMetadata) =>
new( new(

13
ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs

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

18
ErsatzTV.Application/Television/Mapper.cs

@ -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.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.Year?.ToString() ?? string.Empty).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(m => m.Plot ?? 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) => internal static TelevisionSeasonViewModel ProjectToViewModel(Season season) =>
new( new(
@ -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.Title ?? string.Empty).IfNone(string.Empty),
season.Show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? 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.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) => internal static TelevisionEpisodeViewModel ProjectToViewModel(Episode episode) =>
new( new(
@ -32,12 +34,14 @@ namespace ErsatzTV.Application.Television
episode.EpisodeMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty), episode.EpisodeMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
episode.EpisodeMetadata.HeadOrNone().Map(GetThumbnail).IfNone(string.Empty)); episode.EpisodeMetadata.HeadOrNone().Map(GetThumbnail).IfNone(string.Empty));
private static string GetPoster(Metadata metadata) => private static string GetPoster(Metadata metadata) => GetArtwork(metadata, ArtworkKind.Poster);
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster))
.Match(a => a.Path, string.Empty); 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) => private static string GetArtwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail)) Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty); .Match(a => a.Path, string.Empty);
} }
} }

9
ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs

@ -1,4 +1,11 @@
namespace ErsatzTV.Application.Television 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 @@
namespace ErsatzTV.Application.Television 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
.GetOrAdd(libraryPath, file) .GetOrAdd(libraryPath, file)
.BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie)) .BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie))
.BindT(UpdateMetadata) .BindT(UpdateMetadata)
.BindT(UpdatePoster) .BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster))
.BindT(UpdateFanArt); .BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt));
maybeMovie.IfLeft( maybeMovie.IfLeft(
error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value)); error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value));
@ -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 try
{ {
await LocateArtwork(movie, ArtworkKind.Poster).IfSomeAsync( await LocateArtwork(movie, artworkKind).IfSomeAsync(
async posterFile => async posterFile =>
{ {
MovieMetadata metadata = movie.MovieMetadata.Head(); MovieMetadata metadata = movie.MovieMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Poster)) if (RefreshArtwork(posterFile, metadata, artworkKind))
{
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))
{ {
await _movieRepository.Update(movie); await _movieRepository.Update(movie);
} }

27
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

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

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

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

22
ErsatzTV/Extensions/NavigationManagerExtensions.cs

@ -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 @@
@foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired)) @foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
Link="@($"/media/tv/episodes/{card.EpisodeId}")" Link="@($"/media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
Title="@card.ShowTitle" Title="@card.ShowTitle"
Subtitle="@card.Title" Subtitle="@card.Title"
ContainerClass="media-card-episode-container mx-2" ContainerClass="media-card-episode-container mx-2"
CardClass="media-card-episode" CardClass="media-card-episode"
DeleteClicked="@RemoveEpisodeFromCollection" DeleteClicked="@(_ => RemoveEpisodeFromCollection(card))"
ArtworkKind="@ArtworkKind.Thumbnail"/> ArtworkKind="@ArtworkKind.Thumbnail"/>
} }
</MudContainer> </MudContainer>
@ -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( private async Task RemoveItemsWithConfirmation(

15
ErsatzTV/Pages/Movie.razor

@ -52,21 +52,10 @@
private MovieViewModel _movie; private MovieViewModel _movie;
private List<BreadcrumbItem> _breadcrumbs;
protected override Task OnParametersSetAsync() => RefreshData(); protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData() private Task RefreshData() =>
{ Mediator.Send(new GetMovieById(MovieId)).IfSomeAsync(vm => _movie = vm);
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 async Task AddToCollection() private async Task AddToCollection()
{ {

84
ErsatzTV/Pages/TelevisionEpisode.razor

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

12
ErsatzTV/Pages/_Host.cshtml

@ -35,5 +35,17 @@
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.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> </body>
</html> </html>

43
ErsatzTV/Shared/MainLayout.razor

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

4
ErsatzTV/wwwroot/css/site.css

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