mirror of https://github.com/ErsatzTV/ErsatzTV.git
46 changed files with 2117 additions and 677 deletions
@ -0,0 +1,9 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards.Queries |
||||||
|
{ |
||||||
|
public record GetSimpleMediaCollectionCards |
||||||
|
(int Id) : IRequest<Either<BaseError, SimpleMediaCollectionCardResultsViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.MediaCards.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards.Queries |
||||||
|
{ |
||||||
|
public class GetSimpleMediaCollectionCardsHandler : IRequestHandler<GetSimpleMediaCollectionCards, |
||||||
|
Either<BaseError, SimpleMediaCollectionCardResultsViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public GetSimpleMediaCollectionCardsHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public async Task<Either<BaseError, SimpleMediaCollectionCardResultsViewModel>> Handle( |
||||||
|
GetSimpleMediaCollectionCards request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
(await _mediaCollectionRepository.GetSimpleMediaCollectionWithItemsUntracked(request.Id)) |
||||||
|
.ToEither(BaseError.New("Unable to load collection")) |
||||||
|
.Map(ProjectToViewModel); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards |
||||||
|
{ |
||||||
|
public record SimpleMediaCollectionCardResultsViewModel( |
||||||
|
string Name, |
||||||
|
List<MovieCardViewModel> MovieCards, |
||||||
|
List<TelevisionShowCardViewModel> ShowCards, |
||||||
|
List<TelevisionSeasonCardViewModel> SeasonCards, |
||||||
|
List<TelevisionEpisodeCardViewModel> EpisodeCards); |
||||||
|
} |
||||||
@ -1,11 +1,17 @@ |
|||||||
namespace ErsatzTV.Application.MediaCards |
namespace ErsatzTV.Application.MediaCards |
||||||
{ |
{ |
||||||
public record TelevisionSeasonCardViewModel |
public record TelevisionSeasonCardViewModel |
||||||
(int TelevisionSeasonId, string Title, string Subtitle, string SortTitle, string Poster, string Placeholder) : MediaCardViewModel( |
( |
||||||
Title, |
int TelevisionSeasonId, |
||||||
Subtitle, |
string Title, |
||||||
SortTitle, |
string Subtitle, |
||||||
Poster) |
string SortTitle, |
||||||
|
string Poster, |
||||||
|
string Placeholder) : MediaCardViewModel( |
||||||
|
Title, |
||||||
|
Subtitle, |
||||||
|
SortTitle, |
||||||
|
Poster) |
||||||
{ |
{ |
||||||
} |
} |
||||||
} |
} |
||||||
|
|||||||
@ -1,9 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record AddItemsToSimpleMediaCollection |
|
||||||
(int MediaCollectionId, List<int> ItemIds) : MediatR.IRequest<Either<BaseError, Unit>>; |
|
||||||
} |
|
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record AddMoviesToSimpleMediaCollection |
||||||
|
(int MediaCollectionId, List<int> MovieIds) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record AddTelevisionShowToSimpleMediaCollection |
||||||
|
(int MediaCollectionId, int TelevisionShowId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
||||||
@ -0,0 +1,67 @@ |
|||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
AddTelevisionShowToSimpleMediaCollectionHandler : MediatR.IRequestHandler< |
||||||
|
AddTelevisionShowToSimpleMediaCollection, |
||||||
|
Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
private readonly ITelevisionRepository _televisionRepository; |
||||||
|
|
||||||
|
public AddTelevisionShowToSimpleMediaCollectionHandler( |
||||||
|
IMediaCollectionRepository mediaCollectionRepository, |
||||||
|
ITelevisionRepository televisionRepository) |
||||||
|
{ |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
_televisionRepository = televisionRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
AddTelevisionShowToSimpleMediaCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(ApplyAddTelevisionShowRequest) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<Unit> ApplyAddTelevisionShowRequest(RequestParameters parameters) |
||||||
|
{ |
||||||
|
if (parameters.Collection.TelevisionShows.All(s => s.Id != parameters.ShowToAdd.Id)) |
||||||
|
{ |
||||||
|
parameters.Collection.TelevisionShows.Add(parameters.ShowToAdd); |
||||||
|
await _mediaCollectionRepository.Update(parameters.Collection); |
||||||
|
} |
||||||
|
|
||||||
|
return Unit.Default; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RequestParameters>> |
||||||
|
Validate(AddTelevisionShowToSimpleMediaCollection request) => |
||||||
|
(await SimpleMediaCollectionMustExist(request), await ValidateShow(request)) |
||||||
|
.Apply( |
||||||
|
(simpleMediaCollectionToUpdate, show) => |
||||||
|
new RequestParameters(simpleMediaCollectionToUpdate, show)); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist( |
||||||
|
AddTelevisionShowToSimpleMediaCollection updateSimpleMediaCollection) => |
||||||
|
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) |
||||||
|
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, TelevisionShow>> ValidateShow( |
||||||
|
AddTelevisionShowToSimpleMediaCollection request) => |
||||||
|
LoadTelevisionShow(request) |
||||||
|
.Map(v => v.ToValidation<BaseError>("TelevisionShow does not exist")); |
||||||
|
|
||||||
|
private Task<Option<TelevisionShow>> LoadTelevisionShow(AddTelevisionShowToSimpleMediaCollection request) => |
||||||
|
_televisionRepository.GetShow(request.TelevisionShowId); |
||||||
|
|
||||||
|
private record RequestParameters(SimpleMediaCollection Collection, TelevisionShow ShowToAdd); |
||||||
|
} |
||||||
|
} |
||||||
@ -1,11 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using ErsatzTV.Application.MediaItems; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public record ReplaceSimpleMediaCollectionItems |
|
||||||
(int MediaCollectionId, List<int> MediaItemIds) : IRequest<Either<BaseError, List<MediaItemViewModel>>>; |
|
||||||
} |
|
||||||
@ -1,65 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using System.Linq; |
|
||||||
using System.Threading; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Application.MediaItems; |
|
||||||
using ErsatzTV.Core; |
|
||||||
using ErsatzTV.Core.Domain; |
|
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
|
||||||
using LanguageExt; |
|
||||||
using LanguageExt.UnsafeValueAccess; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands |
|
||||||
{ |
|
||||||
public class ReplaceSimpleMediaCollectionItemsHandler : IRequestHandler<ReplaceSimpleMediaCollectionItems, |
|
||||||
Either<BaseError, List<MediaItemViewModel>>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
private readonly IMediaItemRepository _mediaItemRepository; |
|
||||||
|
|
||||||
public ReplaceSimpleMediaCollectionItemsHandler( |
|
||||||
IMediaCollectionRepository mediaCollectionRepository, |
|
||||||
IMediaItemRepository mediaItemRepository) |
|
||||||
{ |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
_mediaItemRepository = mediaItemRepository; |
|
||||||
} |
|
||||||
|
|
||||||
public Task<Either<BaseError, List<MediaItemViewModel>>> Handle( |
|
||||||
ReplaceSimpleMediaCollectionItems request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
Validate(request) |
|
||||||
.MapT(mediaItems => PersistItems(request, mediaItems)) |
|
||||||
.Bind(v => v.ToEitherAsync()); |
|
||||||
|
|
||||||
private async Task<List<MediaItemViewModel>> PersistItems( |
|
||||||
ReplaceSimpleMediaCollectionItems request, |
|
||||||
List<MediaItem> mediaItems) |
|
||||||
{ |
|
||||||
await _mediaCollectionRepository.ReplaceItems(request.MediaCollectionId, mediaItems); |
|
||||||
return mediaItems.Map(MediaItems.Mapper.ProjectToViewModel).ToList(); |
|
||||||
} |
|
||||||
|
|
||||||
private Task<Validation<BaseError, List<MediaItem>>> Validate(ReplaceSimpleMediaCollectionItems request) => |
|
||||||
MediaCollectionMustExist(request).BindT(_ => MediaItemsMustExist(request)); |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, SimpleMediaCollection>> MediaCollectionMustExist( |
|
||||||
ReplaceSimpleMediaCollectionItems request) => |
|
||||||
(await _mediaCollectionRepository.GetSimpleMediaCollection(request.MediaCollectionId)) |
|
||||||
.ToValidation<BaseError>("[MediaCollectionId] does not exist."); |
|
||||||
|
|
||||||
private async Task<Validation<BaseError, List<MediaItem>>> MediaItemsMustExist( |
|
||||||
ReplaceSimpleMediaCollectionItems replaceItems) |
|
||||||
{ |
|
||||||
var allMediaItems = (await replaceItems.MediaItemIds.Map(i => _mediaItemRepository.Get(i)).Sequence()) |
|
||||||
.ToList(); |
|
||||||
if (allMediaItems.Any(x => x.IsNone)) |
|
||||||
{ |
|
||||||
return BaseError.New("[MediaItemId] does not exist"); |
|
||||||
} |
|
||||||
|
|
||||||
return allMediaItems.Sequence().ValueUnsafe().ToList(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,4 +1,10 @@ |
|||||||
namespace ErsatzTV.Application.MediaCollections |
using ErsatzTV.Application.MediaCards; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections |
||||||
{ |
{ |
||||||
public record MediaCollectionViewModel(int Id, string Name); |
public record MediaCollectionViewModel(int Id, string Name) : MediaCardViewModel( |
||||||
|
Name, |
||||||
|
string.Empty, |
||||||
|
Name, |
||||||
|
string.Empty); |
||||||
} |
} |
||||||
|
|||||||
@ -1,7 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Queries |
|
||||||
{ |
|
||||||
public record GetMediaCollectionSummaries(string SearchString) : IRequest<List<MediaCollectionSummaryViewModel>>; |
|
||||||
} |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using System.Linq; |
|
||||||
using System.Threading; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
using static ErsatzTV.Application.MediaCollections.Mapper; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Queries |
|
||||||
{ |
|
||||||
public class |
|
||||||
GetMediaCollectionSummariesHandler : IRequestHandler<GetMediaCollectionSummaries, |
|
||||||
List<MediaCollectionSummaryViewModel>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
|
|
||||||
public GetMediaCollectionSummariesHandler(IMediaCollectionRepository mediaCollectionRepository) => |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
|
|
||||||
public Task<List<MediaCollectionSummaryViewModel>> Handle( |
|
||||||
GetMediaCollectionSummaries request, |
|
||||||
CancellationToken cancellationToken) => |
|
||||||
_mediaCollectionRepository.GetSummaries(request.SearchString) |
|
||||||
.Map(list => list.Map(ProjectToViewModel).ToList()); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,11 +0,0 @@ |
|||||||
using System; |
|
||||||
using System.Collections.Generic; |
|
||||||
using ErsatzTV.Application.MediaItems; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Queries |
|
||||||
{ |
|
||||||
public record GetSimpleMediaCollectionWithItemsById |
|
||||||
(int Id) : IRequest<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>; |
|
||||||
} |
|
||||||
@ -1,37 +0,0 @@ |
|||||||
using System; |
|
||||||
using System.Collections.Generic; |
|
||||||
using System.Linq; |
|
||||||
using System.Threading; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Application.MediaItems; |
|
||||||
using ErsatzTV.Core.Domain; |
|
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
|
||||||
using LanguageExt; |
|
||||||
using MediatR; |
|
||||||
using static LanguageExt.Prelude; |
|
||||||
using static ErsatzTV.Application.MediaCollections.Mapper; |
|
||||||
using static ErsatzTV.Application.MediaItems.Mapper; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Queries |
|
||||||
{ |
|
||||||
public class GetSimpleMediaCollectionWithItemsByIdHandler : IRequestHandler<GetSimpleMediaCollectionWithItemsById, |
|
||||||
Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>> |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
|
|
||||||
public GetSimpleMediaCollectionWithItemsByIdHandler(IMediaCollectionRepository mediaCollectionRepository) => |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
|
|
||||||
public async Task<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>> Handle( |
|
||||||
GetSimpleMediaCollectionWithItemsById request, |
|
||||||
CancellationToken cancellationToken) |
|
||||||
{ |
|
||||||
Option<SimpleMediaCollection> maybeCollection = |
|
||||||
await _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(request.Id); |
|
||||||
|
|
||||||
return maybeCollection.Match<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>( |
|
||||||
c => Tuple(ProjectToViewModel(c), c.Items.Map(ProjectToSearchViewModel).ToList()), |
|
||||||
None); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,8 +1,11 @@ |
|||||||
namespace ErsatzTV.Core.Domain |
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Domain |
||||||
{ |
{ |
||||||
public class MovieMediaItem : MediaItem |
public class MovieMediaItem : MediaItem |
||||||
{ |
{ |
||||||
public int MetadataId { get; set; } |
public int MetadataId { get; set; } |
||||||
public MovieMetadata Metadata { get; set; } |
public MovieMetadata Metadata { get; set; } |
||||||
|
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; } |
||||||
} |
} |
||||||
} |
} |
||||||
|
|||||||
@ -1,9 +1,12 @@ |
|||||||
namespace ErsatzTV.Core.Domain |
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Domain |
||||||
{ |
{ |
||||||
public class TelevisionEpisodeMediaItem : MediaItem |
public class TelevisionEpisodeMediaItem : MediaItem |
||||||
{ |
{ |
||||||
public int SeasonId { get; set; } |
public int SeasonId { get; set; } |
||||||
public TelevisionSeason Season { get; set; } |
public TelevisionSeason Season { get; set; } |
||||||
public TelevisionEpisodeMetadata Metadata { get; set; } |
public TelevisionEpisodeMetadata Metadata { get; set; } |
||||||
|
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; } |
||||||
} |
} |
||||||
} |
} |
||||||
|
|||||||
@ -1,8 +0,0 @@ |
|||||||
namespace ErsatzTV.Core.Domain |
|
||||||
{ |
|
||||||
public class TelevisionMediaCollection : MediaCollection |
|
||||||
{ |
|
||||||
public string ShowTitle { get; set; } |
|
||||||
public int? SeasonNumber { get; set; } |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,63 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using System.Linq; |
|
||||||
using System.Threading.Tasks; |
|
||||||
using ErsatzTV.Core.Domain; |
|
||||||
using ErsatzTV.Core.Interfaces.Metadata; |
|
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
|
||||||
using LanguageExt; |
|
||||||
using static LanguageExt.Prelude; |
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Metadata |
|
||||||
{ |
|
||||||
public class SmartCollectionBuilder : ISmartCollectionBuilder |
|
||||||
{ |
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
|
||||||
|
|
||||||
public SmartCollectionBuilder(IMediaCollectionRepository mediaCollectionRepository) => |
|
||||||
_mediaCollectionRepository = mediaCollectionRepository; |
|
||||||
|
|
||||||
public async Task<bool> RefreshSmartCollections(TelevisionEpisodeMediaItem mediaItem) |
|
||||||
{ |
|
||||||
var results = new List<bool>(); |
|
||||||
|
|
||||||
foreach (TelevisionMediaCollection collection in GetTelevisionCollections(mediaItem)) |
|
||||||
{ |
|
||||||
results.Add(await _mediaCollectionRepository.InsertOrIgnore(collection)); |
|
||||||
} |
|
||||||
|
|
||||||
return results.Any(identity); |
|
||||||
} |
|
||||||
|
|
||||||
private IEnumerable<TelevisionMediaCollection> GetTelevisionCollections(TelevisionEpisodeMediaItem mediaItem) |
|
||||||
{ |
|
||||||
var televisionMediaItems = new List<TelevisionEpisodeMediaItem> { mediaItem }; |
|
||||||
|
|
||||||
IEnumerable<TelevisionMediaCollection> televisionShowCollections = televisionMediaItems |
|
||||||
.Map(c => c.Metadata.Title) |
|
||||||
.Distinct().Map( |
|
||||||
t => new TelevisionMediaCollection |
|
||||||
{ |
|
||||||
Name = $"{t} - All Seasons", |
|
||||||
ShowTitle = t, |
|
||||||
SeasonNumber = null |
|
||||||
}); |
|
||||||
|
|
||||||
IEnumerable<TelevisionMediaCollection> televisionShowSeasonCollections = televisionMediaItems |
|
||||||
.Map(c => new { c.Metadata.Title, c.Metadata.Season }).Distinct() |
|
||||||
.Map( |
|
||||||
ts => |
|
||||||
{ |
|
||||||
return Optional(ts.Season).Map( |
|
||||||
sn => new TelevisionMediaCollection |
|
||||||
{ |
|
||||||
Name = $"{ts.Title} - Season {sn:00}", |
|
||||||
ShowTitle = ts.Title, |
|
||||||
SeasonNumber = sn |
|
||||||
}); |
|
||||||
}) |
|
||||||
.Sequence().Flatten(); |
|
||||||
|
|
||||||
return Seq(televisionShowCollections, televisionShowSeasonCollections).Flatten(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,17 +0,0 @@ |
|||||||
using ErsatzTV.Core.Domain; |
|
||||||
using Microsoft.EntityFrameworkCore; |
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Data.Configurations |
|
||||||
{ |
|
||||||
public class TelevisionMediaCollectionConfiguration : IEntityTypeConfiguration<TelevisionMediaCollection> |
|
||||||
{ |
|
||||||
public void Configure(EntityTypeBuilder<TelevisionMediaCollection> builder) |
|
||||||
{ |
|
||||||
builder.ToTable("TelevisionMediaCollections"); |
|
||||||
|
|
||||||
builder.HasIndex(c => new { c.ShowTitle, c.SeasonNumber }) |
|
||||||
.IsUnique(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,212 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations |
||||||
|
{ |
||||||
|
public partial class CollectionsRework : Migration |
||||||
|
{ |
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropTable( |
||||||
|
"MediaItemSimpleMediaCollection"); |
||||||
|
|
||||||
|
migrationBuilder.DropTable( |
||||||
|
"TelevisionMediaCollections"); |
||||||
|
|
||||||
|
migrationBuilder.CreateTable( |
||||||
|
"SimpleMediaCollectionEpisodes", |
||||||
|
table => new |
||||||
|
{ |
||||||
|
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false), |
||||||
|
TelevisionEpisodesId = table.Column<int>("INTEGER", nullable: false) |
||||||
|
}, |
||||||
|
constraints: table => |
||||||
|
{ |
||||||
|
table.PrimaryKey( |
||||||
|
"PK_SimpleMediaCollectionEpisodes", |
||||||
|
x => new { x.SimpleMediaCollectionsId, x.TelevisionEpisodesId }); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_SimpleMediaCollectionEpisodes_SimpleMediaCollections_SimpleMediaCollectionsId", |
||||||
|
x => x.SimpleMediaCollectionsId, |
||||||
|
"SimpleMediaCollections", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_SimpleMediaCollectionEpisodes_TelevisionEpisodes_TelevisionEpisodesId", |
||||||
|
x => x.TelevisionEpisodesId, |
||||||
|
"TelevisionEpisodes", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
}); |
||||||
|
|
||||||
|
migrationBuilder.CreateTable( |
||||||
|
"SimpleMediaCollectionMovies", |
||||||
|
table => new |
||||||
|
{ |
||||||
|
MoviesId = table.Column<int>("INTEGER", nullable: false), |
||||||
|
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false) |
||||||
|
}, |
||||||
|
constraints: table => |
||||||
|
{ |
||||||
|
table.PrimaryKey( |
||||||
|
"PK_SimpleMediaCollectionMovies", |
||||||
|
x => new { x.MoviesId, x.SimpleMediaCollectionsId }); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_SimpleMediaCollectionMovies_Movies_MoviesId", |
||||||
|
x => x.MoviesId, |
||||||
|
"Movies", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_SimpleMediaCollectionMovies_SimpleMediaCollections_SimpleMediaCollectionsId", |
||||||
|
x => x.SimpleMediaCollectionsId, |
||||||
|
"SimpleMediaCollections", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
}); |
||||||
|
|
||||||
|
migrationBuilder.CreateTable( |
||||||
|
"SimpleMediaCollectionSeasons", |
||||||
|
table => new |
||||||
|
{ |
||||||
|
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false), |
||||||
|
TelevisionSeasonsId = table.Column<int>("INTEGER", nullable: false) |
||||||
|
}, |
||||||
|
constraints: table => |
||||||
|
{ |
||||||
|
table.PrimaryKey( |
||||||
|
"PK_SimpleMediaCollectionSeasons", |
||||||
|
x => new { x.SimpleMediaCollectionsId, x.TelevisionSeasonsId }); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_SimpleMediaCollectionSeasons_SimpleMediaCollections_SimpleMediaCollectionsId", |
||||||
|
x => x.SimpleMediaCollectionsId, |
||||||
|
"SimpleMediaCollections", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_SimpleMediaCollectionSeasons_TelevisionSeasons_TelevisionSeasonsId", |
||||||
|
x => x.TelevisionSeasonsId, |
||||||
|
"TelevisionSeasons", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
}); |
||||||
|
|
||||||
|
migrationBuilder.CreateTable( |
||||||
|
"SimpleMediaCollectionShows", |
||||||
|
table => new |
||||||
|
{ |
||||||
|
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false), |
||||||
|
TelevisionShowsId = table.Column<int>("INTEGER", nullable: false) |
||||||
|
}, |
||||||
|
constraints: table => |
||||||
|
{ |
||||||
|
table.PrimaryKey( |
||||||
|
"PK_SimpleMediaCollectionShows", |
||||||
|
x => new { x.SimpleMediaCollectionsId, x.TelevisionShowsId }); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_SimpleMediaCollectionShows_SimpleMediaCollections_SimpleMediaCollectionsId", |
||||||
|
x => x.SimpleMediaCollectionsId, |
||||||
|
"SimpleMediaCollections", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_SimpleMediaCollectionShows_TelevisionShows_TelevisionShowsId", |
||||||
|
x => x.TelevisionShowsId, |
||||||
|
"TelevisionShows", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
}); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
"IX_SimpleMediaCollectionEpisodes_TelevisionEpisodesId", |
||||||
|
"SimpleMediaCollectionEpisodes", |
||||||
|
"TelevisionEpisodesId"); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
"IX_SimpleMediaCollectionMovies_SimpleMediaCollectionsId", |
||||||
|
"SimpleMediaCollectionMovies", |
||||||
|
"SimpleMediaCollectionsId"); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
"IX_SimpleMediaCollectionSeasons_TelevisionSeasonsId", |
||||||
|
"SimpleMediaCollectionSeasons", |
||||||
|
"TelevisionSeasonsId"); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
"IX_SimpleMediaCollectionShows_TelevisionShowsId", |
||||||
|
"SimpleMediaCollectionShows", |
||||||
|
"TelevisionShowsId"); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropTable( |
||||||
|
"SimpleMediaCollectionEpisodes"); |
||||||
|
|
||||||
|
migrationBuilder.DropTable( |
||||||
|
"SimpleMediaCollectionMovies"); |
||||||
|
|
||||||
|
migrationBuilder.DropTable( |
||||||
|
"SimpleMediaCollectionSeasons"); |
||||||
|
|
||||||
|
migrationBuilder.DropTable( |
||||||
|
"SimpleMediaCollectionShows"); |
||||||
|
|
||||||
|
migrationBuilder.CreateTable( |
||||||
|
"MediaItemSimpleMediaCollection", |
||||||
|
table => new |
||||||
|
{ |
||||||
|
ItemsId = table.Column<int>("INTEGER", nullable: false), |
||||||
|
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false) |
||||||
|
}, |
||||||
|
constraints: table => |
||||||
|
{ |
||||||
|
table.PrimaryKey( |
||||||
|
"PK_MediaItemSimpleMediaCollection", |
||||||
|
x => new { x.ItemsId, x.SimpleMediaCollectionsId }); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_MediaItemSimpleMediaCollection_MediaItems_ItemsId", |
||||||
|
x => x.ItemsId, |
||||||
|
"MediaItems", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_MediaItemSimpleMediaCollection_SimpleMediaCollections_SimpleMediaCollectionsId", |
||||||
|
x => x.SimpleMediaCollectionsId, |
||||||
|
"SimpleMediaCollections", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
}); |
||||||
|
|
||||||
|
migrationBuilder.CreateTable( |
||||||
|
"TelevisionMediaCollections", |
||||||
|
table => new |
||||||
|
{ |
||||||
|
Id = table.Column<int>("INTEGER", nullable: false) |
||||||
|
.Annotation("Sqlite:Autoincrement", true), |
||||||
|
SeasonNumber = table.Column<int>("INTEGER", nullable: true), |
||||||
|
ShowTitle = table.Column<string>("TEXT", nullable: true) |
||||||
|
}, |
||||||
|
constraints: table => |
||||||
|
{ |
||||||
|
table.PrimaryKey("PK_TelevisionMediaCollections", x => x.Id); |
||||||
|
table.ForeignKey( |
||||||
|
"FK_TelevisionMediaCollections_MediaCollections_Id", |
||||||
|
x => x.Id, |
||||||
|
"MediaCollections", |
||||||
|
"Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
}); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
"IX_MediaItemSimpleMediaCollection_SimpleMediaCollectionsId", |
||||||
|
"MediaItemSimpleMediaCollection", |
||||||
|
"SimpleMediaCollectionsId"); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
"IX_TelevisionMediaCollections_ShowTitle_SeasonNumber", |
||||||
|
"TelevisionMediaCollections", |
||||||
|
new[] { "ShowTitle", "SeasonNumber" }, |
||||||
|
unique: true); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,66 @@ |
|||||||
|
@page "/media/collections/{Id:int}" |
||||||
|
@using ErsatzTV.Application.MediaCards |
||||||
|
@using ErsatzTV.Application.MediaCards.Queries |
||||||
|
@inject NavigationManager NavigationManager |
||||||
|
@inject IMediator Mediator |
||||||
|
@inject ILogger<MediaCollectionItems> Logger |
||||||
|
@inject ISnackbar Snackbar |
||||||
|
|
||||||
|
<div class="mb-6" style="display: flex; flex-direction: row;"> |
||||||
|
<MudText GutterBottom="true" Typo="Typo.h2">@_data.Name</MudText> |
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" |
||||||
|
Link="@($"/media/collections/{Id}/edit")" |
||||||
|
Style="margin-bottom: auto; margin-top: auto;"/> |
||||||
|
</div> |
||||||
|
|
||||||
|
@if (_data.MovieCards.Any()) |
||||||
|
{ |
||||||
|
<MudText GutterBottom="true" Typo="Typo.h4">Movies</MudText> |
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> |
||||||
|
@foreach (MovieCardViewModel card in _data.MovieCards) |
||||||
|
{ |
||||||
|
<MediaCard Data="@card" Link="@($"/media/movies/{card.MovieId}")"/> |
||||||
|
} |
||||||
|
</MudContainer> |
||||||
|
} |
||||||
|
|
||||||
|
@if (_data.ShowCards.Any()) |
||||||
|
{ |
||||||
|
<MudText GutterBottom="true" Typo="Typo.h4">Television Shows</MudText> |
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> |
||||||
|
@foreach (TelevisionShowCardViewModel card in _data.ShowCards) |
||||||
|
{ |
||||||
|
<MediaCard Data="@card" Link="@($"/media/tv/shows/{card.TelevisionShowId}")"/> |
||||||
|
} |
||||||
|
</MudContainer> |
||||||
|
} |
||||||
|
|
||||||
|
@* <MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid"> *@ |
||||||
|
@* @foreach (var card in _data.Cards) *@ |
||||||
|
@* { *@ |
||||||
|
@* <MediaCard Data="@card" CustomPlaceholder="@SeasonPlaceholder" Link="@($"/media/tv/shows/{ShowId}/seasons/{card.TelevisionSeasonId}")" DataRefreshed="@(() => RefreshData())"/> *@ |
||||||
|
@* } *@ |
||||||
|
@* </MudContainer> *@ |
||||||
|
|
||||||
|
@code { |
||||||
|
|
||||||
|
[Parameter] |
||||||
|
public int Id { get; set; } |
||||||
|
|
||||||
|
private SimpleMediaCollectionCardResultsViewModel _data; |
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync() => await RefreshData(); |
||||||
|
|
||||||
|
private async Task RefreshData() |
||||||
|
{ |
||||||
|
Either<BaseError, SimpleMediaCollectionCardResultsViewModel> maybeResult = |
||||||
|
await Mediator.Send(new GetSimpleMediaCollectionCards(Id)); |
||||||
|
|
||||||
|
maybeResult.Match( |
||||||
|
result => _data = result, |
||||||
|
error => NavigationManager.NavigateTo("404")); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -1,121 +0,0 @@ |
|||||||
@page "/media/collections/{Id:int}/items" |
|
||||||
@using ErsatzTV.Application.MediaCollections |
|
||||||
@using ErsatzTV.Application.MediaCollections.Commands |
|
||||||
@using ErsatzTV.Application.MediaCollections.Queries |
|
||||||
@using ErsatzTV.Application.MediaItems |
|
||||||
@using ErsatzTV.Application.MediaItems.Queries |
|
||||||
@using Unit = LanguageExt.Unit |
|
||||||
@inject NavigationManager NavigationManager |
|
||||||
@inject IMediator Mediator |
|
||||||
@inject ILogger<MediaCollectionItemsEditor> Logger |
|
||||||
@inject ISnackbar Snackbar |
|
||||||
|
|
||||||
<MudTable Hover="true" Items="_collectionItems"> |
|
||||||
<ToolBarContent> |
|
||||||
<MudText Typo="Typo.h6">@_mediaCollection.Name Media Items</MudText> |
|
||||||
</ToolBarContent> |
|
||||||
<HeaderContent> |
|
||||||
<MudTh>Source</MudTh> |
|
||||||
<MudTh>Type</MudTh> |
|
||||||
<MudTh>Title</MudTh> |
|
||||||
<MudTh>Duration</MudTh> |
|
||||||
</HeaderContent> |
|
||||||
<RowTemplate> |
|
||||||
<MudTd DataLabel="Source">@context.Source</MudTd> |
|
||||||
<MudTd DataLabel="Type">@context.MediaType</MudTd> |
|
||||||
<MudTd DataLabel="Title">@context.Title</MudTd> |
|
||||||
<MudTd DataLabel="Duration">@context.Duration</MudTd> |
|
||||||
</RowTemplate> |
|
||||||
<PagerContent> |
|
||||||
@if (_collectionItems.Any()) |
|
||||||
{ |
|
||||||
<MudTablePager/> |
|
||||||
} |
|
||||||
</PagerContent> |
|
||||||
</MudTable> |
|
||||||
|
|
||||||
<MudTable @ref="_table" Hover="true" ServerData="@(new Func<TableState, Task<TableData<MediaItemSearchResultViewModel>>>(ServerReload))" Class="mt-8"> |
|
||||||
<ToolBarContent> |
|
||||||
<MudText Typo="Typo.h6">All Media Items</MudText> |
|
||||||
<MudToolBarSpacer/> |
|
||||||
<MudTextField T="string" ValueChanged="@OnSearch" Placeholder="Search" Adornment="Adornment.Start" |
|
||||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"> |
|
||||||
</MudTextField> |
|
||||||
</ToolBarContent> |
|
||||||
<HeaderContent> |
|
||||||
<MudTh>Source</MudTh> |
|
||||||
<MudTh>Type</MudTh> |
|
||||||
<MudTh>Title</MudTh> |
|
||||||
<MudTh>Duration</MudTh> |
|
||||||
</HeaderContent> |
|
||||||
<RowTemplate> |
|
||||||
<MudTd DataLabel="Source">@context.Source</MudTd> |
|
||||||
<MudTd DataLabel="Type">@context.MediaType</MudTd> |
|
||||||
<MudTd DataLabel="Title">@context.Title</MudTd> |
|
||||||
<MudTd DataLabel="Duration">@context.Duration</MudTd> |
|
||||||
</RowTemplate> |
|
||||||
<PagerContent> |
|
||||||
<MudTablePager/> |
|
||||||
</PagerContent> |
|
||||||
</MudTable> |
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(AddResultsAsync)" Class="mt-4"> |
|
||||||
Add Results |
|
||||||
</MudButton> |
|
||||||
|
|
||||||
@code { |
|
||||||
|
|
||||||
[Parameter] |
|
||||||
public int Id { get; set; } |
|
||||||
|
|
||||||
private MediaCollectionViewModel _mediaCollection; |
|
||||||
private IEnumerable<MediaItemSearchResultViewModel> _collectionItems; |
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync() => await LoadMediaCollectionAsync(); |
|
||||||
|
|
||||||
private List<int> _mediaItemIds; |
|
||||||
private IEnumerable<MediaItemSearchResultViewModel> _pagedData; |
|
||||||
private MudTable<MediaItemSearchResultViewModel> _table; |
|
||||||
|
|
||||||
private int _totalItems; |
|
||||||
private string _searchString; |
|
||||||
|
|
||||||
private async Task<TableData<MediaItemSearchResultViewModel>> ServerReload(TableState state) |
|
||||||
{ |
|
||||||
List<MediaItemSearchResultViewModel> data = await Mediator.Send(new SearchAllMediaItems(_searchString)); |
|
||||||
|
|
||||||
_mediaItemIds = data.Map(c => c.Id).ToList(); |
|
||||||
_totalItems = data.Count; |
|
||||||
|
|
||||||
_pagedData = data.OrderBy(c => c.Id).Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray(); |
|
||||||
return new TableData<MediaItemSearchResultViewModel> { TotalItems = _totalItems, Items = _pagedData }; |
|
||||||
} |
|
||||||
|
|
||||||
private async Task OnSearch(string text) |
|
||||||
{ |
|
||||||
_searchString = text; |
|
||||||
await _table.ReloadServerData(); |
|
||||||
} |
|
||||||
|
|
||||||
private async Task AddResultsAsync() |
|
||||||
{ |
|
||||||
Either<BaseError, Unit> result = await Mediator.Send(new AddItemsToSimpleMediaCollection(Id, _mediaItemIds)); |
|
||||||
await result.Match( |
|
||||||
async _ => await LoadMediaCollectionAsync(), |
|
||||||
error => |
|
||||||
{ |
|
||||||
Snackbar.Add(error.Value, Severity.Error); |
|
||||||
Logger.LogError("Error adding items to media collection: {Error}", error.Value); |
|
||||||
return Task.CompletedTask; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
private async Task LoadMediaCollectionAsync() |
|
||||||
{ |
|
||||||
Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>> maybeResult = |
|
||||||
await Mediator.Send(new GetSimpleMediaCollectionWithItemsById(Id)); |
|
||||||
maybeResult.Match( |
|
||||||
result => (_mediaCollection, _collectionItems) = result, |
|
||||||
() => NavigationManager.NavigateTo("404")); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,58 @@ |
|||||||
|
@using ErsatzTV.Application.MediaCollections |
||||||
|
@using ErsatzTV.Application.MediaCollections.Queries |
||||||
|
@inject IMediator Mediator |
||||||
|
|
||||||
|
<MudDialog> |
||||||
|
<DialogContent> |
||||||
|
<MudContainer Class="mb-6"> |
||||||
|
<MudHighlighter Class="mud-primary-text" |
||||||
|
Style="background-color: transparent; font-weight: bold" |
||||||
|
Text="@FormatText()" |
||||||
|
HighlightedText="@EntityName"/> |
||||||
|
</MudContainer> |
||||||
|
<MudSelect Label="Collection" @bind-Value="_selectedCollection" For="@(() => _selectedCollection)" Class="mb-6 mx-4"> |
||||||
|
@foreach (MediaCollectionViewModel collection in _collections) |
||||||
|
{ |
||||||
|
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem> |
||||||
|
} |
||||||
|
</MudSelect> |
||||||
|
</DialogContent> |
||||||
|
<DialogActions> |
||||||
|
<MudButton OnClick="Cancel">Cancel</MudButton> |
||||||
|
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="@(_selectedCollection == null)" OnClick="Submit"> |
||||||
|
Add To Collection |
||||||
|
</MudButton> |
||||||
|
</DialogActions> |
||||||
|
</MudDialog> |
||||||
|
|
||||||
|
@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 List<MediaCollectionViewModel> _collections; |
||||||
|
|
||||||
|
private MediaCollectionViewModel _selectedCollection; |
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync() => |
||||||
|
_collections = await Mediator.Send(new GetAllSimpleMediaCollections()); |
||||||
|
|
||||||
|
private string FormatText() => $"Select the collection to add the {EntityType} {EntityName}"; |
||||||
|
|
||||||
|
private void Submit() => MudDialog.Close(DialogResult.Ok(_selectedCollection)); |
||||||
|
|
||||||
|
private void Cancel() => MudDialog.Cancel(); |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue