mirror of https://github.com/ErsatzTV/ErsatzTV.git
				
				
			
			
			
				Browse Source
			
			
			
			
				
		* flag local movies as file not found * show warning icon on cards * unflag movie that is found during scan * skip missing files when building playouts * add state to search index * add file not found health check * link to search from file not found health check * support flagging other media kinds as file not found * continue to schedule missing items * support episode files not found * wip trash page * fix trash url * trash page is functional * update changelog * fix changelog mergepull/572/head
				 73 changed files with 5264 additions and 196 deletions
			
			
		@ -0,0 +1,9 @@
				@@ -0,0 +1,9 @@
					 | 
				
			||||
using System.Collections.Generic; | 
				
			||||
using ErsatzTV.Core; | 
				
			||||
using LanguageExt; | 
				
			||||
using MediatR; | 
				
			||||
using Unit = LanguageExt.Unit; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Application.Maintenance.Commands; | 
				
			||||
 | 
				
			||||
public record DeleteItemsFromDatabase(List<int> MediaItemIds) : IRequest<Either<BaseError, Unit>>; | 
				
			||||
@ -0,0 +1,38 @@
				@@ -0,0 +1,38 @@
					 | 
				
			||||
using System.Threading; | 
				
			||||
using System.Threading.Tasks; | 
				
			||||
using ErsatzTV.Core; | 
				
			||||
using ErsatzTV.Core.Interfaces.Repositories; | 
				
			||||
using ErsatzTV.Core.Interfaces.Search; | 
				
			||||
using LanguageExt; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Application.Maintenance.Commands | 
				
			||||
{ | 
				
			||||
    public class | 
				
			||||
        DeleteItemsFromDatabaseHandler : MediatR.IRequestHandler<DeleteItemsFromDatabase, Either<BaseError, Unit>> | 
				
			||||
    { | 
				
			||||
        private readonly IMediaItemRepository _mediaItemRepository; | 
				
			||||
        private readonly ISearchIndex _searchIndex; | 
				
			||||
 | 
				
			||||
        public DeleteItemsFromDatabaseHandler( | 
				
			||||
            IMediaItemRepository mediaItemRepository, | 
				
			||||
            ISearchIndex searchIndex) | 
				
			||||
        { | 
				
			||||
            _mediaItemRepository = mediaItemRepository; | 
				
			||||
            _searchIndex = searchIndex; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        public async Task<Either<BaseError, Unit>> Handle( | 
				
			||||
            DeleteItemsFromDatabase request, | 
				
			||||
            CancellationToken cancellationToken) | 
				
			||||
        { | 
				
			||||
            Either<BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(request.MediaItemIds); | 
				
			||||
            if (deleteResult.IsRight) | 
				
			||||
            { | 
				
			||||
                await _searchIndex.RemoveItems(request.MediaItemIds); | 
				
			||||
                _searchIndex.Commit(); | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            return deleteResult; | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -1,5 +1,7 @@
				@@ -1,5 +1,7 @@
					 | 
				
			||||
namespace ErsatzTV.Application.MediaCards | 
				
			||||
using ErsatzTV.Core.Domain; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Application.MediaCards | 
				
			||||
{ | 
				
			||||
    public record ActorCardViewModel(int Id, string Name, string Role, string Thumb) : | 
				
			||||
        MediaCardViewModel(Id, Name, Role, Name, Thumb); | 
				
			||||
    public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) : | 
				
			||||
        MediaCardViewModel(Id, Name, Role, Name, Thumb, State); | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -1,10 +1,19 @@
				@@ -1,10 +1,19 @@
					 | 
				
			||||
namespace ErsatzTV.Application.MediaCards | 
				
			||||
using ErsatzTV.Core.Domain; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Application.MediaCards | 
				
			||||
{ | 
				
			||||
    public record ArtistCardViewModel | 
				
			||||
        (int ArtistId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( | 
				
			||||
            ArtistId, | 
				
			||||
            Title, | 
				
			||||
            Subtitle, | 
				
			||||
            SortTitle, | 
				
			||||
            Poster); | 
				
			||||
    ( | 
				
			||||
        int ArtistId, | 
				
			||||
        string Title, | 
				
			||||
        string Subtitle, | 
				
			||||
        string SortTitle, | 
				
			||||
        string Poster, | 
				
			||||
        MediaItemState State) : MediaCardViewModel( | 
				
			||||
        ArtistId, | 
				
			||||
        Title, | 
				
			||||
        Subtitle, | 
				
			||||
        SortTitle, | 
				
			||||
        Poster, | 
				
			||||
        State); | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -1,4 +1,12 @@
				@@ -1,4 +1,12 @@
					 | 
				
			||||
namespace ErsatzTV.Application.MediaCards | 
				
			||||
using ErsatzTV.Core.Domain; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Application.MediaCards | 
				
			||||
{ | 
				
			||||
    public record MediaCardViewModel(int MediaItemId, string Title, string Subtitle, string SortTitle, string Poster); | 
				
			||||
    public record MediaCardViewModel( | 
				
			||||
        int MediaItemId, | 
				
			||||
        string Title, | 
				
			||||
        string Subtitle, | 
				
			||||
        string SortTitle, | 
				
			||||
        string Poster, | 
				
			||||
        MediaItemState State); | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -1,10 +1,19 @@
				@@ -1,10 +1,19 @@
					 | 
				
			||||
namespace ErsatzTV.Application.MediaCards | 
				
			||||
using ErsatzTV.Core.Domain; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Application.MediaCards | 
				
			||||
{ | 
				
			||||
    public record TelevisionShowCardViewModel | 
				
			||||
        (int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( | 
				
			||||
            TelevisionShowId, | 
				
			||||
            Title, | 
				
			||||
            Subtitle, | 
				
			||||
            SortTitle, | 
				
			||||
            Poster); | 
				
			||||
    ( | 
				
			||||
        int TelevisionShowId, | 
				
			||||
        string Title, | 
				
			||||
        string Subtitle, | 
				
			||||
        string SortTitle, | 
				
			||||
        string Poster, | 
				
			||||
        MediaItemState State) : MediaCardViewModel( | 
				
			||||
        TelevisionShowId, | 
				
			||||
        Title, | 
				
			||||
        Subtitle, | 
				
			||||
        SortTitle, | 
				
			||||
        Poster, | 
				
			||||
        State); | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -1,11 +1,17 @@
				@@ -1,11 +1,17 @@
					 | 
				
			||||
using ErsatzTV.Application.MediaCards; | 
				
			||||
using ErsatzTV.Core.Domain; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Application.MediaCollections | 
				
			||||
{ | 
				
			||||
    public record MediaCollectionViewModel(int Id, string Name, bool UseCustomPlaybackOrder) : MediaCardViewModel( | 
				
			||||
    public record MediaCollectionViewModel( | 
				
			||||
        int Id, | 
				
			||||
        string Name, | 
				
			||||
        bool UseCustomPlaybackOrder, | 
				
			||||
        MediaItemState State) : MediaCardViewModel( | 
				
			||||
        Id, | 
				
			||||
        Name, | 
				
			||||
        string.Empty, | 
				
			||||
        Name, | 
				
			||||
        string.Empty); | 
				
			||||
        string.Empty, | 
				
			||||
        State); | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -1,14 +1,14 @@
				@@ -1,14 +1,14 @@
					 | 
				
			||||
using System.Collections.Generic; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Core.Domain | 
				
			||||
namespace ErsatzTV.Core.Domain; | 
				
			||||
 | 
				
			||||
public class MediaItem | 
				
			||||
{ | 
				
			||||
    public class MediaItem | 
				
			||||
    { | 
				
			||||
        public int Id { get; set; } | 
				
			||||
        public int LibraryPathId { get; set; } | 
				
			||||
        public LibraryPath LibraryPath { get; set; } | 
				
			||||
        public List<Collection> Collections { get; set; } | 
				
			||||
        public List<CollectionItem> CollectionItems { get; set; } | 
				
			||||
        public List<TraktListItem> TraktListItems { get; set; } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
    public int Id { get; set; } | 
				
			||||
    public int LibraryPathId { get; set; } | 
				
			||||
    public LibraryPath LibraryPath { get; set; } | 
				
			||||
    public List<Collection> Collections { get; set; } | 
				
			||||
    public List<CollectionItem> CollectionItems { get; set; } | 
				
			||||
    public List<TraktListItem> TraktListItems { get; set; } | 
				
			||||
    public MediaItemState State { get; set; } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,7 @@
				@@ -0,0 +1,7 @@
					 | 
				
			||||
namespace ErsatzTV.Core.Domain; | 
				
			||||
 | 
				
			||||
public enum MediaItemState | 
				
			||||
{ | 
				
			||||
    Normal = 0, | 
				
			||||
    FileNotFound = 1 | 
				
			||||
} | 
				
			||||
@ -0,0 +1,5 @@
				@@ -0,0 +1,5 @@
					 | 
				
			||||
namespace ErsatzTV.Core.Health.Checks; | 
				
			||||
 | 
				
			||||
public interface IFileNotFoundHealthCheck : IHealthCheck | 
				
			||||
{ | 
				
			||||
} | 
				
			||||
@ -1,4 +1,6 @@
				@@ -1,4 +1,6 @@
					 | 
				
			||||
namespace ErsatzTV.Core.Health | 
				
			||||
using LanguageExt; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Core.Health | 
				
			||||
{ | 
				
			||||
    public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message); | 
				
			||||
    public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message, Option<string> Link); | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -1,10 +1,15 @@
				@@ -1,10 +1,15 @@
					 | 
				
			||||
using System.Collections.Generic; | 
				
			||||
using System.Threading.Tasks; | 
				
			||||
using ErsatzTV.Core.Domain; | 
				
			||||
using LanguageExt; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Core.Interfaces.Repositories | 
				
			||||
{ | 
				
			||||
    public interface IMediaItemRepository | 
				
			||||
    { | 
				
			||||
        Task<List<string>> GetAllLanguageCodes(); | 
				
			||||
        Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path); | 
				
			||||
        Task<Unit> FlagNormal(MediaItem mediaItem); | 
				
			||||
        Task<Either<BaseError, Unit>> DeleteItems(List<int> mediaItemIds); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
				 
					 | 
				
			||||
@ -0,0 +1,76 @@
				@@ -0,0 +1,76 @@
					 | 
				
			||||
using System.Collections.Generic; | 
				
			||||
using System.Linq; | 
				
			||||
using System.Threading.Tasks; | 
				
			||||
using ErsatzTV.Core.Domain; | 
				
			||||
using ErsatzTV.Core.Extensions; | 
				
			||||
using ErsatzTV.Core.Health; | 
				
			||||
using ErsatzTV.Core.Health.Checks; | 
				
			||||
using ErsatzTV.Infrastructure.Data; | 
				
			||||
using Microsoft.EntityFrameworkCore; | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Infrastructure.Health.Checks; | 
				
			||||
 | 
				
			||||
public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck | 
				
			||||
{ | 
				
			||||
    private readonly IDbContextFactory<TvContext> _dbContextFactory; | 
				
			||||
 | 
				
			||||
    public FileNotFoundHealthCheck(IDbContextFactory<TvContext> dbContextFactory) => | 
				
			||||
        _dbContextFactory = dbContextFactory; | 
				
			||||
 | 
				
			||||
    protected override string Title => "File Not Found"; | 
				
			||||
 | 
				
			||||
    public async Task<HealthCheckResult> Check() | 
				
			||||
    { | 
				
			||||
        await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); | 
				
			||||
 | 
				
			||||
            List<Episode> episodes = await dbContext.Episodes | 
				
			||||
                .Filter(e => e.State == MediaItemState.FileNotFound) | 
				
			||||
                .Include(e => e.MediaVersions) | 
				
			||||
                .ThenInclude(mv => mv.MediaFiles) | 
				
			||||
                .ToListAsync(); | 
				
			||||
 | 
				
			||||
            List<Movie> movies = await dbContext.Movies | 
				
			||||
                .Filter(m => m.State == MediaItemState.FileNotFound) | 
				
			||||
                .Include(m => m.MediaVersions) | 
				
			||||
                .ThenInclude(mv => mv.MediaFiles) | 
				
			||||
                .ToListAsync(); | 
				
			||||
             | 
				
			||||
            List<MusicVideo> musicVideos = await dbContext.MusicVideos | 
				
			||||
                .Filter(mv => mv.State == MediaItemState.FileNotFound) | 
				
			||||
                .Include(mv => mv.MediaVersions) | 
				
			||||
                .ThenInclude(mv => mv.MediaFiles) | 
				
			||||
                .ToListAsync(); | 
				
			||||
             | 
				
			||||
            List<OtherVideo> otherVideos = await dbContext.OtherVideos | 
				
			||||
                .Filter(ov => ov.State == MediaItemState.FileNotFound) | 
				
			||||
                .Include(ov => ov.MediaVersions) | 
				
			||||
                .ThenInclude(mv => mv.MediaFiles) | 
				
			||||
                .ToListAsync(); | 
				
			||||
 | 
				
			||||
            List<Song> songs = await dbContext.Songs | 
				
			||||
                .Filter(s => s.State == MediaItemState.FileNotFound) | 
				
			||||
                .Include(s => s.MediaVersions) | 
				
			||||
                .ThenInclude(mv => mv.MediaFiles) | 
				
			||||
                .ToListAsync(); | 
				
			||||
             | 
				
			||||
            var all = movies.Map(m => m.MediaVersions.Head().MediaFiles.Head().Path) | 
				
			||||
                .Append(episodes.Map(e => e.MediaVersions.Head().MediaFiles.Head().Path)) | 
				
			||||
                .Append(musicVideos.Map(mv => mv.GetHeadVersion().MediaFiles.Head().Path)) | 
				
			||||
                .Append(otherVideos.Map(ov => ov.GetHeadVersion().MediaFiles.Head().Path)) | 
				
			||||
                .Append(songs.Map(s => s.GetHeadVersion().MediaFiles.Head().Path)) | 
				
			||||
                .ToList(); | 
				
			||||
 | 
				
			||||
            if (all.Any()) | 
				
			||||
            { | 
				
			||||
                var paths = all.Take(5).ToList(); | 
				
			||||
 | 
				
			||||
                var files = string.Join(", ", paths); | 
				
			||||
 | 
				
			||||
                return WarningResult( | 
				
			||||
                    $"There are {all.Count} files that do not exist on disk, including the following: {files}", | 
				
			||||
                    $"/search?query=state%3AFileNotFound"); | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            return OkResult(); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
									
										
											File diff suppressed because it is too large
											Load Diff
										
									
								
							
						@ -0,0 +1,26 @@
				@@ -0,0 +1,26 @@
					 | 
				
			||||
using Microsoft.EntityFrameworkCore.Migrations; | 
				
			||||
 | 
				
			||||
#nullable disable | 
				
			||||
 | 
				
			||||
namespace ErsatzTV.Infrastructure.Migrations | 
				
			||||
{ | 
				
			||||
    public partial class Add_MediaItemState : Migration | 
				
			||||
    { | 
				
			||||
        protected override void Up(MigrationBuilder migrationBuilder) | 
				
			||||
        { | 
				
			||||
            migrationBuilder.AddColumn<int>( | 
				
			||||
                name: "State", | 
				
			||||
                table: "MediaItem", | 
				
			||||
                type: "INTEGER", | 
				
			||||
                nullable: false, | 
				
			||||
                defaultValue: 0); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        protected override void Down(MigrationBuilder migrationBuilder) | 
				
			||||
        { | 
				
			||||
            migrationBuilder.DropColumn( | 
				
			||||
                name: "State", | 
				
			||||
                table: "MediaItem"); | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,564 @@
				@@ -0,0 +1,564 @@
					 | 
				
			||||
@page "/media/trash" | 
				
			||||
@using ErsatzTV.Application.MediaCards | 
				
			||||
@using ErsatzTV.Application.Search.Queries | 
				
			||||
@using ErsatzTV.Extensions | 
				
			||||
@using Unit = LanguageExt.Unit | 
				
			||||
@using ErsatzTV.Application.Maintenance.Commands | 
				
			||||
@inherits MultiSelectBase<Search> | 
				
			||||
@inject NavigationManager _navigationManager | 
				
			||||
@inject ChannelWriter<IBackgroundServiceRequest> _channel | 
				
			||||
 | 
				
			||||
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;"> | 
				
			||||
    <div style="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.Delete" | 
				
			||||
                           OnClick="@(_ => DeleteFromDatabase())"> | 
				
			||||
                    Delete From Database | 
				
			||||
                </MudButton> | 
				
			||||
                <MudButton Class="ml-3" | 
				
			||||
                           Variant="Variant.Filled" | 
				
			||||
                           Color="Color.Secondary" | 
				
			||||
                           StartIcon="@Icons.Material.Filled.Check" | 
				
			||||
                           OnClick="@(_ => ClearSelection())"> | 
				
			||||
                    Clear Selection | 
				
			||||
                </MudButton> | 
				
			||||
            </div> | 
				
			||||
        } | 
				
			||||
        else | 
				
			||||
        { | 
				
			||||
            if (_movies?.Count > 0) | 
				
			||||
            { | 
				
			||||
                <MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#movies")" Style="margin-bottom: auto; margin-top: auto">@_movies.Count Movies</MudLink> | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            if (_shows?.Count > 0) | 
				
			||||
            { | 
				
			||||
                <MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink> | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            if (_seasons?.Count > 0) | 
				
			||||
            { | 
				
			||||
                <MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#seasons")" Style="margin-bottom: auto; margin-top: auto">@_seasons.Count Seasons</MudLink> | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            if (_episodes?.Count > 0) | 
				
			||||
            { | 
				
			||||
                <MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#episodes")" Style="margin-bottom: auto; margin-top: auto">@_episodes.Count Episodes</MudLink> | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            if (_artists?.Count > 0) | 
				
			||||
            { | 
				
			||||
                <MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#artists")" Style="margin-bottom: auto; margin-top: auto">@_artists.Count Artists</MudLink> | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            if (_musicVideos?.Count > 0) | 
				
			||||
            { | 
				
			||||
                <MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#music_videos")" Style="margin-bottom: auto; margin-top: auto">@_musicVideos.Count Music Videos</MudLink> | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            if (_otherVideos?.Count > 0) | 
				
			||||
            { | 
				
			||||
                <MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#other_videos")" Style="margin-bottom: auto; margin-top: auto">@_otherVideos.Count Other Videos</MudLink> | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            if (_songs?.Count > 0) | 
				
			||||
            { | 
				
			||||
                <MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink> | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            if (_movies?.Count == 0 && _shows?.Count == 0 && _seasons?.Count == 0 && _episodes?.Count == 0 && _artists?.Count == 0 && _musicVideos?.Count == 0 && _otherVideos?.Count == 0 && _songs?.Count == 0) | 
				
			||||
            { | 
				
			||||
                <MudText>Nothing to see here...</MudText> | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
    </div> | 
				
			||||
</MudPaper> | 
				
			||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="margin-top: 96px"> | 
				
			||||
    @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" } })"> | 
				
			||||
                Movies | 
				
			||||
            </MudText> | 
				
			||||
            @if (_movies.Count > 50) | 
				
			||||
            { | 
				
			||||
                <MudLink Href="@GetMoviesLink()" Class="ml-4">See All >></MudLink> | 
				
			||||
            } | 
				
			||||
        </div> | 
				
			||||
 | 
				
			||||
        <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> | 
				
			||||
            @foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle)) | 
				
			||||
            { | 
				
			||||
                <MediaCard Data="@card" | 
				
			||||
                           Link="@($"/media/movies/{card.MovieId}")" | 
				
			||||
                           DeleteClicked="@DeleteItemFromDatabase" | 
				
			||||
                           SelectColor="@Color.Error" | 
				
			||||
                           SelectClicked="@(e => SelectClicked(card, e))" | 
				
			||||
                           IsSelected="@IsSelected(card)" | 
				
			||||
                           IsSelectMode="@IsSelectMode()"/> | 
				
			||||
            } | 
				
			||||
        </MudContainer> | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    @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" } })"> | 
				
			||||
                Shows | 
				
			||||
            </MudText> | 
				
			||||
            @if (_shows.Count > 50) | 
				
			||||
            { | 
				
			||||
                <MudLink Href="@GetShowsLink()" Class="ml-4">See All >></MudLink> | 
				
			||||
            } | 
				
			||||
        </div> | 
				
			||||
 | 
				
			||||
        <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> | 
				
			||||
            @foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
            { | 
				
			||||
                <MediaCard Data="@card" | 
				
			||||
                           Link="@($"/media/tv/shows/{card.TelevisionShowId}")" | 
				
			||||
                           DeleteClicked="@DeleteItemFromDatabase" | 
				
			||||
                           SelectColor="@Color.Error" | 
				
			||||
                           SelectClicked="@(e => SelectClicked(card, e))" | 
				
			||||
                           IsSelected="@IsSelected(card)" | 
				
			||||
                           IsSelectMode="@IsSelectMode()"/> | 
				
			||||
            } | 
				
			||||
        </MudContainer> | 
				
			||||
    } | 
				
			||||
     | 
				
			||||
    @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" } })"> | 
				
			||||
                Seasons | 
				
			||||
            </MudText> | 
				
			||||
            @if (_seasons.Count > 50) | 
				
			||||
            { | 
				
			||||
                <MudLink Href="@GetSeasonsLink()" Class="ml-4">See All >></MudLink> | 
				
			||||
            } | 
				
			||||
        </div> | 
				
			||||
 | 
				
			||||
        <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> | 
				
			||||
            @foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
            { | 
				
			||||
                <MediaCard Data="@card" | 
				
			||||
                           Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")" | 
				
			||||
                           DeleteClicked="@DeleteItemFromDatabase" | 
				
			||||
                           SelectColor="@Color.Error" | 
				
			||||
                           SelectClicked="@(e => SelectClicked(card, e))" | 
				
			||||
                           IsSelected="@IsSelected(card)" | 
				
			||||
                           IsSelectMode="@IsSelectMode()"/> | 
				
			||||
            } | 
				
			||||
        </MudContainer> | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    @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" } })"> | 
				
			||||
                Episodes | 
				
			||||
            </MudText> | 
				
			||||
            @if (_episodes.Count > 50) | 
				
			||||
            { | 
				
			||||
                <MudLink Href="@GetEpisodesLink()" Class="ml-4">See All >></MudLink> | 
				
			||||
            } | 
				
			||||
        </div> | 
				
			||||
 | 
				
			||||
        <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> | 
				
			||||
            @foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
            { | 
				
			||||
                <MediaCard Data="@card" | 
				
			||||
                           DeleteClicked="@DeleteItemFromDatabase" | 
				
			||||
                           SelectColor="@Color.Error" | 
				
			||||
                           Link="@($"/media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")" | 
				
			||||
                           Subtitle="@($"{card.ShowTitle} - S{card.Season} E{card.Episode}")" | 
				
			||||
                           SelectClicked="@(e => SelectClicked(card, e))" | 
				
			||||
                           IsSelected="@IsSelected(card)" | 
				
			||||
                           IsSelectMode="@IsSelectMode()"/> | 
				
			||||
            } | 
				
			||||
        </MudContainer> | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    @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" } })"> | 
				
			||||
                Artists | 
				
			||||
            </MudText> | 
				
			||||
            @if (_artists.Count > 50) | 
				
			||||
            { | 
				
			||||
                <MudLink Href="@GetArtistsLink()" Class="ml-4">See All >></MudLink> | 
				
			||||
            } | 
				
			||||
        </div> | 
				
			||||
 | 
				
			||||
        <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> | 
				
			||||
            @foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
            { | 
				
			||||
                <MediaCard Data="@card" | 
				
			||||
                           Link="@($"/media/music/artists/{card.ArtistId}")" | 
				
			||||
                           ArtworkKind="ArtworkKind.Thumbnail" | 
				
			||||
                           DeleteClicked="@DeleteItemFromDatabase" | 
				
			||||
                           SelectColor="@Color.Error" | 
				
			||||
                           SelectClicked="@(e => SelectClicked(card, e))" | 
				
			||||
                           IsSelected="@IsSelected(card)" | 
				
			||||
                           IsSelectMode="@IsSelectMode()"/> | 
				
			||||
            } | 
				
			||||
        </MudContainer> | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    @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" } })"> | 
				
			||||
                Music Videos | 
				
			||||
            </MudText> | 
				
			||||
            @if (_musicVideos.Count > 50) | 
				
			||||
            { | 
				
			||||
                <MudLink Href="@GetMusicVideosLink()" Class="ml-4">See All >></MudLink> | 
				
			||||
            } | 
				
			||||
        </div> | 
				
			||||
 | 
				
			||||
        <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> | 
				
			||||
            @foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
            { | 
				
			||||
                <MediaCard Data="@card" | 
				
			||||
                           Link="" | 
				
			||||
                           ArtworkKind="ArtworkKind.Thumbnail" | 
				
			||||
                           DeleteClicked="@DeleteItemFromDatabase" | 
				
			||||
                           SelectColor="@Color.Error" | 
				
			||||
                           SelectClicked="@(e => SelectClicked(card, e))" | 
				
			||||
                           IsSelected="@IsSelected(card)" | 
				
			||||
                           IsSelectMode="@IsSelectMode()"/> | 
				
			||||
            } | 
				
			||||
        </MudContainer> | 
				
			||||
    } | 
				
			||||
     | 
				
			||||
    @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" } })"> | 
				
			||||
                Other Videos | 
				
			||||
            </MudText> | 
				
			||||
            @if (_otherVideos.Count > 50) | 
				
			||||
            { | 
				
			||||
                <MudLink Href="@GetOtherVideosLink()" Class="ml-4">See All >></MudLink> | 
				
			||||
            } | 
				
			||||
        </div> | 
				
			||||
 | 
				
			||||
        <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> | 
				
			||||
            @foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
            { | 
				
			||||
                <MediaCard Data="@card" | 
				
			||||
                           Link="" | 
				
			||||
                           ArtworkKind="ArtworkKind.Thumbnail" | 
				
			||||
                           DeleteClicked="@DeleteItemFromDatabase" | 
				
			||||
                           SelectColor="@Color.Error" | 
				
			||||
                           SelectClicked="@(e => SelectClicked(card, e))" | 
				
			||||
                           IsSelected="@IsSelected(card)" | 
				
			||||
                           IsSelectMode="@IsSelectMode()"/> | 
				
			||||
            } | 
				
			||||
        </MudContainer> | 
				
			||||
    } | 
				
			||||
     | 
				
			||||
    @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" } })"> | 
				
			||||
                    Songs | 
				
			||||
                </MudText> | 
				
			||||
                @if (_songs.Count > 50) | 
				
			||||
                { | 
				
			||||
                    <MudLink Href="@GetSongsLink()" Class="ml-4">See All >></MudLink> | 
				
			||||
                } | 
				
			||||
            </div> | 
				
			||||
     | 
				
			||||
            <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> | 
				
			||||
                @foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
                { | 
				
			||||
                    <MediaCard Data="@card" | 
				
			||||
                               Link="" | 
				
			||||
                               ArtworkKind="ArtworkKind.Thumbnail" | 
				
			||||
                               DeleteClicked="@DeleteItemFromDatabase" | 
				
			||||
                               SelectColor="@Color.Error" | 
				
			||||
                               SelectClicked="@(e => SelectClicked(card, e))" | 
				
			||||
                               IsSelected="@IsSelected(card)" | 
				
			||||
                               IsSelectMode="@IsSelectMode()"/> | 
				
			||||
                } | 
				
			||||
            </MudContainer> | 
				
			||||
        } | 
				
			||||
</MudContainer> | 
				
			||||
 | 
				
			||||
@code { | 
				
			||||
    private string _query; | 
				
			||||
    private MovieCardResultsViewModel _movies; | 
				
			||||
    private TelevisionShowCardResultsViewModel _shows; | 
				
			||||
    private TelevisionSeasonCardResultsViewModel _seasons; | 
				
			||||
    private TelevisionEpisodeCardResultsViewModel _episodes; | 
				
			||||
    private MusicVideoCardResultsViewModel _musicVideos; | 
				
			||||
    private OtherVideoCardResultsViewModel _otherVideos; | 
				
			||||
    private SongCardResultsViewModel _songs; | 
				
			||||
    private ArtistCardResultsViewModel _artists; | 
				
			||||
 | 
				
			||||
    protected override Task OnInitializedAsync() => RefreshData(); | 
				
			||||
 | 
				
			||||
    protected override async Task RefreshData() | 
				
			||||
    { | 
				
			||||
        _query = "state:FileNotFound"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            _movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50)); | 
				
			||||
            _shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50)); | 
				
			||||
            _seasons = await Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50)); | 
				
			||||
            _episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50)); | 
				
			||||
            _musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50)); | 
				
			||||
            _otherVideos = await Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50)); | 
				
			||||
            _songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50)); | 
				
			||||
            _artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50)); | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) | 
				
			||||
    { | 
				
			||||
        List<MediaCardViewModel> GetSortedItems() | 
				
			||||
        { | 
				
			||||
            return _movies.Cards.OrderBy(m => m.SortTitle) | 
				
			||||
                .Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
                .Append(_seasons.Cards.OrderBy(s => s.SortTitle)) | 
				
			||||
                .Append(_episodes.Cards.OrderBy(ep => ep.SortTitle)) | 
				
			||||
                .Append(_artists.Cards.OrderBy(a => a.SortTitle)) | 
				
			||||
                .Append(_musicVideos.Cards.OrderBy(mv => mv.SortTitle)) | 
				
			||||
                .Append(_otherVideos.Cards.OrderBy(ov => ov.SortTitle)) | 
				
			||||
                .Append(_songs.Cards.OrderBy(ov => ov.SortTitle)) | 
				
			||||
                .ToList(); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        SelectClicked(GetSortedItems, card, e); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private string GetMoviesLink() | 
				
			||||
    { | 
				
			||||
        var uri = "/media/movies/page/1"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            (string key, string value) = _query.EncodeQuery(); | 
				
			||||
            uri = $"{uri}?{key}={value}"; | 
				
			||||
        } | 
				
			||||
        return uri; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private string GetShowsLink() | 
				
			||||
    { | 
				
			||||
        var uri = "/media/tv/shows/page/1"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            (string key, string value) = _query.EncodeQuery(); | 
				
			||||
            uri = $"{uri}?{key}={value}"; | 
				
			||||
        } | 
				
			||||
        return uri; | 
				
			||||
    } | 
				
			||||
     | 
				
			||||
    private string GetSeasonsLink() | 
				
			||||
    { | 
				
			||||
        var uri = "/media/tv/seasons/page/1"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            (string key, string value) = _query.EncodeQuery(); | 
				
			||||
            uri = $"{uri}?{key}={value}"; | 
				
			||||
        } | 
				
			||||
        return uri; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private string GetEpisodesLink() | 
				
			||||
    { | 
				
			||||
        var uri = "/media/tv/episodes/page/1"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            (string key, string value) = _query.EncodeQuery(); | 
				
			||||
            uri = $"{uri}?{key}={value}"; | 
				
			||||
        } | 
				
			||||
        return uri; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private string GetArtistsLink() | 
				
			||||
    { | 
				
			||||
        var uri = "/media/music/artists/page/1"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            (string key, string value) = _query.EncodeQuery(); | 
				
			||||
            uri = $"{uri}?{key}={value}"; | 
				
			||||
        } | 
				
			||||
        return uri; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private string GetMusicVideosLink() | 
				
			||||
    { | 
				
			||||
        var uri = "/media/music/videos/page/1"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            (string key, string value) = _query.EncodeQuery(); | 
				
			||||
            uri = $"{uri}?{key}={value}"; | 
				
			||||
        } | 
				
			||||
        return uri; | 
				
			||||
    } | 
				
			||||
     | 
				
			||||
    private string GetOtherVideosLink() | 
				
			||||
    { | 
				
			||||
        var uri = "/media/other/videos/page/1"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            (string key, string value) = _query.EncodeQuery(); | 
				
			||||
            uri = $"{uri}?{key}={value}"; | 
				
			||||
        } | 
				
			||||
        return uri; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private string GetSongsLink() | 
				
			||||
    { | 
				
			||||
        var uri = "/media/music/songs/page/1"; | 
				
			||||
        if (!string.IsNullOrWhiteSpace(_query)) | 
				
			||||
        { | 
				
			||||
            (string key, string value) = _query.EncodeQuery(); | 
				
			||||
            uri = $"{uri}?{key}={value}"; | 
				
			||||
        } | 
				
			||||
        return uri; | 
				
			||||
    } | 
				
			||||
     | 
				
			||||
    private Task DeleteFromDatabase() => DeleteItemsFromDatabase( | 
				
			||||
        SelectedItems.OfType<MovieCardViewModel>().Map(m => m.MovieId).ToList(), | 
				
			||||
        SelectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId).ToList(), | 
				
			||||
        SelectedItems.OfType<TelevisionSeasonCardViewModel>().Map(s => s.TelevisionSeasonId).ToList(), | 
				
			||||
        SelectedItems.OfType<TelevisionEpisodeCardViewModel>().Map(e => e.EpisodeId).ToList(), | 
				
			||||
        SelectedItems.OfType<ArtistCardViewModel>().Map(a => a.ArtistId).ToList(), | 
				
			||||
        SelectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList(), | 
				
			||||
        SelectedItems.OfType<OtherVideoCardViewModel>().Map(ov => ov.OtherVideoId).ToList(), | 
				
			||||
        SelectedItems.OfType<SongCardViewModel>().Map(s => s.SongId).ToList()); | 
				
			||||
 | 
				
			||||
    private async Task DeleteItemsFromDatabase( | 
				
			||||
        List<int> movieIds, | 
				
			||||
        List<int> showIds, | 
				
			||||
        List<int> seasonIds, | 
				
			||||
        List<int> episodeIds, | 
				
			||||
        List<int> artistIds, | 
				
			||||
        List<int> musicVideoIds, | 
				
			||||
        List<int> otherVideoIds, | 
				
			||||
        List<int> songIds, | 
				
			||||
        string entityName = "selected items") | 
				
			||||
    { | 
				
			||||
        int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count + | 
				
			||||
                    musicVideoIds.Count + otherVideoIds.Count + songIds.Count; | 
				
			||||
 | 
				
			||||
        var parameters = new DialogParameters | 
				
			||||
        { { "EntityType", count.ToString() }, { "EntityName", entityName } }; | 
				
			||||
        var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; | 
				
			||||
 | 
				
			||||
        IDialogReference dialog = Dialog.Show<DeleteFromDatabaseDialog>("Delete From Database", parameters, options); | 
				
			||||
        DialogResult result = await dialog.Result; | 
				
			||||
        if (!result.Cancelled) | 
				
			||||
        { | 
				
			||||
            var request = new DeleteItemsFromDatabase( | 
				
			||||
                movieIds.Append(showIds) | 
				
			||||
                    .Append(seasonIds) | 
				
			||||
                    .Append(episodeIds) | 
				
			||||
                    .Append(artistIds) | 
				
			||||
                    .Append(musicVideoIds) | 
				
			||||
                    .Append(otherVideoIds) | 
				
			||||
                    .Append(songIds) | 
				
			||||
                    .ToList()); | 
				
			||||
 | 
				
			||||
            Either<BaseError, Unit> addResult = await Mediator.Send(request); | 
				
			||||
            await addResult.Match( | 
				
			||||
                Left: error => | 
				
			||||
                { | 
				
			||||
                    Snackbar.Add($"Unexpected error deleting items from database: {error.Value}"); | 
				
			||||
                    Logger.LogError("Unexpected error deleting items from database: {Error}", error.Value); | 
				
			||||
                    return Task.CompletedTask; | 
				
			||||
                }, | 
				
			||||
                Right: async _ => | 
				
			||||
                { | 
				
			||||
                    Snackbar.Add($"Deleted {count} items from the database", Severity.Success); | 
				
			||||
                    ClearSelection(); | 
				
			||||
                    await RefreshData(); | 
				
			||||
                }); | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private async Task DeleteItemFromDatabase(MediaCardViewModel vm) | 
				
			||||
    { | 
				
			||||
        DeleteItemsFromDatabase request; | 
				
			||||
         | 
				
			||||
        switch (vm) | 
				
			||||
        { | 
				
			||||
            case MovieCardViewModel movie: | 
				
			||||
                request = new DeleteItemsFromDatabase(new List<int> { movie.MovieId }); | 
				
			||||
                await DeleteItemsWithConfirmation("movie", $"{movie.Title} ({movie.Subtitle})", request); | 
				
			||||
                break; | 
				
			||||
            case TelevisionShowCardViewModel show: | 
				
			||||
                request = new DeleteItemsFromDatabase(new List<int> { show.TelevisionShowId }); | 
				
			||||
                await DeleteItemsWithConfirmation("show", $"{show.Title} ({show.Subtitle})", request); | 
				
			||||
                break; | 
				
			||||
            case TelevisionSeasonCardViewModel season: | 
				
			||||
                request = new DeleteItemsFromDatabase(new List<int> { season.TelevisionSeasonId }); | 
				
			||||
                await DeleteItemsWithConfirmation("season", $"{season.Title} ({season.Subtitle})", request); | 
				
			||||
                break; | 
				
			||||
            case TelevisionEpisodeCardViewModel episode: | 
				
			||||
                request = new DeleteItemsFromDatabase(new List<int> { episode.EpisodeId }); | 
				
			||||
                await DeleteItemsWithConfirmation("episode", $"{episode.Title} ({episode.Subtitle})", request); | 
				
			||||
                break; | 
				
			||||
            case ArtistCardViewModel artist: | 
				
			||||
                request = new DeleteItemsFromDatabase(new List<int> { artist.ArtistId }); | 
				
			||||
                await DeleteItemsWithConfirmation("artist", $"{artist.Title} ({artist.Subtitle})", request); | 
				
			||||
                break; | 
				
			||||
            case MusicVideoCardViewModel musicVideo: | 
				
			||||
                request = new DeleteItemsFromDatabase(new List<int> { musicVideo.MusicVideoId }); | 
				
			||||
                await DeleteItemsWithConfirmation("music video", $"{musicVideo.Title} ({musicVideo.Subtitle})", request); | 
				
			||||
                break; | 
				
			||||
            case OtherVideoCardViewModel otherVideo: | 
				
			||||
                request = new DeleteItemsFromDatabase(new List<int> { otherVideo.OtherVideoId }); | 
				
			||||
                await DeleteItemsWithConfirmation("other video", $"{otherVideo.Title} ({otherVideo.Subtitle})", request); | 
				
			||||
                break; | 
				
			||||
            case SongCardViewModel song: | 
				
			||||
                request = new DeleteItemsFromDatabase(new List<int> { song.SongId }); | 
				
			||||
                await DeleteItemsWithConfirmation("song", $"{song.Title} ({song.Subtitle})", request); | 
				
			||||
                break; | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    private async Task DeleteItemsWithConfirmation( | 
				
			||||
        string entityType, | 
				
			||||
        string entityName, | 
				
			||||
        DeleteItemsFromDatabase request) | 
				
			||||
    { | 
				
			||||
        var parameters = new DialogParameters { { "EntityType", entityType }, { "EntityName", entityName } }; | 
				
			||||
        var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; | 
				
			||||
 | 
				
			||||
        IDialogReference dialog = Dialog.Show<DeleteFromDatabaseDialog>("Delete From Database", parameters, options); | 
				
			||||
        DialogResult result = await dialog.Result; | 
				
			||||
        if (!result.Cancelled) | 
				
			||||
        { | 
				
			||||
            await Mediator.Send(request); | 
				
			||||
            await RefreshData(); | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -0,0 +1,53 @@
				@@ -0,0 +1,53 @@
					 | 
				
			||||
@inject IMediator _mediator | 
				
			||||
 | 
				
			||||
<div @onkeydown="@OnKeyDown"> | 
				
			||||
    <MudDialog> | 
				
			||||
        <DialogContent> | 
				
			||||
            <MudContainer Class="mb-6"> | 
				
			||||
                <MudHighlighter Class="mud-primary-text" | 
				
			||||
                                Style="background-color: transparent; font-weight: bold" | 
				
			||||
                                Text="@FormatText()" | 
				
			||||
                                HighlightedText="@EntityName"/> | 
				
			||||
            </MudContainer> | 
				
			||||
        </DialogContent> | 
				
			||||
        <DialogActions> | 
				
			||||
            <MudButton OnClick="Cancel">Cancel</MudButton> | 
				
			||||
            <MudButton Color="Color.Error" Variant="Variant.Filled" OnClick="Submit"> | 
				
			||||
                Delete From Database | 
				
			||||
            </MudButton> | 
				
			||||
        </DialogActions> | 
				
			||||
    </MudDialog> | 
				
			||||
</div> | 
				
			||||
 | 
				
			||||
@code { | 
				
			||||
 | 
				
			||||
    [CascadingParameter] | 
				
			||||
    MudDialogInstance MudDialog { get; set; } | 
				
			||||
 | 
				
			||||
    [Parameter] | 
				
			||||
    public string EntityType { get; set; } | 
				
			||||
 | 
				
			||||
    [Parameter] | 
				
			||||
    public string EntityName { get; set; } | 
				
			||||
 | 
				
			||||
    [Parameter] | 
				
			||||
    public string DetailText { get; set; } | 
				
			||||
 | 
				
			||||
    [Parameter] | 
				
			||||
    public string DetailHighlight { get; set; } | 
				
			||||
 | 
				
			||||
    private string FormatText() => $"Do you really want to delete the {EntityType} {EntityName} from the database?"; | 
				
			||||
 | 
				
			||||
    private void Submit() => MudDialog.Close(DialogResult.Ok(true)); | 
				
			||||
 | 
				
			||||
    private void Cancel() => MudDialog.Cancel(); | 
				
			||||
 | 
				
			||||
    private void OnKeyDown(KeyboardEventArgs e) | 
				
			||||
    { | 
				
			||||
        if (e.Code is "Enter" or "NumpadEnter") | 
				
			||||
        { | 
				
			||||
            Submit(); | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue