mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* start to add multi-collections * create multi collection with no items * edit multi collections * fix plex credentials threading issue * add playback order to multi collection items * group episodes outside of shuffled enumerator * move playback order onto each schedule item * fix multi collection grouping * update changelogpull/316/head
87 changed files with 10831 additions and 200 deletions
@ -0,0 +1,13 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record CreateMultiCollectionItem(int CollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder); |
||||||
|
|
||||||
|
public record CreateMultiCollection |
||||||
|
(string Name, List<CreateMultiCollectionItem> Items) : IRequest<Either<BaseError, MultiCollectionViewModel>>; |
||||||
|
} |
@ -0,0 +1,80 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class CreateMultiCollectionHandler : |
||||||
|
IRequestHandler<CreateMultiCollection, Either<BaseError, MultiCollectionViewModel>> |
||||||
|
{ |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
|
||||||
|
public CreateMultiCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
|
||||||
|
public async Task<Either<BaseError, MultiCollectionViewModel>> Handle( |
||||||
|
CreateMultiCollection request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
Validation<BaseError, MultiCollection> validation = await Validate(dbContext, request); |
||||||
|
return await validation.Apply(c => PersistCollection(dbContext, c)); |
||||||
|
} |
||||||
|
|
||||||
|
private static async Task<MultiCollectionViewModel> PersistCollection( |
||||||
|
TvContext dbContext, |
||||||
|
MultiCollection multiCollection) |
||||||
|
{ |
||||||
|
await dbContext.MultiCollections.AddAsync(multiCollection); |
||||||
|
await dbContext.SaveChangesAsync(); |
||||||
|
await dbContext.Entry(multiCollection) |
||||||
|
.Collection(c => c.MultiCollectionItems) |
||||||
|
.Query() |
||||||
|
.Include(i => i.Collection) |
||||||
|
.LoadAsync(); |
||||||
|
return ProjectToViewModel(multiCollection); |
||||||
|
} |
||||||
|
|
||||||
|
private static Task<Validation<BaseError, MultiCollection>> Validate( |
||||||
|
TvContext dbContext, |
||||||
|
CreateMultiCollection request) => |
||||||
|
ValidateName(dbContext, request).MapT( |
||||||
|
name => new MultiCollection |
||||||
|
{ |
||||||
|
Name = name, |
||||||
|
MultiCollectionItems = request.Items.Map(i => new MultiCollectionItem |
||||||
|
{ |
||||||
|
CollectionId = i.CollectionId, |
||||||
|
ScheduleAsGroup = i.ScheduleAsGroup, |
||||||
|
PlaybackOrder = i.PlaybackOrder |
||||||
|
}).ToList() |
||||||
|
}); |
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, string>> ValidateName( |
||||||
|
TvContext dbContext, |
||||||
|
CreateMultiCollection createMultiCollection) |
||||||
|
{ |
||||||
|
List<string> allNames = await dbContext.MultiCollections |
||||||
|
.Map(c => c.Name) |
||||||
|
.ToListAsync(); |
||||||
|
|
||||||
|
Validation<BaseError, string> result1 = createMultiCollection.NotEmpty(c => c.Name) |
||||||
|
.Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name)); |
||||||
|
|
||||||
|
var result2 = Optional(createMultiCollection.Name) |
||||||
|
.Filter(name => !allNames.Contains(name)) |
||||||
|
.ToValidation<BaseError>("MultiCollection name must be unique"); |
||||||
|
|
||||||
|
return (result1, result2).Apply((_, _) => createMultiCollection.Name); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record DeleteMultiCollection(int MultiCollectionId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using ErsatzTV.Infrastructure.Extensions; |
||||||
|
using LanguageExt; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class DeleteMultiCollectionHandler : MediatR.IRequestHandler<DeleteMultiCollection, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
|
||||||
|
public DeleteMultiCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle( |
||||||
|
DeleteMultiCollection request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
|
||||||
|
Validation<BaseError, MultiCollection> validation = await MultiCollectionMustExist(dbContext, request); |
||||||
|
return await validation.Apply(c => DoDeletion(dbContext, c)); |
||||||
|
} |
||||||
|
|
||||||
|
private static Task<Unit> DoDeletion(TvContext dbContext, MultiCollection multiCollection) |
||||||
|
{ |
||||||
|
dbContext.MultiCollections.Remove(multiCollection); |
||||||
|
return dbContext.SaveChangesAsync().ToUnit(); |
||||||
|
} |
||||||
|
|
||||||
|
private static Task<Validation<BaseError, MultiCollection>> MultiCollectionMustExist( |
||||||
|
TvContext dbContext, |
||||||
|
DeleteMultiCollection request) => |
||||||
|
dbContext.MultiCollections |
||||||
|
.SelectOneAsync(c => c.Id, c => c.Id == request.MultiCollectionId) |
||||||
|
.Map(o => o.ToValidation<BaseError>($"MultiCollection {request.MultiCollectionId} does not exist.")); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record UpdateMultiCollectionItem(int CollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder); |
||||||
|
|
||||||
|
public record UpdateMultiCollection |
||||||
|
( |
||||||
|
int MultiCollectionId, |
||||||
|
string Name, |
||||||
|
List<UpdateMultiCollectionItem> Items) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
@ -0,0 +1,127 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Channels; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Application.Playouts.Commands; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using ErsatzTV.Infrastructure.Extensions; |
||||||
|
using LanguageExt; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiCollection, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public UpdateMultiCollectionHandler( |
||||||
|
IDbContextFactory<TvContext> dbContextFactory, |
||||||
|
IMediaCollectionRepository mediaCollectionRepository, |
||||||
|
ChannelWriter<IBackgroundServiceRequest> channel) |
||||||
|
{ |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
_channel = channel; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle( |
||||||
|
UpdateMultiCollection request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
Validation<BaseError, MultiCollection> validation = await Validate(dbContext, request); |
||||||
|
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, MultiCollection c, UpdateMultiCollection request) |
||||||
|
{ |
||||||
|
c.Name = request.Name; |
||||||
|
|
||||||
|
// save name first so playouts don't get rebuild for a name change
|
||||||
|
await dbContext.SaveChangesAsync(); |
||||||
|
|
||||||
|
var toAdd = request.Items |
||||||
|
.Filter(i => c.MultiCollectionItems.All(i2 => i2.CollectionId != i.CollectionId)) |
||||||
|
.Map( |
||||||
|
i => new MultiCollectionItem |
||||||
|
{ |
||||||
|
CollectionId = i.CollectionId, |
||||||
|
MultiCollectionId = c.Id, |
||||||
|
ScheduleAsGroup = i.ScheduleAsGroup, |
||||||
|
PlaybackOrder = i.PlaybackOrder |
||||||
|
}) |
||||||
|
.ToList(); |
||||||
|
var toRemove = c.MultiCollectionItems |
||||||
|
.Filter(i => request.Items.All(i2 => i2.CollectionId != i.CollectionId)) |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
// remove items that are no longer present
|
||||||
|
c.MultiCollectionItems.RemoveAll(toRemove.Contains); |
||||||
|
|
||||||
|
// update existing items
|
||||||
|
foreach (MultiCollectionItem item in c.MultiCollectionItems) |
||||||
|
{ |
||||||
|
foreach (UpdateMultiCollectionItem incoming in request.Items.Filter( |
||||||
|
i => i.CollectionId == item.CollectionId)) |
||||||
|
{ |
||||||
|
item.ScheduleAsGroup = incoming.ScheduleAsGroup; |
||||||
|
item.PlaybackOrder = incoming.PlaybackOrder; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// add new items
|
||||||
|
c.MultiCollectionItems.AddRange(toAdd); |
||||||
|
|
||||||
|
// rebuild playouts
|
||||||
|
if (await dbContext.SaveChangesAsync() > 0) |
||||||
|
{ |
||||||
|
// rebuild all playouts that use this collection
|
||||||
|
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingMultiCollection( |
||||||
|
request.MultiCollectionId)) |
||||||
|
{ |
||||||
|
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return Unit.Default; |
||||||
|
} |
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, MultiCollection>> Validate( |
||||||
|
TvContext dbContext, |
||||||
|
UpdateMultiCollection request) => |
||||||
|
(await MultiCollectionMustExist(dbContext, request), await ValidateName(dbContext, request)) |
||||||
|
.Apply((collectionToUpdate, _) => collectionToUpdate); |
||||||
|
|
||||||
|
private static Task<Validation<BaseError, MultiCollection>> MultiCollectionMustExist( |
||||||
|
TvContext dbContext, |
||||||
|
UpdateMultiCollection updateCollection) => |
||||||
|
dbContext.MultiCollections |
||||||
|
.Include(mc => mc.MultiCollectionItems) |
||||||
|
.SelectOneAsync(c => c.Id, c => c.Id == updateCollection.MultiCollectionId) |
||||||
|
.Map(o => o.ToValidation<BaseError>("MultiCollection does not exist.")); |
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, string>> ValidateName(TvContext dbContext, UpdateMultiCollection updateMultiCollection) |
||||||
|
{ |
||||||
|
List<string> allNames = await dbContext.MultiCollections |
||||||
|
.Filter(mc => mc.Id != updateMultiCollection.MultiCollectionId) |
||||||
|
.Map(c => c.Name) |
||||||
|
.ToListAsync(); |
||||||
|
|
||||||
|
Validation<BaseError, string> result1 = updateMultiCollection.NotEmpty(c => c.Name) |
||||||
|
.Bind(_ => updateMultiCollection.NotLongerThan(50)(c => c.Name)); |
||||||
|
|
||||||
|
var result2 = Optional(updateMultiCollection.Name) |
||||||
|
.Filter(name => !allNames.Contains(name)) |
||||||
|
.ToValidation<BaseError>("MultiCollection name must be unique"); |
||||||
|
|
||||||
|
return (result1, result2).Apply((_, _) => updateMultiCollection.Name); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,10 +1,25 @@ |
|||||||
using ErsatzTV.Core.Domain; |
using System.Linq; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections |
namespace ErsatzTV.Application.MediaCollections |
||||||
{ |
{ |
||||||
internal static class Mapper |
internal static class Mapper |
||||||
{ |
{ |
||||||
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) => |
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) => |
||||||
new(collection.Id, collection.Name); |
new(collection.Id, collection.Name, collection.UseCustomPlaybackOrder); |
||||||
|
|
||||||
|
internal static MultiCollectionViewModel ProjectToViewModel(MultiCollection multiCollection) => |
||||||
|
new( |
||||||
|
multiCollection.Id, |
||||||
|
multiCollection.Name, |
||||||
|
Optional(multiCollection.MultiCollectionItems).Flatten().Map(ProjectToViewModel).ToList()); |
||||||
|
|
||||||
|
private static MultiCollectionItemViewModel ProjectToViewModel(MultiCollectionItem multiCollectionItem) => |
||||||
|
new( |
||||||
|
multiCollectionItem.MultiCollectionId, |
||||||
|
ProjectToViewModel(multiCollectionItem.Collection), |
||||||
|
multiCollectionItem.ScheduleAsGroup, |
||||||
|
multiCollectionItem.PlaybackOrder); |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,10 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections |
||||||
|
{ |
||||||
|
public record MultiCollectionItemViewModel( |
||||||
|
int MultiCollectionId, |
||||||
|
MediaCollectionViewModel Collection, |
||||||
|
bool ScheduleAsGroup, |
||||||
|
PlaybackOrder PlaybackOrder); |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections |
||||||
|
{ |
||||||
|
public record MultiCollectionViewModel(int Id, string Name, List<MultiCollectionItemViewModel> Items); |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections |
||||||
|
{ |
||||||
|
public record PagedMultiCollectionsViewModel(int TotalCount, List<MultiCollectionViewModel> Page); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetAllMultiCollections : IRequest<List<MultiCollectionViewModel>>; |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public class GetAllMultiCollectionsHandler : IRequestHandler<GetAllMultiCollections, List<MultiCollectionViewModel>> |
||||||
|
{ |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
|
||||||
|
public GetAllMultiCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
|
||||||
|
public async Task<List<MultiCollectionViewModel>> Handle( |
||||||
|
GetAllMultiCollections request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
return await dbContext.MultiCollections |
||||||
|
.ToListAsync(cancellationToken) |
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetMultiCollectionById(int Id) : IRequest<Option<MultiCollectionViewModel>>; |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using ErsatzTV.Infrastructure.Extensions; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public class GetMultiCollectionByIdHandler : IRequestHandler<GetMultiCollectionById, Option<MultiCollectionViewModel>> |
||||||
|
{ |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
|
||||||
|
public GetMultiCollectionByIdHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
|
||||||
|
public async Task<Option<MultiCollectionViewModel>> Handle( |
||||||
|
GetMultiCollectionById request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
return await dbContext.MultiCollections |
||||||
|
.Include(mc => mc.MultiCollectionItems) |
||||||
|
.ThenInclude(mc => mc.Collection) |
||||||
|
.SelectOneAsync(c => c.Id, c => c.Id == request.Id) |
||||||
|
.MapT(ProjectToViewModel); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetPagedMultiCollections(int PageNum, int PageSize) : IRequest<PagedMultiCollectionsViewModel>; |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Data; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using Dapper; |
||||||
|
using ErsatzTV.Infrastructure.Data; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public class GetPagedMultiCollectionsHandler : IRequestHandler<GetPagedMultiCollections, PagedMultiCollectionsViewModel> |
||||||
|
{ |
||||||
|
private readonly IDbConnection _dbConnection; |
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||||
|
|
||||||
|
public GetPagedMultiCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection) |
||||||
|
{ |
||||||
|
_dbContextFactory = dbContextFactory; |
||||||
|
_dbConnection = dbConnection; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<PagedMultiCollectionsViewModel> Handle( |
||||||
|
GetPagedMultiCollections request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
int count = await _dbConnection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM Collection"); |
||||||
|
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||||
|
List<MultiCollectionViewModel> page = await dbContext.MultiCollections.FromSqlRaw( |
||||||
|
@"SELECT * FROM MultiCollection
|
||||||
|
ORDER BY Name |
||||||
|
COLLATE NOCASE |
||||||
|
LIMIT {0} OFFSET {1}",
|
||||||
|
request.PageSize, |
||||||
|
request.PageNum * request.PageSize) |
||||||
|
.Include(mc => mc.MultiCollectionItems) |
||||||
|
.ThenInclude(i => i.Collection) |
||||||
|
.ToListAsync(cancellationToken) |
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList()); |
||||||
|
|
||||||
|
return new PagedMultiCollectionsViewModel(count, page); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,11 +1,8 @@ |
|||||||
using ErsatzTV.Core.Domain; |
namespace ErsatzTV.Application.ProgramSchedules |
||||||
|
|
||||||
namespace ErsatzTV.Application.ProgramSchedules |
|
||||||
{ |
{ |
||||||
public record ProgramScheduleViewModel( |
public record ProgramScheduleViewModel( |
||||||
int Id, |
int Id, |
||||||
string Name, |
string Name, |
||||||
PlaybackOrder MediaCollectionPlaybackOrder, |
|
||||||
bool KeepMultiPartEpisodesTogether, |
bool KeepMultiPartEpisodesTogether, |
||||||
bool TreatCollectionsAsShows); |
bool TreatCollectionsAsShows); |
||||||
} |
} |
||||||
|
@ -0,0 +1,12 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Domain |
||||||
|
{ |
||||||
|
public class MultiCollection |
||||||
|
{ |
||||||
|
public int Id { get; set; } |
||||||
|
public string Name { get; set; } |
||||||
|
public List<Collection> Collections { get; set; } |
||||||
|
public List<MultiCollectionItem> MultiCollectionItems { get; set; } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
namespace ErsatzTV.Core.Domain |
||||||
|
{ |
||||||
|
public class MultiCollectionItem |
||||||
|
{ |
||||||
|
public int MultiCollectionId { get; set; } |
||||||
|
public MultiCollection MultiCollection { get; set; } |
||||||
|
public int CollectionId { get; set; } |
||||||
|
public Collection Collection { get; set; } |
||||||
|
public bool ScheduleAsGroup { get; set; } |
||||||
|
public PlaybackOrder PlaybackOrder { get; set; } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Scheduling |
||||||
|
{ |
||||||
|
public record CollectionWithItems( |
||||||
|
int CollectionId, |
||||||
|
List<MediaItem> MediaItems, |
||||||
|
bool ScheduleAsGroup, |
||||||
|
PlaybackOrder PlaybackOrder, |
||||||
|
bool UseCustomOrder); |
||||||
|
} |
@ -1,9 +1,40 @@ |
|||||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||||
using System.Diagnostics; |
using System.Diagnostics; |
||||||
using ErsatzTV.Core.Domain; |
using ErsatzTV.Core.Domain; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
namespace ErsatzTV.Core.Scheduling |
namespace ErsatzTV.Core.Scheduling |
||||||
{ |
{ |
||||||
[DebuggerDisplay("{First}")] |
[DebuggerDisplay("{" + nameof(First) + "}")] |
||||||
public record GroupedMediaItem(MediaItem First, List<MediaItem> Additional); |
public class GroupedMediaItem |
||||||
|
{ |
||||||
|
public GroupedMediaItem() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public GroupedMediaItem(MediaItem first, List<MediaItem> additional) |
||||||
|
{ |
||||||
|
First = first; |
||||||
|
Additional = additional ?? new List<MediaItem>(); |
||||||
|
} |
||||||
|
|
||||||
|
public MediaItem First { get; set; } |
||||||
|
public List<MediaItem> Additional { get; set; } |
||||||
|
|
||||||
|
public static IList<MediaItem> FlattenGroups(GroupedMediaItem[] copy, int mediaItemCount) |
||||||
|
{ |
||||||
|
var result = new MediaItem[mediaItemCount]; |
||||||
|
var i = 0; |
||||||
|
foreach (GroupedMediaItem group in copy) |
||||||
|
{ |
||||||
|
result[i++] = group.First; |
||||||
|
foreach (MediaItem additional in Optional(group.Additional).Flatten()) |
||||||
|
{ |
||||||
|
result[i++] = additional; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,41 @@ |
|||||||
|
using System; |
||||||
|
using System.Linq; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Scheduling |
||||||
|
{ |
||||||
|
public class MultiCollectionGroup : GroupedMediaItem |
||||||
|
{ |
||||||
|
public MultiCollectionGroup(CollectionWithItems collectionWithItems) |
||||||
|
{ |
||||||
|
if (collectionWithItems.UseCustomOrder) |
||||||
|
{ |
||||||
|
if (collectionWithItems.MediaItems.Count > 0) |
||||||
|
{ |
||||||
|
First = collectionWithItems.MediaItems.Head(); |
||||||
|
Additional = collectionWithItems.MediaItems.Tail().ToList(); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
throw new InvalidOperationException("Collection has no items"); |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
switch (collectionWithItems.PlaybackOrder) |
||||||
|
{ |
||||||
|
case PlaybackOrder.Chronological: |
||||||
|
var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new ChronologicalMediaComparer()) |
||||||
|
.ToList(); |
||||||
|
First = sortedItems.Head(); |
||||||
|
Additional = sortedItems.Tail().ToList(); |
||||||
|
break; |
||||||
|
default: |
||||||
|
throw new NotSupportedException( |
||||||
|
$"Unsupported MultiCollection PlaybackOrder: {collectionWithItems.PlaybackOrder}"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Scheduling |
||||||
|
{ |
||||||
|
public class MultiCollectionGrouper |
||||||
|
{ |
||||||
|
public static List<GroupedMediaItem> GroupMediaItems(IList<CollectionWithItems> collections) |
||||||
|
{ |
||||||
|
var result = new List<GroupedMediaItem>(); |
||||||
|
|
||||||
|
foreach (CollectionWithItems collection in collections) |
||||||
|
{ |
||||||
|
if (collection.ScheduleAsGroup) |
||||||
|
{ |
||||||
|
result.Add(new MultiCollectionGroup(collection)); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
result.AddRange(collection.MediaItems.Map(i => new GroupedMediaItem { First = i })); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Data.Configurations |
||||||
|
{ |
||||||
|
public class MultiCollectionConfiguration : IEntityTypeConfiguration<MultiCollection> |
||||||
|
{ |
||||||
|
public void Configure(EntityTypeBuilder<MultiCollection> builder) |
||||||
|
{ |
||||||
|
builder.ToTable("MultiCollection"); |
||||||
|
|
||||||
|
builder.HasMany(m => m.Collections) |
||||||
|
.WithMany(m => m.MultiCollections) |
||||||
|
.UsingEntity<MultiCollectionItem>( |
||||||
|
j => j.HasOne(mci => mci.Collection) |
||||||
|
.WithMany(c => c.MultiCollectionItems) |
||||||
|
.HasForeignKey(mci => mci.CollectionId) |
||||||
|
.OnDelete(DeleteBehavior.Cascade), |
||||||
|
j => j.HasOne(mci => mci.MultiCollection) |
||||||
|
.WithMany(mc => mc.MultiCollectionItems) |
||||||
|
.HasForeignKey(mci => mci.MultiCollectionId) |
||||||
|
.OnDelete(DeleteBehavior.Cascade), |
||||||
|
j => j.HasKey(mci => new { mci.MultiCollectionId, mci.CollectionId })); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Data.Configurations |
||||||
|
{ |
||||||
|
public class MultiCollectionItemConfiguration : IEntityTypeConfiguration<MultiCollectionItem> |
||||||
|
{ |
||||||
|
public void Configure(EntityTypeBuilder<MultiCollectionItem> builder) => builder.ToTable("MultiCollectionItem"); |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,125 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations |
||||||
|
{ |
||||||
|
public partial class Add_MultiCollection : Migration |
||||||
|
{ |
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "MultiCollectionId", |
||||||
|
table: "ProgramScheduleItem", |
||||||
|
type: "INTEGER", |
||||||
|
nullable: true); |
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "MultiCollectionId", |
||||||
|
table: "PlayoutProgramScheduleAnchor", |
||||||
|
type: "INTEGER", |
||||||
|
nullable: true); |
||||||
|
|
||||||
|
migrationBuilder.CreateTable( |
||||||
|
name: "MultiCollection", |
||||||
|
columns: table => new |
||||||
|
{ |
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||||
|
.Annotation("Sqlite:Autoincrement", true), |
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: true) |
||||||
|
}, |
||||||
|
constraints: table => |
||||||
|
{ |
||||||
|
table.PrimaryKey("PK_MultiCollection", x => x.Id); |
||||||
|
}); |
||||||
|
|
||||||
|
migrationBuilder.CreateTable( |
||||||
|
name: "MultiCollectionItem", |
||||||
|
columns: table => new |
||||||
|
{ |
||||||
|
MultiCollectionId = table.Column<int>(type: "INTEGER", nullable: false), |
||||||
|
CollectionId = table.Column<int>(type: "INTEGER", nullable: false), |
||||||
|
ScheduleAsGroup = table.Column<bool>(type: "INTEGER", nullable: false), |
||||||
|
PlaybackOrder = table.Column<int>(type: "INTEGER", nullable: false) |
||||||
|
}, |
||||||
|
constraints: table => |
||||||
|
{ |
||||||
|
table.PrimaryKey("PK_MultiCollectionItem", x => new { x.MultiCollectionId, x.CollectionId }); |
||||||
|
table.ForeignKey( |
||||||
|
name: "FK_MultiCollectionItem_Collection_CollectionId", |
||||||
|
column: x => x.CollectionId, |
||||||
|
principalTable: "Collection", |
||||||
|
principalColumn: "Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
table.ForeignKey( |
||||||
|
name: "FK_MultiCollectionItem_MultiCollection_MultiCollectionId", |
||||||
|
column: x => x.MultiCollectionId, |
||||||
|
principalTable: "MultiCollection", |
||||||
|
principalColumn: "Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
}); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
name: "IX_ProgramScheduleItem_MultiCollectionId", |
||||||
|
table: "ProgramScheduleItem", |
||||||
|
column: "MultiCollectionId"); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
name: "IX_PlayoutProgramScheduleAnchor_MultiCollectionId", |
||||||
|
table: "PlayoutProgramScheduleAnchor", |
||||||
|
column: "MultiCollectionId"); |
||||||
|
|
||||||
|
migrationBuilder.CreateIndex( |
||||||
|
name: "IX_MultiCollectionItem_CollectionId", |
||||||
|
table: "MultiCollectionItem", |
||||||
|
column: "CollectionId"); |
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey( |
||||||
|
name: "FK_PlayoutProgramScheduleAnchor_MultiCollection_MultiCollectionId", |
||||||
|
table: "PlayoutProgramScheduleAnchor", |
||||||
|
column: "MultiCollectionId", |
||||||
|
principalTable: "MultiCollection", |
||||||
|
principalColumn: "Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey( |
||||||
|
name: "FK_ProgramScheduleItem_MultiCollection_MultiCollectionId", |
||||||
|
table: "ProgramScheduleItem", |
||||||
|
column: "MultiCollectionId", |
||||||
|
principalTable: "MultiCollection", |
||||||
|
principalColumn: "Id", |
||||||
|
onDelete: ReferentialAction.Cascade); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropForeignKey( |
||||||
|
name: "FK_PlayoutProgramScheduleAnchor_MultiCollection_MultiCollectionId", |
||||||
|
table: "PlayoutProgramScheduleAnchor"); |
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey( |
||||||
|
name: "FK_ProgramScheduleItem_MultiCollection_MultiCollectionId", |
||||||
|
table: "ProgramScheduleItem"); |
||||||
|
|
||||||
|
migrationBuilder.DropTable( |
||||||
|
name: "MultiCollectionItem"); |
||||||
|
|
||||||
|
migrationBuilder.DropTable( |
||||||
|
name: "MultiCollection"); |
||||||
|
|
||||||
|
migrationBuilder.DropIndex( |
||||||
|
name: "IX_ProgramScheduleItem_MultiCollectionId", |
||||||
|
table: "ProgramScheduleItem"); |
||||||
|
|
||||||
|
migrationBuilder.DropIndex( |
||||||
|
name: "IX_PlayoutProgramScheduleAnchor_MultiCollectionId", |
||||||
|
table: "PlayoutProgramScheduleAnchor"); |
||||||
|
|
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "MultiCollectionId", |
||||||
|
table: "ProgramScheduleItem"); |
||||||
|
|
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "MultiCollectionId", |
||||||
|
table: "PlayoutProgramScheduleAnchor"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations |
||||||
|
{ |
||||||
|
public partial class Add_ProgramScheduleItemPlaybackOrder : Migration |
||||||
|
{ |
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "PlaybackOrder", |
||||||
|
table: "ProgramScheduleItem", |
||||||
|
type: "INTEGER", |
||||||
|
nullable: false, |
||||||
|
defaultValue: 0); |
||||||
|
|
||||||
|
migrationBuilder.Sql( |
||||||
|
"UPDATE ProgramScheduleItem SET PlaybackOrder = (SELECT MediaCollectionPlaybackOrder FROM ProgramSchedule WHERE ProgramSchedule.Id = ProgramScheduleItem.ProgramScheduleId)"); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "PlaybackOrder", |
||||||
|
table: "ProgramScheduleItem"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations |
||||||
|
{ |
||||||
|
public partial class Remove_ProgramScheduleMediaCollectionPlaybackOrder : Migration |
||||||
|
{ |
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "MediaCollectionPlaybackOrder", |
||||||
|
table: "ProgramSchedule"); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "MediaCollectionPlaybackOrder", |
||||||
|
table: "ProgramSchedule", |
||||||
|
type: "INTEGER", |
||||||
|
nullable: false, |
||||||
|
defaultValue: 0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,171 @@ |
|||||||
|
@page "/media/multi-collections/{Id:int}/edit" |
||||||
|
@page "/media/multi-collections/add" |
||||||
|
@using ErsatzTV.Application.MediaCollections |
||||||
|
@using ErsatzTV.Application.MediaCollections.Commands |
||||||
|
@using ErsatzTV.Application.MediaCollections.Queries |
||||||
|
@inject IMediator _mediator |
||||||
|
@inject NavigationManager _navigationManager |
||||||
|
@inject ISnackbar _snackbar |
||||||
|
@inject ILogger<MultiCollectionEditor> _logger |
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> |
||||||
|
<div style="max-width: 400px;"> |
||||||
|
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Multi Collection" : "Add Multi Collection")</MudText> |
||||||
|
|
||||||
|
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> |
||||||
|
<FluentValidator/> |
||||||
|
<MudCard> |
||||||
|
<MudCardContent> |
||||||
|
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> |
||||||
|
<MudAutocomplete @ref="_collectionAutocomplete" |
||||||
|
Class="mt-4" |
||||||
|
T="MediaCollectionViewModel" |
||||||
|
Label="Collection" |
||||||
|
@bind-value="_selectedCollection" |
||||||
|
SearchFunc="@SearchCollections" |
||||||
|
ToStringFunc="@(c => c?.Name)"/> |
||||||
|
</MudCardContent> |
||||||
|
<MudCardActions> |
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="@(_ => AddCollection())" Class="ml-2"> |
||||||
|
Add Collection |
||||||
|
</MudButton> |
||||||
|
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="mr-2 ml-auto"> |
||||||
|
@(IsEdit ? "Save Changes" : "Add Multi Collection") |
||||||
|
</MudButton> |
||||||
|
</MudCardActions> |
||||||
|
</MudCard> |
||||||
|
</EditForm> |
||||||
|
</div> |
||||||
|
|
||||||
|
<MudTable Hover="true" Items="_model.Items.OrderBy(i => i.Collection.Name, StringComparer.CurrentCultureIgnoreCase)" Dense="true" Class="mt-6"> |
||||||
|
<ToolBarContent> |
||||||
|
<MudText Typo="Typo.h6">@_model.Name Items</MudText> |
||||||
|
</ToolBarContent> |
||||||
|
<ColGroup> |
||||||
|
<col/> |
||||||
|
<col style="width: 20%"/> |
||||||
|
<col style="width: 30%"/> |
||||||
|
<col style="width: 60px;"/> |
||||||
|
</ColGroup> |
||||||
|
<HeaderContent> |
||||||
|
<MudTh>Collection</MudTh> |
||||||
|
<MudTh>Schedule As Group</MudTh> |
||||||
|
<MudTh>Playback Order</MudTh> |
||||||
|
<MudTh/> |
||||||
|
</HeaderContent> |
||||||
|
<RowTemplate> |
||||||
|
<MudTd DataLabel="Collection"> |
||||||
|
<MudText Typo="Typo.body2"> |
||||||
|
@context.Collection.Name |
||||||
|
</MudText> |
||||||
|
</MudTd> |
||||||
|
<MudTd DataLabel="Schedule As Group"> |
||||||
|
<MudCheckBox @bind-Checked="@context.ScheduleAsGroup" For="@(() => context.ScheduleAsGroup)"/> |
||||||
|
</MudTd> |
||||||
|
<MudTd DataLabel="Playback Order"> |
||||||
|
@if (context.ScheduleAsGroup) |
||||||
|
{ |
||||||
|
<MudText Typo="Typo.body2"> |
||||||
|
@(context.Collection.UseCustomPlaybackOrder ? "Custom" : "Chronological") |
||||||
|
</MudText> |
||||||
|
} |
||||||
|
</MudTd> |
||||||
|
<MudTd> |
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" |
||||||
|
OnClick="@(_ => RemoveCollection(context))"> |
||||||
|
</MudIconButton> |
||||||
|
</MudTd> |
||||||
|
</RowTemplate> |
||||||
|
</MudTable> |
||||||
|
</MudContainer> |
||||||
|
|
||||||
|
@code { |
||||||
|
[Parameter] |
||||||
|
public int Id { get; set; } |
||||||
|
|
||||||
|
private readonly MultiCollectionEditViewModel _model = new(); |
||||||
|
private EditContext _editContext; |
||||||
|
private ValidationMessageStore _messageStore; |
||||||
|
private List<MediaCollectionViewModel> _collections; |
||||||
|
private MediaCollectionViewModel _selectedCollection; |
||||||
|
private MudAutocomplete<MediaCollectionViewModel> _collectionAutocomplete; |
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync() |
||||||
|
{ |
||||||
|
_collections = await _mediator.Send(new GetAllCollections()) |
||||||
|
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); |
||||||
|
|
||||||
|
if (IsEdit) |
||||||
|
{ |
||||||
|
Option<MultiCollectionViewModel> maybeCollection = await _mediator.Send(new GetMultiCollectionById(Id)); |
||||||
|
maybeCollection.IfSome(collection => |
||||||
|
{ |
||||||
|
_model.Id = collection.Id; |
||||||
|
_model.Name = collection.Name; |
||||||
|
_model.Items = collection.Items.Map(item => |
||||||
|
new MultiCollectionItemEditViewModel |
||||||
|
{ |
||||||
|
Collection = item.Collection, |
||||||
|
ScheduleAsGroup = item.ScheduleAsGroup, |
||||||
|
PlaybackOrder = item.PlaybackOrder, |
||||||
|
}).ToList(); |
||||||
|
}); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
_model.Name = "New Multi Collection"; |
||||||
|
_model.Items = new List<MultiCollectionItemEditViewModel>(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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()) |
||||||
|
{ |
||||||
|
Seq<BaseError> errorMessage = IsEdit ? |
||||||
|
(await _mediator.Send(new UpdateMultiCollection(Id, _model.Name, _model.Items.Map(i => new UpdateMultiCollectionItem(i.Collection.Id, i.ScheduleAsGroup, i.PlaybackOrder)).ToList()))).LeftToSeq() : |
||||||
|
(await _mediator.Send(new CreateMultiCollection(_model.Name, _model.Items.Map(i => new CreateMultiCollectionItem(i.Collection.Id, i.ScheduleAsGroup, i.PlaybackOrder)).ToList()))).LeftToSeq(); |
||||||
|
|
||||||
|
errorMessage.HeadOrNone().Match( |
||||||
|
error => |
||||||
|
{ |
||||||
|
_snackbar.Add(error.Value, Severity.Error); |
||||||
|
_logger.LogError("Error saving collection: {Error}", error.Value); |
||||||
|
}, |
||||||
|
() => _navigationManager.NavigateTo("/media/collections")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void RemoveCollection(MultiCollectionItemEditViewModel item) |
||||||
|
{ |
||||||
|
_model.Items.Remove(item); |
||||||
|
} |
||||||
|
|
||||||
|
private void AddCollection() |
||||||
|
{ |
||||||
|
if (_selectedCollection != null && _model.Items.All(i => i.Collection != _selectedCollection)) |
||||||
|
{ |
||||||
|
_model.Items.Add(new MultiCollectionItemEditViewModel |
||||||
|
{ |
||||||
|
Collection = _selectedCollection, |
||||||
|
PlaybackOrder = PlaybackOrder.Chronological |
||||||
|
}); |
||||||
|
|
||||||
|
_selectedCollection = null; |
||||||
|
_collectionAutocomplete.Reset(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private Task<IEnumerable<MediaCollectionViewModel>> SearchCollections(string value) => |
||||||
|
_collections.Filter(c => _model.Items.All(i => i.Collection != c) && c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
using ErsatzTV.ViewModels; |
||||||
|
using FluentValidation; |
||||||
|
|
||||||
|
namespace ErsatzTV.Validators |
||||||
|
{ |
||||||
|
public class CollectionEditViewModelValidator : AbstractValidator<CollectionEditViewModel> |
||||||
|
{ |
||||||
|
public CollectionEditViewModelValidator() => RuleFor(c => c.Name).NotEmpty(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
using ErsatzTV.ViewModels; |
||||||
|
using FluentValidation; |
||||||
|
|
||||||
|
namespace ErsatzTV.Validators |
||||||
|
{ |
||||||
|
public class MultiCollectionEditViewModelValidator : AbstractValidator<MultiCollectionEditViewModel> |
||||||
|
{ |
||||||
|
public MultiCollectionEditViewModelValidator() => RuleFor(c => c.Name).NotEmpty(); |
||||||
|
} |
||||||
|
} |
@ -1,10 +0,0 @@ |
|||||||
using ErsatzTV.ViewModels; |
|
||||||
using FluentValidation; |
|
||||||
|
|
||||||
namespace ErsatzTV.Validators |
|
||||||
{ |
|
||||||
public class SimpleMediaCollectionEditViewModelValidator : AbstractValidator<SimpleMediaCollectionEditViewModel> |
|
||||||
{ |
|
||||||
public SimpleMediaCollectionEditViewModelValidator() => RuleFor(c => c.Name).NotEmpty(); |
|
||||||
} |
|
||||||
} |
|
@ -1,6 +1,6 @@ |
|||||||
namespace ErsatzTV.ViewModels |
namespace ErsatzTV.ViewModels |
||||||
{ |
{ |
||||||
public class SimpleMediaCollectionEditViewModel |
public class CollectionEditViewModel |
||||||
{ |
{ |
||||||
public int Id { get; set; } |
public int Id { get; set; } |
||||||
public string Name { get; set; } |
public string Name { get; set; } |
@ -0,0 +1,11 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace ErsatzTV.ViewModels |
||||||
|
{ |
||||||
|
public class MultiCollectionEditViewModel |
||||||
|
{ |
||||||
|
public int Id { get; set; } |
||||||
|
public string Name { get; set; } |
||||||
|
public List<MultiCollectionItemEditViewModel> Items { get; set; } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
using ErsatzTV.Application.MediaCollections; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.ViewModels |
||||||
|
{ |
||||||
|
public class MultiCollectionItemEditViewModel |
||||||
|
{ |
||||||
|
public MediaCollectionViewModel Collection { get; set; } |
||||||
|
public bool ScheduleAsGroup { get; set; } |
||||||
|
public PlaybackOrder PlaybackOrder { get; set; } |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue