* add chapter statistics and new filler options * refactor playout builder * more refactor prep for filler * rewrite schedulers * refactor collectionkey * add tail filler kind * migrate tail filler to filler preset * optionally show filler * fix playout detail row count * remove duration tail filler options * implement tail and fallback in flood scheduler * implement tail and fallback in one scheduler * implement tail and fallback in multiple scheduler * implement looping fallback filler * more duration tests * start to add post-roll filler to flood * rework playoutitem filler tagging * rework scheduler logging * calculate whether configured filler will fit * implement pre-roll and post-roll duration and count filler * improve duration filler calculation * add minutes to search index * update channel guide to work with new filler * add mid-roll filler * don't clone enumerators for filler calculations * support pre-roll and post-roll pad filler * implement mid-roll pad filler * allow clearing filler selections in schedule editor * fix tests * filler config validation * use consistent time zone for tests
257 lines
12 KiB
Plaintext
257 lines
12 KiB
Plaintext
@page "/media/tv/shows/{ShowId:int}"
|
|
@using ErsatzTV.Application.Television
|
|
@using ErsatzTV.Application.Television.Queries
|
|
@using System.Globalization
|
|
@using ErsatzTV.Application.MediaCards
|
|
@using ErsatzTV.Application.MediaCards.Queries
|
|
@using ErsatzTV.Application.MediaCollections
|
|
@using ErsatzTV.Application.MediaCollections.Commands
|
|
@using ErsatzTV.Application.ProgramSchedules
|
|
@using ErsatzTV.Application.ProgramSchedules.Commands
|
|
@using ErsatzTV.Extensions
|
|
@using Unit = LanguageExt.Unit
|
|
@inject IMediator _mediator
|
|
@inject ILogger<TelevisionSeasonList> _logger
|
|
@inject ISnackbar _snackbar
|
|
@inject IDialogService _dialog
|
|
@inject NavigationManager _navigationManager
|
|
@inject ChannelWriter<IBackgroundServiceRequest> _channel
|
|
|
|
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
|
<div class="fanart-tint"></div>
|
|
@if (!string.IsNullOrWhiteSpace(_show.FanArt))
|
|
{
|
|
if (_show.FanArt.StartsWith("http://") || _show.FanArt.StartsWith("https://"))
|
|
{
|
|
<img src="@_show.FanArt" alt="fan art"/>
|
|
}
|
|
else
|
|
{
|
|
<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;" class="mb-6">
|
|
@if (!string.IsNullOrWhiteSpace(_show.Poster))
|
|
{
|
|
if (_show.Poster.StartsWith("http://") || _show.Poster.StartsWith("https://"))
|
|
{
|
|
<img class="mud-elevation-2 mr-6"
|
|
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
|
src="@_show.Poster" alt="movie poster"/>
|
|
}
|
|
else
|
|
{
|
|
<img class="mud-elevation-2 mr-6"
|
|
style="border-radius: 4px; flex-shrink: 0; max-height: 440px;"
|
|
src="@($"/artwork/posters/{_show.Poster}")" alt="movie poster"/>
|
|
}
|
|
}
|
|
<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>
|
|
<MudCard Class="mb-6">
|
|
<MudCardContent>
|
|
@if (_sortedContentRatings.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Content Ratings: </MudText>
|
|
<MudLink Href="@(@$"content_rating:""{_sortedContentRatings.Head()}""".GetRelativeSearchQuery())">@_sortedContentRatings.Head()</MudLink>
|
|
@foreach (string contentRating in _sortedContentRatings.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"content_rating:""{contentRating}""".GetRelativeSearchQuery())">@contentRating</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedLanguages.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Languages: </MudText>
|
|
<MudLink Href="@(@$"language:""{_sortedLanguages.Head().EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedLanguages.Head().EnglishName</MudLink>
|
|
@foreach (CultureInfo language in _sortedLanguages.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"language:""{language.EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@language.EnglishName</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedStudios.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Studios: </MudText>
|
|
<MudLink Href="@(@$"studio:""{_sortedStudios.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedStudios.Head()</MudLink>
|
|
@foreach (string studio in _sortedStudios.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"studio:""{studio.ToLowerInvariant()}""".GetRelativeSearchQuery())">@studio</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedGenres.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Genres: </MudText>
|
|
<MudLink Href="@(@$"genre:""{_sortedGenres.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedGenres.Head()</MudLink>
|
|
@foreach (string genre in _sortedGenres.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"genre:""{genre.ToLowerInvariant()}""".GetRelativeSearchQuery())">@genre</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedTags.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Tags: </MudText>
|
|
<MudLink Href="@(@$"tag:""{_sortedTags.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedTags.Head()</MudLink>
|
|
@foreach (string tag in _sortedTags.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"tag:""{tag.ToLowerInvariant()}""".GetRelativeSearchQuery())">@tag</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large">
|
|
<MudText Class="mb-4">Seasons</MudText>
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid">
|
|
@foreach (TelevisionSeasonCardViewModel card in _data.Cards)
|
|
{
|
|
<MediaCard Data="@card" Placeholder="@card.Placeholder"
|
|
Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")"
|
|
AddToCollectionClicked="@AddSeasonToCollection"/>
|
|
}
|
|
</MudContainer>
|
|
@if (_show.Actors.Any())
|
|
{
|
|
<MudContainer MaxWidth="MaxWidth.Large">
|
|
<MudText Class="mb-4">Actors</MudText>
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid">
|
|
@foreach (ActorCardViewModel actor in _show.Actors)
|
|
{
|
|
<MediaCard Data="@actor"
|
|
Link="@(@$"actor:""{actor.Name.ToLowerInvariant()}""".GetRelativeSearchQuery())"
|
|
ArtworkKind="ArtworkKind.Thumbnail"/>
|
|
}
|
|
</MudContainer>
|
|
}
|
|
|
|
@code {
|
|
|
|
[Parameter]
|
|
public int ShowId { get; set; }
|
|
|
|
private TelevisionShowViewModel _show;
|
|
private List<string> _sortedContentRatings = new();
|
|
private List<CultureInfo> _sortedLanguages = new();
|
|
private List<string> _sortedStudios = new();
|
|
private List<string> _sortedGenres = new();
|
|
private List<string> _sortedTags = new();
|
|
|
|
private int _pageSize => 100;
|
|
private readonly int _pageNumber = 1;
|
|
|
|
private TelevisionSeasonCardResultsViewModel _data;
|
|
|
|
protected override Task OnParametersSetAsync() => RefreshData();
|
|
|
|
private async Task RefreshData()
|
|
{
|
|
await _mediator.Send(new GetTelevisionShowById(ShowId))
|
|
.IfSomeAsync(vm =>
|
|
{
|
|
_show = vm;
|
|
_sortedContentRatings = _show.ContentRatings.OrderBy(cr => cr).ToList();
|
|
_sortedLanguages = _show.Languages.OrderBy(ci => ci.EnglishName).ToList();
|
|
_sortedStudios = _show.Studios.OrderBy(s => s).ToList();
|
|
_sortedGenres = _show.Genres.OrderBy(g => g).ToList();
|
|
_sortedTags = _show.Tags.OrderBy(t => t).ToList();
|
|
});
|
|
|
|
_data = await _mediator.Send(new GetTelevisionSeasonCards(ShowId, _pageNumber, _pageSize));
|
|
}
|
|
|
|
private async Task AddToCollection()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.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 AddShowToCollection(collection.Id, ShowId));
|
|
_navigationManager.NavigateTo($"/media/collections/{collection.Id}");
|
|
}
|
|
}
|
|
|
|
private async Task AddToSchedule()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.Title } };
|
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
|
|
|
IDialogReference dialog = _dialog.Show<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.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null));
|
|
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
|
|
}
|
|
}
|
|
|
|
private async Task AddSeasonToCollection(MediaCardViewModel card)
|
|
{
|
|
if (card is TelevisionSeasonCardViewModel season)
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", season.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 AddSeasonToCollection(collection.Id, season.TelevisionSeasonId);
|
|
Either<BaseError, Unit> addResult = await _mediator.Send(request);
|
|
addResult.Match(
|
|
Left: error =>
|
|
{
|
|
_snackbar.Add($"Unexpected error adding season to collection: {error.Value}");
|
|
_logger.LogError("Unexpected error adding season to collection: {Error}", error.Value);
|
|
},
|
|
Right: _ => _snackbar.Add($"Added {season.Title} to collection {collection.Name}", Severity.Success));
|
|
}
|
|
}
|
|
}
|
|
|
|
} |