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 @@ -187,7 +187,7 @@ internal static class Mapper
e.EpisodeMetadata.Head(),
maybeJellyfin,
maybeEmby,
false,
true,
string.Empty))
.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 : @@ -79,9 +79,11 @@ public class GetCollectionCardsHandler :
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)

206
ErsatzTV/Pages/Artist.razor

@ -19,17 +19,22 @@ @@ -19,17 +19,22 @@
<img src="@($"artwork/fanart/{_artist.FanArt}")" alt="fan art"/>
}
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px">
<div style="display: flex; flex-direction: row;" class="mb-6">
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 100px">
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Spacing="6">
@if (!string.IsNullOrWhiteSpace(_artist.Thumbnail))
{
<img class="mud-elevation-2 mr-6"
style="border-radius: 4px; flex-shrink: 0; height: 220px; width: 220px"
src="@($"artwork/thumbnails/{_artist.Thumbnail}")" alt="artist thumbnail"/>
<MudImage Elevation="2" Src="@($"artwork/thumbnails/{_artist.Thumbnail}")" Class="rounded-lg" Style="height: 220px; width: 220px; margin-left: auto; margin-right: auto;" />
}
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h2" Class="media-item-title">@_artist.Name</MudText>
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_artist.Disambiguation</MudText>
<MudStack Row="false">
<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))
{
<MudCard Elevation="2" Class="mb-6">
@ -47,23 +52,22 @@ @@ -47,23 +52,22 @@
</MudCardContent>
</MudCard>
}
<div>
<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 Class="ml-3"
Variant="Variant.Filled"
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Schedule"
OnClick="@AddToSchedule">
Add To Schedule
</MudButton>
</div>
</MudStack>
</div>
</div>
</MudStack>
<MudCard Class="mb-6">
<MudCardContent>
@if (_sortedLanguages.Any())
@ -118,86 +122,110 @@ @@ -118,86 +122,110 @@
</MudCard>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
@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">
@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;"/>
@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"/>
<MudStack Row="false" Spacing="6">
@foreach (MusicVideoCardViewModel musicVideo in _musicVideos.Cards)
{
<MudCard>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" id="@($"music-video-{musicVideo.MusicVideoId}")">
@if (!string.IsNullOrWhiteSpace(musicVideo.Poster))
{
<div style="display: flex; flex-direction: column; position: relative">
<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>
}
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>
}
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"/>
</MudHidden>
}
<MudCardContent>
<div style="display: flex; flex-direction: column; height: 100%">
<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>
}
</MudPaper>
}
else
</div>
</MudCardContent>
</MudStack>
@if (musicVideo.State == MediaItemState.FileNotFound)
{
<div style="display: flex; height: 220px; position: relative; width: 293px;">
<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 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>
}
<MudCardContent Class="ml-3">
<div style="display: flex; flex-direction: column; height: 100%">
<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>
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>
</MudCardContent>
</div>
@if (musicVideo.State == MediaItemState.FileNotFound)
{
<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>
}
}
</MudCard>
}
</MudStack>
</MudContainer>
@code {
@ -242,7 +270,7 @@ @@ -242,7 +270,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
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);
NavigationManager.NavigateTo($"media/collections/{collection.Id}");
@ -256,7 +284,7 @@ @@ -256,7 +284,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToScheduleDialog>("Add To Schedule", parameters, options);
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);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
@ -270,7 +298,7 @@ @@ -270,7 +298,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
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);
Either<BaseError, Unit> addResult = await Mediator.Send(request, _cts.Token);

48
ErsatzTV/Pages/CollectionEditor.razor

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

564
ErsatzTV/Pages/CollectionItems.razor

@ -5,313 +5,333 @@ @@ -5,313 +5,333 @@
@inject NavigationManager NavigationManager
@inject IJSRuntime JsRuntime
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;">
<div style="align-items: center; display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%;" class="ml-6 mr-6">
@if (IsSelectMode())
{
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
<div style="margin-left: auto">
<MudButton Variant="Variant.Filled"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.Remove"
OnClick="@(_ => RemoveSelectionFromCollection(Id))">
Remove From Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="@(_ => ClearSelection())">
Clear Selection
</MudButton>
</div>
}
else
{
<div style="align-items: center; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4">@_data?.Name</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"media/collections/{Id}/edit")"/>
</div>
<MudForm Style="max-height: 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%; align-items: center" class="ml-6 mr-6">
@if (IsSelectMode())
{
<div class="flex-grow-1">
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
</div>
<div style="margin-left: auto" class="d-none d-md-flex">
<MudButton Variant="Variant.Filled"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.Remove"
OnClick="@(_ => RemoveSelectionFromCollection(Id))">
Remove From Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="@(_ => ClearSelection())">
Clear Selection
</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.Remove" Label="Remove From Collection" OnClick="@(_ => RemoveSelectionFromCollection(Id))"/>
<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)
{
<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)
{
<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)
{
<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)
{
<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)
{
<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)
{
<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)
{
<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)
{
<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)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")">@_data.ImageCards.Count Images</MudLink>
}
@if (SupportsCustomOrdering())
{
<div style="margin-left: auto">
<MudSwitch T="bool"
Value="@(_data?.UseCustomPlaybackOrder == true)"
Color="Color.Primary"
ValueChanged="@OnUseCustomOrderChanged"
Label="Use Custom Playback Order"/>
</div>
}
}
</div>
</MudPaper>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="margin-top: 64px">
@if (_data?.MovieCards.Count > 0)
{
<MudText GutterBottom="true"
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()"/>
<MudText GutterBottom="true"
Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Images
</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@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()"/>
}
</MudStack>
}
</MudContainer>
}
@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>
</div>
</MudForm>
@code {
@ -512,7 +532,7 @@ @@ -512,7 +532,7 @@
IDialogReference dialog = await Dialog.ShowAsync<RemoveFromCollectionDialog>("Remove From Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is { Canceled: false })
{
await Mediator.Send(request, CancellationToken);
await RefreshData();

268
ErsatzTV/Pages/Collections.razor

@ -6,130 +6,146 @@ @@ -6,130 +6,146 @@
@inject IDialogService Dialog
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="media/collections/add">
Add Collection
</MudButton>
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" Href="media/multi-collections/add">
Add Multi Collection
</MudButton>
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
<div style="margin-right: auto" class="d-none d-md-flex">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Href="media/collections/add">
Add Collection
</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>
<MudTable Class="mt-4"
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>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
@ -167,7 +183,7 @@ @@ -167,7 +183,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is { Canceled: false })
{
await Mediator.Send(new DeleteCollection(collection.Id), _cts.Token);
if (_collectionsTable != null)
@ -184,7 +200,7 @@ @@ -184,7 +200,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Multi Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is { Canceled: false })
{
await Mediator.Send(new DeleteMultiCollection(collection.Id), _cts.Token);
if (_multiCollectionsTable != null)
@ -201,7 +217,7 @@ @@ -201,7 +217,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Smart Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is { Canceled: false })
{
await Mediator.Send(new DeleteSmartCollection(collection.Id), _cts.Token);
if (_smartCollectionsTable != null)

2
ErsatzTV/Pages/Movie.razor

@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
@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" />
<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)
{
<div style="position: absolute; right: 10px; top: 8px;">

61
ErsatzTV/Pages/Search.razor

@ -10,10 +10,10 @@ @@ -10,10 +10,10 @@
<MudForm Style="max-height: 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())
{
<div style="align-items: center; display: flex; width: 100%;">
<div class="flex-grow-1">
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
</div>
<div style="margin-left: auto" class="d-none d-md-flex">
@ -140,9 +140,7 @@ @@ -140,9 +140,7 @@
@if (_movies?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
Movies
</MudText>
@if (_movies.Count > 50)
@ -168,9 +166,7 @@ @@ -168,9 +166,7 @@
@if (_shows?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Shows
</MudText>
@if (_shows.Count > 50)
@ -196,9 +192,7 @@ @@ -196,9 +192,7 @@
@if (_seasons?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
Seasons
</MudText>
@if (_seasons.Count > 50)
@ -224,9 +218,7 @@ @@ -224,9 +218,7 @@
@if (_episodes?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Episodes
</MudText>
@if (_episodes.Count > 50)
@ -253,9 +245,7 @@ @@ -253,9 +245,7 @@
@if (_artists?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Artists
</MudText>
@if (_artists.Count > 50)
@ -282,9 +272,7 @@ @@ -282,9 +272,7 @@
@if (_musicVideos?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Music Videos
</MudText>
@if (_musicVideos.Count > 50)
@ -311,9 +299,7 @@ @@ -311,9 +299,7 @@
@if (_otherVideos?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Other Videos
</MudText>
@if (_otherVideos.Count > 50)
@ -340,9 +326,7 @@ @@ -340,9 +326,7 @@
@if (_songs?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
Songs
</MudText>
@if (_songs.Count > 50)
@ -369,9 +353,7 @@ @@ -369,9 +353,7 @@
@if (_images?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Images
</MudText>
@if (_images.Count > 50)
@ -604,6 +586,27 @@ @@ -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)
{
var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", artist.Title } };

275
ErsatzTV/Pages/TelevisionEpisodeList.razor

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

2
ErsatzTV/Pages/TelevisionSeasonSearchResults.razor

@ -138,7 +138,7 @@ @@ -138,7 +138,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
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);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);

2
ErsatzTV/Pages/TraktLists.razor

@ -152,7 +152,7 @@ @@ -152,7 +152,7 @@
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small };
IDialogReference dialog = await Dialog.ShowAsync<AddTraktListDialog>("Add Trakt List", options);
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);
}

3
ErsatzTV/Shared/MediaCard.razor

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

9
ErsatzTV/Validators/CollectionEditViewModelValidator.cs

@ -1,9 +0,0 @@ @@ -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 @@ @@ -6,8 +6,6 @@
.media-card-container { width: 152px; }
.media-card-episode-container { width: 392px; }
.media-card {
display: flex;
/*filter: brightness(100%);*/
@ -22,8 +20,6 @@ @@ -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-episode { width: 392px; }
.media-card:hover { /*filter: brightness(75%);*/ }
.media-card-title {

Loading…
Cancel
Save