* rework many list pages * refactor * rework movie details and season list
334 lines
15 KiB
Plaintext
334 lines
15 KiB
Plaintext
@page "/media/movies/{MovieId:int}"
|
|
@using ErsatzTV.Application.MediaCards
|
|
@using ErsatzTV.Application.MediaCollections
|
|
@using ErsatzTV.Application.MediaItems
|
|
@using ErsatzTV.Application.Movies
|
|
@using ErsatzTV.Extensions
|
|
@using LanguageExt.UnsafeValueAccess
|
|
@implements IDisposable
|
|
@inject IMediator Mediator
|
|
@inject IDialogService Dialog
|
|
@inject NavigationManager NavigationManager
|
|
@inject ILogger<Movie> Logger
|
|
@inject ISnackbar Snackbar
|
|
@inject PersistentComponentState ApplicationState
|
|
|
|
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
|
|
<div class="fanart-tint"></div>
|
|
@if (!string.IsNullOrWhiteSpace(_movie?.FanArt))
|
|
{
|
|
if (_movie.FanArt.StartsWith("http://") || _movie.FanArt.StartsWith("https://"))
|
|
{
|
|
<img src="@_movie.FanArt" alt="fan art"/>
|
|
}
|
|
else
|
|
{
|
|
<img src="@($"artwork/fanart/{_movie.FanArt}")" alt="fan art"/>
|
|
}
|
|
}
|
|
</MudContainer>
|
|
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 100px">
|
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Spacing="6">
|
|
@if (!string.IsNullOrWhiteSpace(_movie?.Poster))
|
|
{
|
|
<div style="display: flex; flex-direction: column; max-height: 325px; position: relative">
|
|
<MudImage Src="@GetPosterUrl(_movie.Poster)" Class="rounded-lg" Style="max-height: 325px" ObjectFit="ObjectFit.Contain" />
|
|
@if (_movie.MediaItemState == 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 (_movie.MediaItemState == MediaItemState.Unavailable)
|
|
{
|
|
<div style="position: absolute; right: 10px; top: 8px;">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
<div style="display: flex; flex-direction: column; height: 100%">
|
|
<MudStack Row="false">
|
|
<MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown">
|
|
<MudText Typo="Typo.h4" Class="media-item-title">@_movie?.Title</MudText>
|
|
</MudHidden>
|
|
<MudHidden Invert="true" Breakpoint="Breakpoint.MdAndUp">
|
|
<MudText Typo="Typo.h2" Class="media-item-title">@_movie?.Title</MudText>
|
|
</MudHidden>
|
|
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_movie?.Year</MudText>
|
|
</MudStack>
|
|
@if (!string.IsNullOrWhiteSpace(_movie?.Plot))
|
|
{
|
|
<MudCard Elevation="2" Class="mb-6">
|
|
<MudCardContent Class="mx-3 my-3" Style="height: 100%">
|
|
<MudText Style="flex-grow: 1">@_movie.Plot</MudText>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
}
|
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="mb-6">
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Add"
|
|
OnClick="@AddToCollection">
|
|
Add To Collection
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.PlaylistAdd"
|
|
OnClick="@AddToPlaylist">
|
|
Add To Playlist
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Secondary"
|
|
StartIcon="@Icons.Material.Filled.Info"
|
|
OnClick="@ShowInfo">
|
|
Show Media Info
|
|
</MudButton>
|
|
</MudStack>
|
|
</div>
|
|
</MudStack>
|
|
@if (_movie?.MediaItemState == MediaItemState.FileNotFound)
|
|
{
|
|
<MudCard Class="mb-6">
|
|
<MudCardContent>
|
|
<div 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: </MudText>
|
|
<MudText>@_movie.Path</MudText>
|
|
</div>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
}
|
|
else if (_movie?.MediaItemState == MediaItemState.Unavailable)
|
|
{
|
|
<MudCard Class="mb-6">
|
|
<MudCardContent>
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Class="mr-2"/>
|
|
<MudText>Unavailable: </MudText>
|
|
<MudText>@_movie.LocalPath</MudText>
|
|
</div>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
}
|
|
<MudCard Class="mt-6 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().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedLanguages.Head()</MudLink>
|
|
@foreach (string language in _sortedLanguages.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"language:""{language.ToLowerInvariant()}""".GetRelativeSearchQuery())">@language</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 (_sortedDirectors.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Directors: </MudText>
|
|
<MudLink Href="@(@$"director:""{_sortedDirectors.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedDirectors.Head()</MudLink>
|
|
@foreach (string director in _sortedDirectors.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"director:""{director.ToLowerInvariant()}""".GetRelativeSearchQuery())">@director</MudLink>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_sortedWriters.Any())
|
|
{
|
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
|
<MudText GutterBottom="true">Writers: </MudText>
|
|
<MudLink Href="@(@$"writer:""{_sortedWriters.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedWriters.Head()</MudLink>
|
|
@foreach (string writer in _sortedWriters.Skip(1))
|
|
{
|
|
<MudText>, </MudText>
|
|
<MudLink Href="@(@$"writer:""{writer.ToLowerInvariant()}""".GetRelativeSearchQuery())">@writer</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>
|
|
@if (_movie is not null && _movie.Actors.Any())
|
|
{
|
|
<MudContainer MaxWidth="MaxWidth.Large">
|
|
<MudText Class="mb-4">Actors</MudText>
|
|
<MudStack Row="true" Wrap="Wrap.Wrap">
|
|
@foreach (ActorCardViewModel actor in _movie.Actors)
|
|
{
|
|
<MediaCard Data="@actor"
|
|
Href="@(@$"actor:""{actor.Name.ToLowerInvariant()}""".GetRelativeSearchQuery())"
|
|
ArtworkKind="ArtworkKind.Thumbnail"/>
|
|
}
|
|
</MudStack>
|
|
</MudContainer>
|
|
}
|
|
|
|
@code {
|
|
private readonly CancellationTokenSource _cts = new();
|
|
private PersistingComponentStateSubscription _persistingSubscription;
|
|
|
|
[Parameter]
|
|
public int MovieId { get; set; }
|
|
|
|
private MovieViewModel _movie;
|
|
private List<string> _sortedContentRatings = new();
|
|
private List<string> _sortedLanguages = new();
|
|
private List<string> _sortedDirectors = new();
|
|
private List<string> _sortedWriters = new();
|
|
private List<string> _sortedStudios = new();
|
|
private List<string> _sortedGenres = new();
|
|
private List<string> _sortedTags = new();
|
|
|
|
public void Dispose()
|
|
{
|
|
_persistingSubscription.Dispose();
|
|
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
}
|
|
|
|
protected override Task OnInitializedAsync()
|
|
{
|
|
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
|
|
|
|
return base.OnInitializedAsync();
|
|
}
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
if (!ApplicationState.TryTakeFromJson("_movie", out MovieViewModel restored))
|
|
{
|
|
_movie = await RefreshData();
|
|
}
|
|
else
|
|
{
|
|
_movie = restored;
|
|
}
|
|
|
|
_sortedContentRatings = _movie?.ContentRatings.OrderBy(cr => cr).ToList();
|
|
_sortedLanguages = _movie?.Languages.OrderBy(l => l).ToList();
|
|
_sortedStudios = _movie?.Studios.OrderBy(s => s).ToList();
|
|
_sortedGenres = _movie?.Genres.OrderBy(g => g).ToList();
|
|
_sortedTags = _movie?.Tags.OrderBy(t => t).ToList();
|
|
_sortedDirectors = _movie?.Directors.OrderBy(d => d).ToList();
|
|
_sortedWriters = _movie?.Writers.OrderBy(w => w).ToList();
|
|
}
|
|
|
|
private Task PersistData()
|
|
{
|
|
ApplicationState.PersistAsJson("_movie", _movie);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private async Task<MovieViewModel> RefreshData()
|
|
{
|
|
Option<MovieViewModel> vm = await Mediator.Send(new GetMovieById(MovieId), _cts.Token);
|
|
return vm.IsSome ? vm.ValueUnsafe() : null;
|
|
}
|
|
|
|
private async Task AddToCollection()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "movie" }, { "EntityName", _movie.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 is { Canceled: false, Data: MediaCollectionViewModel collection })
|
|
{
|
|
await Mediator.Send(new AddMovieToCollection(collection.Id, MovieId), _cts.Token);
|
|
NavigationManager.NavigateTo($"media/collections/{collection.Id}");
|
|
}
|
|
}
|
|
|
|
private async Task AddToPlaylist()
|
|
{
|
|
var parameters = new DialogParameters { { "EntityType", "movie" }, { "EntityName", _movie.Title } };
|
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<AddToPlaylistDialog>("Add To Playlist", parameters, options);
|
|
DialogResult result = await dialog.Result;
|
|
if (result is { Canceled: false, Data: PlaylistViewModel playlist })
|
|
{
|
|
await Mediator.Send(new AddMovieToPlaylist(playlist.Id, MovieId), _cts.Token);
|
|
NavigationManager.NavigateTo($"media/playlists/{playlist.Id}");
|
|
}
|
|
}
|
|
|
|
private async Task ShowInfo()
|
|
{
|
|
Either<BaseError, MediaItemInfo> maybeInfo = await Mediator.Send(new GetMediaItemInfo(MovieId));
|
|
|
|
foreach (BaseError error in maybeInfo.LeftToSeq())
|
|
{
|
|
Snackbar.Add("Unexpected error loading media info");
|
|
Logger.LogError("Unexpected error loading media info: {Error}", error.Value);
|
|
}
|
|
|
|
foreach (MediaItemInfo info in maybeInfo.RightToSeq())
|
|
{
|
|
var parameters = new DialogParameters { { "MediaItemInfo", info } };
|
|
var options = new DialogOptions { CloseButton = true, CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
|
|
IDialogReference dialog = await Dialog.ShowAsync<MediaItemInfoDialog>(_movie.Title, parameters, options);
|
|
DialogResult _ = await dialog.Result;
|
|
}
|
|
}
|
|
|
|
private static string GetPosterUrl(string poster)
|
|
{
|
|
return poster.StartsWith("http://") || poster.StartsWith("https://") ? poster : $"artwork/posters/{poster}";
|
|
}
|
|
|
|
} |