Browse Source

lots of mobile updates including detail pages (#2135)

* update artist page layout

* update season page layout

* rework collection view

* cleanup

* update collection editor
pull/2136/head
Jason Dove 1 month ago committed by GitHub
parent
commit
1a09bb26d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      ErsatzTV.Application/MediaCards/Mapper.cs
  2. 2
      ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs
  3. 206
      ErsatzTV/Pages/Artist.razor
  4. 48
      ErsatzTV/Pages/CollectionEditor.razor
  5. 564
      ErsatzTV/Pages/CollectionItems.razor
  6. 268
      ErsatzTV/Pages/Collections.razor
  7. 2
      ErsatzTV/Pages/Movie.razor
  8. 61
      ErsatzTV/Pages/Search.razor
  9. 275
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  10. 2
      ErsatzTV/Pages/TelevisionSeasonList.razor
  11. 2
      ErsatzTV/Pages/TelevisionSeasonSearchResults.razor
  12. 2
      ErsatzTV/Pages/TraktLists.razor
  13. 3
      ErsatzTV/Shared/MediaCard.razor
  14. 9
      ErsatzTV/Validators/CollectionEditViewModelValidator.cs
  15. 4
      ErsatzTV/wwwroot/css/site.css

2
ErsatzTV.Application/MediaCards/Mapper.cs

@ -187,7 +187,7 @@ internal static class Mapper
e.EpisodeMetadata.Head(), e.EpisodeMetadata.Head(),
maybeJellyfin, maybeJellyfin,
maybeEmby, maybeEmby,
false, true,
string.Empty)) string.Empty))
.ToList(), .ToList(),
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(), collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),

2
ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs

@ -79,9 +79,11 @@ public class GetCollectionCardsHandler :
.ThenInclude(i => (i as Episode).Season) .ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.Show) .ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata) .ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).Season) .ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.SeasonMetadata) .ThenInclude(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).MediaVersions) .ThenInclude(i => (i as Episode).MediaVersions)
.ThenInclude(mv => mv.MediaFiles) .ThenInclude(mv => mv.MediaFiles)

206
ErsatzTV/Pages/Artist.razor

@ -19,17 +19,22 @@
<img src="@($"artwork/fanart/{_artist.FanArt}")" alt="fan art"/> <img src="@($"artwork/fanart/{_artist.FanArt}")" alt="fan art"/>
} }
</MudContainer> </MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px"> <MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 100px">
<div style="display: flex; flex-direction: row;" class="mb-6"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Spacing="6">
@if (!string.IsNullOrWhiteSpace(_artist.Thumbnail)) @if (!string.IsNullOrWhiteSpace(_artist.Thumbnail))
{ {
<img class="mud-elevation-2 mr-6" <MudImage Elevation="2" Src="@($"artwork/thumbnails/{_artist.Thumbnail}")" Class="rounded-lg" Style="height: 220px; width: 220px; margin-left: auto; margin-right: auto;" />
style="border-radius: 4px; flex-shrink: 0; height: 220px; width: 220px"
src="@($"artwork/thumbnails/{_artist.Thumbnail}")" alt="artist thumbnail"/>
} }
<div style="display: flex; flex-direction: column; height: 100%"> <div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h2" Class="media-item-title">@_artist.Name</MudText> <MudStack Row="false">
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_artist.Disambiguation</MudText> <MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown">
<MudText Typo="Typo.h4" Class="media-item-title">@_artist?.Name</MudText>
</MudHidden>
<MudHidden Invert="true" Breakpoint="Breakpoint.MdAndUp">
<MudText Typo="Typo.h2" Class="media-item-title">@_artist?.Name</MudText>
</MudHidden>
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_artist.Disambiguation</MudText>
</MudStack>
@if (!string.IsNullOrWhiteSpace(_artist.Biography)) @if (!string.IsNullOrWhiteSpace(_artist.Biography))
{ {
<MudCard Elevation="2" Class="mb-6"> <MudCard Elevation="2" Class="mb-6">
@ -47,23 +52,22 @@
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
} }
<div> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="mb-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="@AddToCollection">
Add To Collection Add To Collection
</MudButton> </MudButton>
<MudButton Class="ml-3" <MudButton Variant="Variant.Filled"
Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Schedule" StartIcon="@Icons.Material.Filled.Schedule"
OnClick="@AddToSchedule"> OnClick="@AddToSchedule">
Add To Schedule Add To Schedule
</MudButton> </MudButton>
</div> </MudStack>
</div> </div>
</div> </MudStack>
<MudCard Class="mb-6"> <MudCard Class="mb-6">
<MudCardContent> <MudCardContent>
@if (_sortedLanguages.Any()) @if (_sortedLanguages.Any())
@ -118,86 +122,110 @@
</MudCard> </MudCard>
</MudContainer> </MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8"> <MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
@foreach (MusicVideoCardViewModel musicVideo in _musicVideos.Cards) <MudStack Row="false" Spacing="6">
{ @foreach (MusicVideoCardViewModel musicVideo in _musicVideos.Cards)
<MudCard Class="mb-6" Style="display: flex; flex-direction: column"> {
<div id="@($"music-video-{musicVideo.MusicVideoId}")" style="display: flex; flex-direction: row; scroll-margin-top: 85px"> <MudCard>
@if (!string.IsNullOrWhiteSpace(musicVideo.Poster)) <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" id="@($"music-video-{musicVideo.MusicVideoId}")">
{ @if (!string.IsNullOrWhiteSpace(musicVideo.Poster))
<MudPaper style="display: flex; flex-direction: column; position: relative"> {
<MudCardMedia Image="@($"artwork/thumbnails/{musicVideo.Poster}")" Style="flex-grow: 1; height: 220px; width: 293px;"/> <div style="display: flex; flex-direction: column; position: relative">
@if (musicVideo.State == MediaItemState.FileNotFound) <MudImage Src="@($"artwork/thumbnails/{musicVideo.Poster}")" Style="height: 220px; max-width: 265px; margin-left: auto; margin-right: auto;"/>
{ @if (musicVideo.State == MediaItemState.FileNotFound)
<div style="position: absolute; right: 10px; top: 8px;"> {
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/> <div style="position: absolute; right: 10px; top: 8px;">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
</div>
}
else if (musicVideo.State == MediaItemState.Unavailable)
{
<div style="position: absolute; right: 10px; top: 8px;">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/>
</div>
}
</div>
}
else
{
<MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown">
@if (musicVideo.State is not MediaItemState.Normal and not MediaItemState.RemoteOnly)
{
<div style="display: flex; position: relative; height: 220px; width: 265px; ">
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.False"/>
@if (musicVideo.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 (musicVideo.State == MediaItemState.Unavailable)
{
<div style="position: absolute; right: 10px; top: 8px;">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/>
</div>
}
</div>
}
</MudHidden>
<MudHidden Invert="true" Breakpoint="Breakpoint.MdAndUp">
<div style="display: flex; position: relative; height: 220px; width: 265px; ">
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.False" />
@if (musicVideo.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 (musicVideo.State == 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>
} </MudHidden>
else if (musicVideo.State == MediaItemState.Unavailable) }
{ <MudCardContent>
<div style="position: absolute; right: 10px; top: 8px;"> <div style="display: flex; flex-direction: column; height: 100%">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/> <MudText Typo="Typo.h4">@musicVideo.Title</MudText>
@if (!string.IsNullOrWhiteSpace(musicVideo.Album))
{
<div style="display: flex; flex-direction: row">
<MudText GutterBottom="true">Album:&nbsp;</MudText>
<MudLink Href="@(@$"album:""{musicVideo.Album}""".GetRelativeSearchQuery())">@musicVideo.Album</MudLink>
</div>
}
<MudText Style="flex-grow: 1">@musicVideo.Plot</MudText>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(_ => AddMusicVideoToCollection(musicVideo))">
Add To Collection
</MudButton>
</div> </div>
} </div>
</MudPaper> </MudCardContent>
} </MudStack>
else @if (musicVideo.State == MediaItemState.FileNotFound)
{ {
<div style="display: flex; height: 220px; position: relative; width: 293px;"> <div class="ml-3 mt-3 mb-3" style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.False"/> <MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Class="mr-2"/>
@if (musicVideo.State == MediaItemState.FileNotFound) <MudText>File Not Found:&nbsp;</MudText>
{ <MudText>@musicVideo.Path</MudText>
<div style="position: absolute; right: 10px; top: 8px;">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
</div>
}
else if (musicVideo.State == 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>
} }
<MudCardContent Class="ml-3"> else if (musicVideo.State == MediaItemState.Unavailable)
<div style="display: flex; flex-direction: column; height: 100%"> {
<MudText Typo="Typo.h4">@musicVideo.Title</MudText> <div class="ml-3 mt-3 mb-3" style="display: flex; flex-direction: row; flex-wrap: wrap">
@if (!string.IsNullOrWhiteSpace(musicVideo.Album)) <MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Class="mr-2"/>
{ <MudText>Unavailable:&nbsp;</MudText>
<div style="display: flex; flex-direction: row"> <MudText>@musicVideo.LocalPath</MudText>
<MudText GutterBottom="true">Album:&nbsp;</MudText>
<MudLink Href="@(@$"album:""{musicVideo.Album}""".GetRelativeSearchQuery())">@musicVideo.Album</MudLink>
</div>
}
<MudText Style="flex-grow: 1">@musicVideo.Plot</MudText>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(_ => AddMusicVideoToCollection(musicVideo))">
Add To Collection
</MudButton>
</div>
</div> </div>
</MudCardContent> }
</div> </MudCard>
@if (musicVideo.State == MediaItemState.FileNotFound) }
{ </MudStack>
<div class="ml-3 mt-3 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>@musicVideo.Path</MudText>
</div>
}
else if (musicVideo.State == MediaItemState.Unavailable)
{
<div class="ml-3 mt-3 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>@musicVideo.LocalPath</MudText>
</div>
}
</MudCard>
}
</MudContainer> </MudContainer>
@code { @code {
@ -242,7 +270,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection) if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{ {
await Mediator.Send(new AddArtistToCollection(collection.Id, ArtistId), _cts.Token); await Mediator.Send(new AddArtistToCollection(collection.Id, ArtistId), _cts.Token);
NavigationManager.NavigateTo($"media/collections/{collection.Id}"); NavigationManager.NavigateTo($"media/collections/{collection.Id}");
@ -256,7 +284,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToScheduleDialog>("Add To Schedule", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToScheduleDialog>("Add To Schedule", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule) if (result is { Canceled: false, Data: ProgramScheduleViewModel schedule })
{ {
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items"); NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
@ -270,7 +298,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection) if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{ {
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId); var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token); Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token);

48
ErsatzTV/Pages/CollectionEditor.razor

@ -7,25 +7,23 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudForm @ref="_form" @bind-IsValid="@_success" Style="max-height: 100%">
<div style="max-width: 400px;"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Collection" : "Add Collection")</MudText> <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="HandleSubmitAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Collection" : "Add Collection")</MudButton>
</MudPaper>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<FluentValidationValidator/> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCard> <MudText Typo="Typo.h5" Class="mb-2">Collection</MudText>
<MudCardContent> <MudDivider Class="mb-6"/>
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
</MudCardContent> <div class="d-flex">
<MudCardActions> <MudText>Name</MudText>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto"> </div>
@(IsEdit ? "Save Changes" : "Add Collection") <MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Required="true" RequiredError="Schedule name is required!"/>
</MudButton> </MudStack>
</MudCardActions> </MudContainer>
</MudCard>
</EditForm>
</div> </div>
</MudContainer> </MudForm>
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
@ -34,8 +32,8 @@
public int Id { get; set; } public int Id { get; set; }
private readonly CollectionEditViewModel _model = new(); private readonly CollectionEditViewModel _model = new();
private EditContext _editContext; private MudForm _form;
private ValidationMessageStore _messageStore; private bool _success;
public void Dispose() public void Dispose()
{ {
@ -60,18 +58,12 @@
} }
} }
protected override void OnInitialized()
{
_editContext = new EditContext(_model);
_messageStore = new ValidationMessageStore(_editContext);
}
private bool IsEdit => Id != 0; private bool IsEdit => Id != 0;
private async Task HandleSubmitAsync() private async Task HandleSubmitAsync()
{ {
_messageStore.Clear(); await _form.Validate();
if (_editContext.Validate()) if (_success)
{ {
Seq<BaseError> errorMessage = IsEdit ? (await Mediator.Send(new UpdateCollection(Id, _model.Name), _cts.Token)).LeftToSeq() : (await Mediator.Send(new CreateCollection(_model.Name), _cts.Token)).LeftToSeq(); Seq<BaseError> errorMessage = IsEdit ? (await Mediator.Send(new UpdateCollection(Id, _model.Name), _cts.Token)).LeftToSeq() : (await Mediator.Send(new CreateCollection(_model.Name), _cts.Token)).LeftToSeq();

564
ErsatzTV/Pages/CollectionItems.razor

@ -5,313 +5,333 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IJSRuntime JsRuntime @inject IJSRuntime JsRuntime
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;"> <MudForm Style="max-height: 100%">
<div style="align-items: center; display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%;" class="ml-6 mr-6"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100;">
@if (IsSelectMode()) <div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%; align-items: center" class="ml-6 mr-6">
{ @if (IsSelectMode())
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText> {
<div style="margin-left: auto"> <div class="flex-grow-1">
<MudButton Variant="Variant.Filled" <MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
Color="Color.Error" </div>
StartIcon="@Icons.Material.Filled.Remove" <div style="margin-left: auto" class="d-none d-md-flex">
OnClick="@(_ => RemoveSelectionFromCollection(Id))"> <MudButton Variant="Variant.Filled"
Remove From Collection Color="Color.Error"
</MudButton> StartIcon="@Icons.Material.Filled.Remove"
<MudButton Class="ml-3" OnClick="@(_ => RemoveSelectionFromCollection(Id))">
Variant="Variant.Filled" Remove From Collection
Color="Color.Secondary" </MudButton>
StartIcon="@Icons.Material.Filled.Check" <MudButton Class="ml-3"
OnClick="@(_ => ClearSelection())"> Variant="Variant.Filled"
Clear Selection Color="Color.Secondary"
</MudButton> StartIcon="@Icons.Material.Filled.Check"
</div> OnClick="@(_ => ClearSelection())">
} Clear Selection
else </MudButton>
{ </div>
<div style="align-items: center; display: flex; flex-direction: row;"> <div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none">
<MudText Typo="Typo.h4">@_data?.Name</MudText> <div class="flex-grow-1"></div>
<MudIconButton Icon="@Icons.Material.Filled.Edit" <MudMenu Icon="@Icons.Material.Filled.MoreVert">
Href="@($"media/collections/{Id}/edit")"/> <MudMenuItem Icon="@Icons.Material.Filled.Remove" Label="Remove From Collection" OnClick="@(_ => RemoveSelectionFromCollection(Id))"/>
</div> <MudMenuItem Icon="@Icons.Material.Filled.Check" Label="Clear Selection" OnClick="ClearSelection"/>
</MudMenu>
</div>
}
else
{
<div style="align-items: center; display: flex; width: 100%">
<MudText>@_data?.Name</MudText>
<div class="d-none d-md-flex" style="align-items: center">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Href="@($"media/collections/{Id}/edit")" />
@if (_data?.MovieCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#movies")">@_data.MovieCards.Count Movies</MudLink>
}
@if (_data?.ShowCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#shows")">@_data.ShowCards.Count Shows</MudLink>
}
@if (_data?.SeasonCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#seasons")">@_data.SeasonCards.Count Seasons</MudLink>
}
@if (_data?.EpisodeCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")">@_data.EpisodeCards.Count Episodes</MudLink>
}
@if (_data?.ArtistCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#artists")">@_data.ArtistCards.Count Artists</MudLink>
}
@if (_data?.MusicVideoCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#music_videos")">@_data.MusicVideoCards.Count Music Videos</MudLink>
}
@if (_data?.OtherVideoCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#other_videos")">@_data.OtherVideoCards.Count Other Videos</MudLink>
}
@if (_data?.SongCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#songs")">@_data.SongCards.Count Songs</MudLink>
}
@if (_data?.ImageCards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")">@_data.ImageCards.Count Images</MudLink>
}
</div>
@if (SupportsCustomOrdering())
{
<div class="d-none d-md-flex" style="margin-left: auto">
<MudSwitch T="bool"
Value="@(_data?.UseCustomPlaybackOrder == true)"
Color="Color.Primary"
ValueChanged="@OnUseCustomOrderChanged"
Label="Use Custom Playback Order"/>
</div>
}
<div class="d-flex d-sm-none" style="margin-left: auto">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Href="@($"media/collections/{Id}/edit")" />
</div>
</div>
}
</div>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
@if (_data?.MovieCards.Count > 0) @if (_data?.MovieCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#movies")">@_data.MovieCards.Count Movies</MudLink> <MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
Movies
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" UserAttributes="@(new Dictionary<string, object> { { "id", "sortable-collection" } })">
@foreach (MovieCardViewModel card in OrderMovies(_data.MovieCards))
{
<MediaCard Data="@card"
Href="@($"media/movies/{card.MovieId}")"
DeleteClicked="@RemoveMovieFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
@if (_data?.ShowCards.Count > 0) @if (_data?.ShowCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#shows")">@_data.ShowCards.Count Shows</MudLink> <MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Shows
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/shows/{card.TelevisionShowId}")"
DeleteClicked="@RemoveShowFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
@if (_data?.SeasonCards.Count > 0) @if (_data?.SeasonCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#seasons")">@_data.SeasonCards.Count Seasons</MudLink> <MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
Seasons
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/seasons/{card.TelevisionSeasonId}")"
Title="@card.ShowTitle"
Subtitle="@card.Title"
DeleteClicked="@RemoveSeasonFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
@if (_data?.EpisodeCards.Count > 0) @if (_data?.EpisodeCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")">@_data.EpisodeCards.Count Episodes</MudLink> <MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Episodes
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired))
{
<MediaCard Data="@card"
Href="@($"media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
Subtitle="@($"{card.ShowTitle} - S{card.Season} E{card.Episode}")"
DeleteClicked="@(_ => RemoveEpisodeFromCollection(card))"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
@if (_data?.ArtistCards.Count > 0) @if (_data?.ArtistCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#artists")">@_data.ArtistCards.Count Artists</MudLink> <MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Artists
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (ArtistCardViewModel card in _data.ArtistCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/music/artists/{card.ArtistId}")"
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveArtistFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
@if (_data?.MusicVideoCards.Count > 0) @if (_data?.MusicVideoCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#music_videos")">@_data.MusicVideoCards.Count Music Videos</MudLink> <MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Music Videos
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (MusicVideoCardViewModel card in _data.MusicVideoCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveMusicVideoFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
@if (_data?.OtherVideoCards.Count > 0) @if (_data?.OtherVideoCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#other_videos")">@_data.OtherVideoCards.Count Other Videos</MudLink> <MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Other Videos
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (OtherVideoCardViewModel card in _data.OtherVideoCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveOtherVideoFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
@if (_data?.SongCards.Count > 0) @if (_data?.SongCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#songs")">@_data.SongCards.Count Songs</MudLink> <MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
Songs
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (SongCardViewModel card in _data.SongCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveSongFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
@if (_data?.ImageCards.Count > 0) @if (_data?.ImageCards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")">@_data.ImageCards.Count Images</MudLink> <MudText GutterBottom="true"
} Typo="Typo.h4"
Style="scroll-margin-top: 160px"
@if (SupportsCustomOrdering()) UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
{ Images
<div style="margin-left: auto"> </MudText>
<MudSwitch T="bool" <MudDivider Class="mb-6"/>
Value="@(_data?.UseCustomPlaybackOrder == true)"
Color="Color.Primary" <MudStack Row="true" Wrap="Wrap.Wrap">
ValueChanged="@OnUseCustomOrderChanged" @foreach (ImageCardViewModel card in _data.ImageCards.OrderBy(e => e.SortTitle))
Label="Use Custom Playback Order"/> {
</div> <MediaCard Data="@card"
} Href=""
} ArtworkKind="ArtworkKind.Thumbnail"
</div> DeleteClicked="@RemoveImageFromCollection"
</MudPaper> SelectColor="@Color.Error"
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="margin-top: 64px"> SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
@if (_data?.MovieCards.Count > 0) IsSelectMode="@IsSelectMode()"/>
{ }
<MudText GutterBottom="true" </MudStack>
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
Movies
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid" UserAttributes="@(new Dictionary<string, object> { { "id", "sortable-collection" } })">
@foreach (MovieCardViewModel card in OrderMovies(_data.MovieCards))
{
<MediaCard Data="@card"
Href="@($"media/movies/{card.MovieId}")"
DeleteClicked="@RemoveMovieFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_data?.ShowCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Television Shows
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/shows/{card.TelevisionShowId}")"
DeleteClicked="@RemoveShowFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_data?.SeasonCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
Television Seasons
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/seasons/{card.TelevisionSeasonId}")"
Title="@card.ShowTitle"
Subtitle="@card.Title"
DeleteClicked="@RemoveSeasonFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_data?.EpisodeCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Television Episodes
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired))
{
<MediaCard Data="@card"
Href="@($"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(card))"
ArtworkKind="@ArtworkKind.Thumbnail"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_data?.ArtistCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Artists
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (ArtistCardViewModel card in _data.ArtistCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/music/artists/{card.ArtistId}")"
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveArtistFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_data?.MusicVideoCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Music Videos
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MusicVideoCardViewModel card in _data.MusicVideoCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveMusicVideoFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_data?.OtherVideoCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Other Videos
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (OtherVideoCardViewModel card in _data.OtherVideoCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveOtherVideoFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_data?.SongCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
Songs
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (SongCardViewModel card in _data.SongCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveSongFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
} }
</MudContainer> </MudContainer>
} </div>
</MudForm>
@if (_data?.ImageCards.Count > 0)
{
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Images
</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (ImageCardViewModel card in _data.ImageCards.OrderBy(e => e.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@RemoveImageFromCollection"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
</MudContainer>
@code { @code {
@ -512,7 +532,7 @@
IDialogReference dialog = await Dialog.ShowAsync<RemoveFromCollectionDialog>("Remove From Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<RemoveFromCollectionDialog>("Remove From Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled) if (result is { Canceled: false })
{ {
await Mediator.Send(request, CancellationToken); await Mediator.Send(request, CancellationToken);
await RefreshData(); await RefreshData();

268
ErsatzTV/Pages/Collections.razor

@ -6,130 +6,146 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudForm Style="max-height: 100%">
<div> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="media/collections/add"> <div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
Add Collection <div style="margin-right: auto" class="d-none d-md-flex">
</MudButton> <MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Href="media/collections/add">
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" Href="media/multi-collections/add"> Add Collection
Add Multi Collection </MudButton>
</MudButton> <MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" Href="media/multi-collections/add">
Add Multi Collection
</MudButton>
</div>
<div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none">
<div class="flex-grow-1"></div>
<MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Collection" Href="media/collections/add"/>
<MudMenuItem Icon="@Icons.Material.Filled.PlaylistAdd" Label="Add Multi Collection" Href="media/multi-collections/add"/>
</MudMenu>
</div>
</div>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Collections</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
@bind-RowsPerPage="@_collectionsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<MediaCollectionViewModel>>>(ServerReloadCollections))"
Dense="true"
@ref="_collectionsTable">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col style="width: 120px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Collection">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"media/collections/{context.Id}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Collection">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteMediaCollection(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Multi Collections</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
@bind-RowsPerPage="@_multiCollectionsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<MultiCollectionViewModel>>>(ServerReloadMultiCollections))"
Dense="true"
@ref="_multiCollectionsTable">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col style="width: 120px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Collection">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"media/multi-collections/{context.Id}/edit")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Collection">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteMultiCollection(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Smart Collections</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
@bind-RowsPerPage="@_smartCollectionsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<SmartCollectionViewModel>>>(ServerReloadSmartCollections))"
Dense="true"
@ref="_smartCollectionsTable">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col style="width: 120px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Collection">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@context.Query.GetRelativeSearchQuery()">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Collection">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteSmartCollection(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<div class="mt-6"></div>
</MudContainer>
</div> </div>
<MudTable Class="mt-4" </MudForm>
Hover="true"
@bind-RowsPerPage="@_collectionsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<MediaCollectionViewModel>>>(ServerReloadCollections))"
Dense="true"
@ref="_collectionsTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Collections</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col style="width: 120px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Collection">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"media/collections/{context.Id}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Collection">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteMediaCollection(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudTable Class="mt-4"
Hover="true"
@bind-RowsPerPage="@_multiCollectionsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<MultiCollectionViewModel>>>(ServerReloadMultiCollections))"
Dense="true"
@ref="_multiCollectionsTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Multi Collections</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col style="width: 120px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Collection">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"media/multi-collections/{context.Id}/edit")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Collection">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteMultiCollection(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudTable Class="mt-4"
Hover="true"
@bind-RowsPerPage="@_smartCollectionsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<SmartCollectionViewModel>>>(ServerReloadSmartCollections))"
Dense="true"
@ref="_smartCollectionsTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Smart Collections</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col style="width: 120px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Collection">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@context.Query.GetRelativeSearchQuery()">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Collection">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteSmartCollection(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
@ -167,7 +183,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled) if (result is { Canceled: false })
{ {
await Mediator.Send(new DeleteCollection(collection.Id), _cts.Token); await Mediator.Send(new DeleteCollection(collection.Id), _cts.Token);
if (_collectionsTable != null) if (_collectionsTable != null)
@ -184,7 +200,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Multi Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Multi Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled) if (result is { Canceled: false })
{ {
await Mediator.Send(new DeleteMultiCollection(collection.Id), _cts.Token); await Mediator.Send(new DeleteMultiCollection(collection.Id), _cts.Token);
if (_multiCollectionsTable != null) if (_multiCollectionsTable != null)
@ -201,7 +217,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Smart Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Smart Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled) if (result is { Canceled: false })
{ {
await Mediator.Send(new DeleteSmartCollection(collection.Id), _cts.Token); await Mediator.Send(new DeleteSmartCollection(collection.Id), _cts.Token);
if (_smartCollectionsTable != null) if (_smartCollectionsTable != null)

2
ErsatzTV/Pages/Movie.razor

@ -32,7 +32,7 @@
@if (!string.IsNullOrWhiteSpace(_movie?.Poster)) @if (!string.IsNullOrWhiteSpace(_movie?.Poster))
{ {
<div style="display: flex; flex-direction: column; max-height: 325px; position: relative"> <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" /> <MudImage Src="@GetPosterUrl(_movie.Poster)" Elevation="2" Class="rounded-lg" Style="max-height: 325px; margin-left: auto; margin-right: auto" />
@if (_movie.MediaItemState == MediaItemState.FileNotFound) @if (_movie.MediaItemState == MediaItemState.FileNotFound)
{ {
<div style="position: absolute; right: 10px; top: 8px;"> <div style="position: absolute; right: 10px; top: 8px;">

61
ErsatzTV/Pages/Search.razor

@ -10,10 +10,10 @@
<MudForm Style="max-height: 100%"> <MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100;"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100;">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6"> <div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%; align-items: center" class="ml-6 mr-6">
@if (IsSelectMode()) @if (IsSelectMode())
{ {
<div style="align-items: center; display: flex; width: 100%;"> <div class="flex-grow-1">
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText> <MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
</div> </div>
<div style="margin-left: auto" class="d-none d-md-flex"> <div style="margin-left: auto" class="d-none d-md-flex">
@ -140,9 +140,7 @@
@if (_movies?.Count > 0) @if (_movies?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
Movies Movies
</MudText> </MudText>
@if (_movies.Count > 50) @if (_movies.Count > 50)
@ -168,9 +166,7 @@
@if (_shows?.Count > 0) @if (_shows?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Shows Shows
</MudText> </MudText>
@if (_shows.Count > 50) @if (_shows.Count > 50)
@ -196,9 +192,7 @@
@if (_seasons?.Count > 0) @if (_seasons?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
Seasons Seasons
</MudText> </MudText>
@if (_seasons.Count > 50) @if (_seasons.Count > 50)
@ -224,9 +218,7 @@
@if (_episodes?.Count > 0) @if (_episodes?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Episodes Episodes
</MudText> </MudText>
@if (_episodes.Count > 50) @if (_episodes.Count > 50)
@ -253,9 +245,7 @@
@if (_artists?.Count > 0) @if (_artists?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Artists Artists
</MudText> </MudText>
@if (_artists.Count > 50) @if (_artists.Count > 50)
@ -282,9 +272,7 @@
@if (_musicVideos?.Count > 0) @if (_musicVideos?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Music Videos Music Videos
</MudText> </MudText>
@if (_musicVideos.Count > 50) @if (_musicVideos.Count > 50)
@ -311,9 +299,7 @@
@if (_otherVideos?.Count > 0) @if (_otherVideos?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Other Videos Other Videos
</MudText> </MudText>
@if (_otherVideos.Count > 50) @if (_otherVideos.Count > 50)
@ -340,9 +326,7 @@
@if (_songs?.Count > 0) @if (_songs?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
Songs Songs
</MudText> </MudText>
@if (_songs.Count > 50) @if (_songs.Count > 50)
@ -369,9 +353,7 @@
@if (_images?.Count > 0) @if (_images?.Count > 0)
{ {
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Images Images
</MudText> </MudText>
@if (_images.Count > 50) @if (_images.Count > 50)
@ -604,6 +586,27 @@
} }
} }
if (card is TelevisionSeasonCardViewModel season)
{
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{season.Title} Season {season.TelevisionSeasonNumber}" } };
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 })
{
var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
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} Season {season.TelevisionSeasonNumber} to collection {collection.Name}", Severity.Success));
}
}
if (card is ArtistCardViewModel artist) if (card is ArtistCardViewModel artist)
{ {
var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", artist.Title } }; var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", artist.Title } };

275
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -27,165 +27,155 @@
} }
} }
</MudContainer> </MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px"> <MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 100px">
<div style="display: flex; flex-direction: row;"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Spacing="6">
@if (!string.IsNullOrWhiteSpace(_season?.Poster)) @if (!string.IsNullOrWhiteSpace(_season?.Poster))
{ {
if (_season.Poster.StartsWith("http://") || _season.Poster.StartsWith("https://")) <MudImage Src="@GetPosterUrl(_season.Poster)" Elevation="2" Class="rounded-lg" Style="max-height: 325px; margin-left: auto; margin-right: auto"/>
{
<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%"> <MudStack Row="false" Style="flex: 1">
<MudLink Href="@($"media/tv/shows/{_season?.ShowId}")"> <MudStack Row="false">
<MudText Typo="Typo.h2" Class="media-item-title">@_season?.Title</MudText> <MudLink Href="@($"media/tv/shows/{_season?.ShowId}")">
</MudLink> <MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown">
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_season?.Name</MudText> <MudText Typo="Typo.h4" Class="media-item-title">@_season?.Title</MudText>
<div> </MudHidden>
<MudHidden Invert="true" Breakpoint="Breakpoint.MdAndUp">
<MudText Typo="Typo.h2" Class="media-item-title">@_season?.Title</MudText>
</MudHidden>
</MudLink>
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_season?.Name</MudText>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="mb-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="@AddToCollection">
Add To Collection Add To Collection
</MudButton> </MudButton>
<MudButton Class="ml-3" <MudButton Variant="Variant.Filled"
Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd" StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@AddToPlaylist"> OnClick="@AddToPlaylist">
Add To Playlist Add To Playlist
</MudButton> </MudButton>
<MudButton Class="ml-3" <MudButton Variant="Variant.Filled"
Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Schedule" StartIcon="@Icons.Material.Filled.Schedule"
OnClick="@AddToSchedule"> OnClick="@AddToSchedule">
Add To Schedule Add To Schedule
</MudButton> </MudButton>
</div> </MudStack>
</div> </MudStack>
</div> </MudStack>
</MudContainer> </MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8"> <MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
@foreach (TelevisionEpisodeCardViewModel episode in _data.Cards) <MudStack Row="false" Spacing="6">
{ @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"> <MudCard>
@if (!string.IsNullOrWhiteSpace(episode.Poster)) <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" id="@($"episode-{episode.EpisodeId}")">
{ @if (!string.IsNullOrWhiteSpace(episode.Poster))
<MudPaper style="display: flex; flex-direction: column; position: relative"> {
@if (episode.Poster.StartsWith("http://") || episode.Poster.StartsWith("https://")) <div style="display: flex; flex-direction: column; position: relative">
{ <MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown">
<MudCardMedia Image="@episode.Poster" Style="flex-grow: 1; height: 220px; width: 392px;"/> <MudImage Src="@GetPosterUrl(episode.Poster)" Style="max-height: 220px; max-width: 265px; margin-left: auto; margin-right: auto;"/>
} </MudHidden>
else <MudHidden Invert="true" Breakpoint="Breakpoint.MdAndUp">
{ <MudImage Src="@GetPosterUrl(episode.Poster)" Style="max-height: 220px; max-width: 392px; margin-left: auto; margin-right: auto;"/>
<MudCardMedia Image="@($"artwork/thumbnails/{episode.Poster}")" Style="flex-grow: 1; height: 220px; width: 392px;"/> </MudHidden>
} @if (episode.State == MediaItemState.FileNotFound)
@if (episode.State == MediaItemState.FileNotFound) {
{ <div style="position: absolute; right: 10px; top: 8px;">
<div style="position: absolute; right: 10px; top: 8px;"> <MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/>
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Size="Size.Large"/> </div>
</div> }
} else if (episode.State == MediaItemState.Unavailable)
else if (episode.State == MediaItemState.Unavailable) {
{ <div style="position: absolute; right: 10px; top: 8px;">
<div style="position: absolute; right: 10px; top: 8px;"> <MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/>
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large"/> </div>
</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>
<div class="mt-6"> }
<MudButton Variant="Variant.Filled" <MudCardContent Class="ml-3">
Color="Color.Primary" <div style="display: flex; flex-direction: column; height: 100%">
StartIcon="@Icons.Material.Filled.PlaylistAdd" <MudText Typo="Typo.h4" Class="mb-6">@episode.Episode. @episode.Title</MudText>
OnClick="@(_ => AddEpisodeToPlaylist(episode))"> <MudText Class="d-none d-md-flex" Style="flex-grow: 1">@episode.Plot</MudText>
Add To Playlist <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown">
</MudButton> <MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(_ => AddEpisodeToCollection(episode))">
Add To Collection
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddEpisodeToPlaylist(episode))">
Add To Playlist
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Info"
OnClick="@(_ => ShowInfo(episode))">
Show Media Info
</MudButton>
</MudStack>
</div> </div>
<div class="mt-6"> </MudCardContent>
<MudButton Variant="Variant.Filled" </MudStack>
Color="Color.Secondary" <div class="pl-3 pt-3">
StartIcon="@Icons.Material.Filled.Info" @if (episode.State == MediaItemState.FileNotFound)
OnClick="@(_ => ShowInfo(episode))"> {
Show Media Info <div class="mb-3" style="display: flex; flex-direction: row; flex-wrap: wrap">
</MudButton> <MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Class="mr-2"/>
<MudText>File Not Found:&nbsp;</MudText>
<MudText>@episode.Path</MudText>
</div> </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 class="d-none d-md-flex" style="flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Released: @episode.Aired.ToShortDateString()</MudText>
</div> </div>
</MudCardContent> @if (episode.Directors.Any())
</div> {
<div class="pl-3 pt-3"> var sorted = episode.Directors.OrderBy(w => w).ToList();
@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"> <div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Directors:&nbsp;</MudText> <MudText GutterBottom="true">Directors:&nbsp;</MudText>
<MudLink Href="@(@$"director:""{sorted.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@sorted.Head()</MudLink> <MudLink Href="@(@$"director:""{sorted.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@sorted.Head()</MudLink>
@foreach (string director in sorted.Skip(1)) @foreach (string director in sorted.Skip(1))
{ {
<MudText>,&nbsp;</MudText> <MudText>,&nbsp;</MudText>
<MudLink Href="@(@$"director:""{director.ToLowerInvariant()}""".GetRelativeSearchQuery())">@director</MudLink> <MudLink Href="@(@$"director:""{director.ToLowerInvariant()}""".GetRelativeSearchQuery())">@director</MudLink>
} }
</div> </div>
} }
@if (episode.Writers.Any()) @if (episode.Writers.Any())
{ {
var sorted = episode.Writers.OrderBy(w => w).ToList(); var sorted = episode.Writers.OrderBy(w => w).ToList();
<div style="display: flex; flex-direction: row; flex-wrap: wrap"> <div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Writers:&nbsp;</MudText> <MudText GutterBottom="true">Writers:&nbsp;</MudText>
<MudLink Href="@(@$"writer:""{sorted.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@sorted.Head()</MudLink> <MudLink Href="@(@$"writer:""{sorted.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@sorted.Head()</MudLink>
@foreach (string writer in sorted.Skip(1)) @foreach (string writer in sorted.Skip(1))
{ {
<MudText>,&nbsp;</MudText> <MudText>,&nbsp;</MudText>
<MudLink Href="@(@$"writer:""{writer.ToLowerInvariant()}""".GetRelativeSearchQuery())">@writer</MudLink> <MudLink Href="@(@$"writer:""{writer.ToLowerInvariant()}""".GetRelativeSearchQuery())">@writer</MudLink>
} }
</div> </div>
} }
</div> </div>
</MudCard> </MudCard>
} }
</MudStack>
</MudContainer> </MudContainer>
@code { @code {
@ -196,8 +186,8 @@
private TelevisionSeasonViewModel _season; private TelevisionSeasonViewModel _season;
private int _pageSize => 100; private static int PageSize => 100;
private readonly int _pageNumber = 1; private const int PageNumber = 1;
private TelevisionEpisodeCardResultsViewModel _data = new(0, new List<TelevisionEpisodeCardViewModel>(), null); private TelevisionEpisodeCardResultsViewModel _data = new(0, new List<TelevisionEpisodeCardViewModel>(), null);
@ -229,7 +219,7 @@
await Mediator.Send(new GetTelevisionSeasonById(SeasonId), _cts.Token) await Mediator.Send(new GetTelevisionSeasonById(SeasonId), _cts.Token)
.IfSomeAsync(vm => _season = vm); .IfSomeAsync(vm => _season = vm);
_data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, _pageNumber, _pageSize), _cts.Token); _data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, PageNumber, PageSize), _cts.Token);
} }
private async Task AddToCollection() private async Task AddToCollection()
@ -239,7 +229,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection) if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{ {
await Mediator.Send(new AddSeasonToCollection(collection.Id, SeasonId), _cts.Token); await Mediator.Send(new AddSeasonToCollection(collection.Id, SeasonId), _cts.Token);
NavigationManager.NavigateTo($"media/collections/{collection.Id}"); NavigationManager.NavigateTo($"media/collections/{collection.Id}");
@ -253,7 +243,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToPlaylistDialog>("Add To Playlist", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToPlaylistDialog>("Add To Playlist", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is PlaylistViewModel playlist) if (result is { Canceled: false, Data: PlaylistViewModel playlist })
{ {
await Mediator.Send(new AddSeasonToPlaylist(playlist.Id, SeasonId), _cts.Token); await Mediator.Send(new AddSeasonToPlaylist(playlist.Id, SeasonId), _cts.Token);
NavigationManager.NavigateTo($"media/playlists/{playlist.Id}"); NavigationManager.NavigateTo($"media/playlists/{playlist.Id}");
@ -267,7 +257,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToScheduleDialog>("Add To Schedule", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToScheduleDialog>("Add To Schedule", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule) if (result is { Canceled: false, Data: ProgramScheduleViewModel schedule })
{ {
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items"); NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
@ -281,7 +271,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection) if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{ {
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId); var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token); Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token);
@ -302,7 +292,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToPlaylistDialog>("Add To Playlist", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToPlaylistDialog>("Add To Playlist", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is PlaylistViewModel playlist) if (result is { Canceled: false, Data: PlaylistViewModel playlist })
{ {
var request = new AddEpisodeToPlaylist(playlist.Id, episode.EpisodeId); var request = new AddEpisodeToPlaylist(playlist.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token); Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token);
@ -338,4 +328,9 @@
} }
} }
private static string GetPosterUrl(string poster)
{
return poster.StartsWith("http://") || poster.StartsWith("https://") ? poster : $"artwork/posters/{poster}";
}
} }

2
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -28,7 +28,7 @@
</MudContainer> </MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 100px"> <MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 100px">
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Spacing="6"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Spacing="6">
<MudImage Src="@GetPosterUrl(_show.Poster)" Class="rounded-lg" Style="max-height: 325px" ObjectFit="ObjectFit.Contain" /> <MudImage Src="@GetPosterUrl(_show.Poster)" Elevation="2" Class="rounded-lg" Style="max-height: 325px; margin-left: auto; margin-right: auto" />
<div style="display: flex; flex-direction: column; height: 100%"> <div style="display: flex; flex-direction: column; height: 100%">
<MudStack Row="false"> <MudStack Row="false">
<MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown"> <MudHidden Invert="true" Breakpoint="Breakpoint.SmAndDown">

2
ErsatzTV/Pages/TelevisionSeasonSearchResults.razor

@ -138,7 +138,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection) if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{ {
var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId); var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken); Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);

2
ErsatzTV/Pages/TraktLists.razor

@ -152,7 +152,7 @@
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small };
IDialogReference dialog = await Dialog.ShowAsync<AddTraktListDialog>("Add Trakt List", options); IDialogReference dialog = await Dialog.ShowAsync<AddTraktListDialog>("Add Trakt List", options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is string url) if (result is { Canceled: false, Data: string url })
{ {
await WorkerChannel.WriteAsync(new AddTraktList(url), _cts.Token); await WorkerChannel.WriteAsync(new AddTraktList(url), _cts.Token);
} }

3
ErsatzTV/Shared/MediaCard.razor

@ -1,6 +1,5 @@
@using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards
@using static Prelude @using static Prelude
@inject IMediator Mediator
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<div class="@((ContainerClass ?? "media-card-container mr-6") + " pb-3")" id="@($"item_{Data.MediaItemId}")"> <div class="@((ContainerClass ?? "media-card-container mr-6") + " pb-3")" id="@($"item_{Data.MediaItemId}")">
@ -78,7 +77,7 @@
<MudText Align="Align.Center" Class="media-card-title" UserAttributes="@(new Dictionary<string, object> { { "title", Data.Title } })"> <MudText Align="Align.Center" Class="media-card-title" UserAttributes="@(new Dictionary<string, object> { { "title", Data.Title } })">
@(Title ?? Data.Title) @(Title ?? Data.Title)
</MudText> </MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary"> <MudText Typo="Typo.body2" Align="Align.Center" Class="media-card-title mud-text-secondary">
@(Subtitle ?? Data.Subtitle) @(Subtitle ?? Data.Subtitle)
</MudText> </MudText>
</div> </div>

9
ErsatzTV/Validators/CollectionEditViewModelValidator.cs

@ -1,9 +0,0 @@
using ErsatzTV.ViewModels;
using FluentValidation;
namespace ErsatzTV.Validators;
public class CollectionEditViewModelValidator : AbstractValidator<CollectionEditViewModel>
{
public CollectionEditViewModelValidator() => RuleFor(c => c.Name).NotEmpty();
}

4
ErsatzTV/wwwroot/css/site.css

@ -6,8 +6,6 @@
.media-card-container { width: 152px; } .media-card-container { width: 152px; }
.media-card-episode-container { width: 392px; }
.media-card { .media-card {
display: flex; display: flex;
/*filter: brightness(100%);*/ /*filter: brightness(100%);*/
@ -22,8 +20,6 @@
.media-card-selected-delete { box-shadow: 0 0 0 3px #f44336, 0 0 4px rgba(0, 0, 0, 0.3); } .media-card-selected-delete { box-shadow: 0 0 0 3px #f44336, 0 0 4px rgba(0, 0, 0, 0.3); }
.media-card-episode { width: 392px; }
.media-card:hover { /*filter: brightness(75%);*/ } .media-card:hover { /*filter: brightness(75%);*/ }
.media-card-title { .media-card-title {

Loading…
Cancel
Save