Files
ersatztv/ErsatzTV/Pages/TelevisionEpisodeList.razor
T

262 lines
12 KiB
Plaintext

@page "/media/tv/seasons/{SeasonId:int}"
@using ErsatzTV.Extensions
@using ErsatzTV.Application.Television
@using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.ProgramSchedules
@implements IDisposable
@inject IMediator _mediator
@inject ILogger<TelevisionEpisodeList> _logger
@inject ISnackbar _snackbar
@inject IDialogService _dialog
@inject NavigationManager _navigationManager
@inject ChannelWriter<IBackgroundServiceRequest> _channel
@inject IJSRuntime _jsRuntime
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
<div class="fanart-tint"></div>
@if (!string.IsNullOrWhiteSpace(_season.FanArt))
{
if (_season.FanArt.StartsWith("http://") || _season.FanArt.StartsWith("https://"))
{
<img src="@_season.FanArt" alt="fan art"/>
}
else
{
<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))
{
if (_season.Poster.StartsWith("http://") || _season.Poster.StartsWith("https://"))
{
<img class="mud-elevation-2 mr-6"
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
src="@_season.Poster" alt="season poster"/>
}
else
{
<img class="mud-elevation-2 mr-6"
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
src="@($"artwork/posters/{_season.Poster}")" alt="season 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 id="@($"episode-{episode.EpisodeId}")" style="display: flex; flex-direction: row; scroll-margin-top: 85px">
@if (!string.IsNullOrWhiteSpace(episode.Poster))
{
<MudPaper style="display: flex; flex-direction: column; position: relative">
@if (episode.Poster.StartsWith("http://") || episode.Poster.StartsWith("https://"))
{
<MudCardMedia Image="@episode.Poster" Style="flex-grow: 1; height: 220px; width: 392px;"/>
}
else
{
<MudCardMedia Image="@($"artwork/thumbnails/{episode.Poster}")" Style="flex-grow: 1; height: 220px; width: 392px;"/>
}
@if (episode.State == MediaItemState.FileNotFound)
{
<div style="position: absolute; right: 10px; top: 8px;">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
</div>
}
else if (episode.State == MediaItemState.Unavailable)
{
<div style="position: absolute; right: 10px; top: 8px;">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/>
</div>
}
</MudPaper>
}
<MudCardContent Class="ml-3">
<div style="display: flex; flex-direction: column; height: 100%">
<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="@(_ => AddEpisodeToCollection(episode))">
Add To Collection
</MudButton>
</div>
</div>
</MudCardContent>
</div>
<div class="pl-3 pt-3">
@if (episode.State == MediaItemState.FileNotFound)
{
<div class="mb-3" style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Class="mr-2"/>
<MudText>File Not Found:&nbsp;</MudText>
<MudText>@episode.Path</MudText>
</div>
}
else if (episode.State == MediaItemState.Unavailable)
{
<div class="mb-3" style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Class="mr-2"/>
<MudText>Unavailable:&nbsp;</MudText>
<MudText>@episode.LocalPath</MudText>
</div>
}
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Released: @episode.Aired.ToShortDateString()</MudText>
</div>
@if (episode.Directors.Any())
{
var sorted = episode.Directors.OrderBy(w => w).ToList();
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Directors:&nbsp;</MudText>
<MudLink Href="@(@$"director:""{sorted.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@sorted.Head()</MudLink>
@foreach (string director in sorted.Skip(1))
{
<MudText>,&nbsp;</MudText>
<MudLink Href="@(@$"director:""{director.ToLowerInvariant()}""".GetRelativeSearchQuery())">@director</MudLink>
}
</div>
}
@if (episode.Writers.Any())
{
var sorted = episode.Writers.OrderBy(w => w).ToList();
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Writers:&nbsp;</MudText>
<MudLink Href="@(@$"writer:""{sorted.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@sorted.Head()</MudLink>
@foreach (string writer in sorted.Skip(1))
{
<MudText>,&nbsp;</MudText>
<MudLink Href="@(@$"writer:""{writer.ToLowerInvariant()}""".GetRelativeSearchQuery())">@writer</MudLink>
}
</div>
}
</div>
</MudCard>
}
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public int SeasonId { get; set; }
private TelevisionSeasonViewModel _season;
private int _pageSize => 100;
private readonly int _pageNumber = 1;
private TelevisionEpisodeCardResultsViewModel _data;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
await _navigationManager.NavigateToFragmentAsync(_jsRuntime);
}
catch (Exception)
{
// ignored
}
}
}
protected override Task OnParametersSetAsync() => RefreshData();
private async Task RefreshData()
{
await _mediator.Send(new GetTelevisionSeasonById(SeasonId), _cts.Token)
.IfSomeAsync(vm => _season = vm);
_data = await _mediator.Send(new GetTelevisionEpisodeCards(SeasonId, _pageNumber, _pageSize), _cts.Token);
}
private async Task AddToCollection()
{
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Name}" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await _dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{
await _mediator.Send(new AddSeasonToCollection(collection.Id, SeasonId), _cts.Token);
_navigationManager.NavigateTo($"media/collections/{collection.Id}");
}
}
private async Task AddToSchedule()
{
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Name}" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await _dialog.ShowAsync<AddToScheduleDialog>("Add To Schedule", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
_navigationManager.NavigateTo($"schedules/{schedule.Id}/items");
}
}
private async Task AddEpisodeToCollection(TelevisionEpisodeCardViewModel episode)
{
var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.Title } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await _dialog.ShowAsync<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, _cts.Token);
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));
}
}
}