custom collection playback order (#64)
* add custom index to collection items * add custom collection order to ui * cleanup
This commit is contained in:
@@ -7,5 +7,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
List<MovieCardViewModel> MovieCards,
|
List<MovieCardViewModel> MovieCards,
|
||||||
List<TelevisionShowCardViewModel> ShowCards,
|
List<TelevisionShowCardViewModel> ShowCards,
|
||||||
List<TelevisionSeasonCardViewModel> SeasonCards,
|
List<TelevisionSeasonCardViewModel> SeasonCards,
|
||||||
List<TelevisionEpisodeCardViewModel> EpisodeCards);
|
List<TelevisionEpisodeCardViewModel> EpisodeCards)
|
||||||
|
{
|
||||||
|
public bool UseCustomPlaybackOrder { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,11 +52,20 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
ProjectToViewModel(Collection collection) =>
|
ProjectToViewModel(Collection collection) =>
|
||||||
new(
|
new(
|
||||||
collection.Name,
|
collection.Name,
|
||||||
collection.MediaItems.OfType<Movie>().Map(m => ProjectToViewModel(m.MovieMetadata.Head())).ToList(),
|
collection.MediaItems.OfType<Movie>().Map(
|
||||||
|
m => ProjectToViewModel(m.MovieMetadata.Head()) with
|
||||||
|
{
|
||||||
|
CustomIndex = GetCustomIndex(collection, m.Id)
|
||||||
|
}).ToList(),
|
||||||
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(),
|
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(),
|
||||||
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(),
|
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(),
|
||||||
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head()))
|
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head()))
|
||||||
.ToList());
|
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||||
|
|
||||||
|
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
||||||
|
Optional(collection.CollectionItems.Find(ci => ci.MediaItemId == mediaItemId))
|
||||||
|
.Map(ci => ci.CustomIndex ?? 0)
|
||||||
|
.IfNone(0);
|
||||||
|
|
||||||
internal static SearchCardResultsViewModel ProjectToSearchResults(List<MediaItem> items) =>
|
internal static SearchCardResultsViewModel ProjectToSearchResults(List<MediaItem> items) =>
|
||||||
new(
|
new(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MediaCardViewModel(string Title, string Subtitle, string SortTitle, string Poster);
|
public record MediaCardViewModel(int MediaItemId, string Title, string Subtitle, string SortTitle, string Poster);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
{
|
{
|
||||||
public record MovieCardViewModel
|
public record MovieCardViewModel
|
||||||
(int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
||||||
|
MovieId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster)
|
Poster)
|
||||||
{
|
{
|
||||||
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
string Title,
|
string Title,
|
||||||
string Plot,
|
string Plot,
|
||||||
string Poster) : MediaCardViewModel(
|
string Poster) : MediaCardViewModel(
|
||||||
|
EpisodeId,
|
||||||
Title,
|
Title,
|
||||||
$"Episode {Episode}",
|
$"Episode {Episode}",
|
||||||
$"Episode {Episode}",
|
$"Episode {Episode}",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
string SortTitle,
|
string SortTitle,
|
||||||
string Poster,
|
string Poster,
|
||||||
string Placeholder) : MediaCardViewModel(
|
string Placeholder) : MediaCardViewModel(
|
||||||
|
TelevisionSeasonId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
public record TelevisionShowCardViewModel
|
public record TelevisionShowCardViewModel
|
||||||
(int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
||||||
|
TelevisionShowId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
{
|
{
|
||||||
public record UpdateCollection
|
public record UpdateCollection
|
||||||
(int CollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>;
|
(int CollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
public Option<bool> UseCustomPlaybackOrder { get; set; } = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record UpdateCollectionCustomOrder
|
||||||
|
(
|
||||||
|
int CollectionId,
|
||||||
|
List<MediaItemCustomOrder> MediaItemCustomOrders) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
|
||||||
|
public record MediaItemCustomOrder(int MediaItemId, int CustomIndex);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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 LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
UpdateCollectionCustomOrderHandler : MediatR.IRequestHandler<UpdateCollectionCustomOrder,
|
||||||
|
Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public UpdateCollectionCustomOrderHandler(
|
||||||
|
IMediaCollectionRepository mediaCollectionRepository,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||||
|
{
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
_channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
|
UpdateCollectionCustomOrder request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(c => ApplyUpdateRequest(c, request))
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollectionCustomOrder request)
|
||||||
|
{
|
||||||
|
foreach (MediaItemCustomOrder updateItem in request.MediaItemCustomOrders)
|
||||||
|
{
|
||||||
|
Option<CollectionItem> maybeCollectionItem =
|
||||||
|
c.CollectionItems.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId);
|
||||||
|
|
||||||
|
maybeCollectionItem.IfSome(ci => ci.CustomIndex = updateItem.CustomIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await _mediaCollectionRepository.Update(c))
|
||||||
|
{
|
||||||
|
// rebuild all playouts that use this collection
|
||||||
|
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
|
||||||
|
request.CollectionId))
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Collection>> Validate(UpdateCollectionCustomOrder request) =>
|
||||||
|
CollectionMustExist(request);
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||||
|
UpdateCollectionCustomOrder request) =>
|
||||||
|
_mediaCollectionRepository.Get(request.CollectionId)
|
||||||
|
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.Playouts.Commands;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
@@ -9,10 +11,16 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
{
|
{
|
||||||
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
|
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
|
||||||
{
|
{
|
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
public UpdateCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
public UpdateCollectionHandler(
|
||||||
|
IMediaCollectionRepository mediaCollectionRepository,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||||
|
{
|
||||||
_mediaCollectionRepository = mediaCollectionRepository;
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
_channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle(
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
UpdateCollection request,
|
UpdateCollection request,
|
||||||
@@ -21,10 +29,21 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.MapT(c => ApplyUpdateRequest(c, request))
|
.MapT(c => ApplyUpdateRequest(c, request))
|
||||||
.Bind(v => v.ToEitherAsync());
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection update)
|
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection request)
|
||||||
{
|
{
|
||||||
c.Name = update.Name;
|
c.Name = request.Name;
|
||||||
await _mediaCollectionRepository.Update(c);
|
request.UseCustomPlaybackOrder.IfSome(
|
||||||
|
useCustomPlaybackOrder => c.UseCustomPlaybackOrder = useCustomPlaybackOrder);
|
||||||
|
if (await _mediaCollectionRepository.Update(c) && request.UseCustomPlaybackOrder.IsSome)
|
||||||
|
{
|
||||||
|
// rebuild all playouts that use this collection
|
||||||
|
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
|
||||||
|
request.CollectionId))
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace ErsatzTV.Application.MediaCollections
|
namespace ErsatzTV.Application.MediaCollections
|
||||||
{
|
{
|
||||||
public record MediaCollectionViewModel(int Id, string Name) : MediaCardViewModel(
|
public record MediaCollectionViewModel(int Id, string Name) : MediaCardViewModel(
|
||||||
|
Id,
|
||||||
Name,
|
Name,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
Name,
|
Name,
|
||||||
|
|||||||
@@ -14,17 +14,22 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
private readonly Map<int, List<MediaItem>> _data;
|
private readonly Map<int, List<MediaItem>> _data;
|
||||||
|
|
||||||
public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data;
|
public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data;
|
||||||
|
|
||||||
public Task<Collection> Add(Collection collection) => throw new NotSupportedException();
|
public Task<Collection> Add(Collection collection) => throw new NotSupportedException();
|
||||||
public Task<bool> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException();
|
public Task<bool> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException();
|
||||||
public Task<bool> AddMediaItems(int collectionId, List<int> mediaItemIds) => throw new NotSupportedException();
|
public Task<bool> AddMediaItems(int collectionId, List<int> mediaItemIds) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<Option<Collection>> Get(int id) => throw new NotSupportedException();
|
public Task<Option<Collection>> Get(int id) => throw new NotSupportedException();
|
||||||
public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException();
|
public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException();
|
||||||
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException();
|
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<List<Collection>> GetAll() => throw new NotSupportedException();
|
public Task<List<Collection>> GetAll() => throw new NotSupportedException();
|
||||||
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask();
|
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask();
|
||||||
Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException();
|
Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException();
|
||||||
public Task Delete(int collectionId) => throw new NotSupportedException();
|
public Task Delete(int collectionId) => throw new NotSupportedException();
|
||||||
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException();
|
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException();
|
||||||
|
public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Scheduling;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Tests.Scheduling
|
||||||
|
{
|
||||||
|
public class CustomOrderContentTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void MediaItems_Should_Sort_By_CustomOrder()
|
||||||
|
{
|
||||||
|
Collection collection = CreateCollection(10);
|
||||||
|
List<MediaItem> contents = Episodes(10);
|
||||||
|
var state = new CollectionEnumeratorState();
|
||||||
|
|
||||||
|
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state);
|
||||||
|
|
||||||
|
for (var i = 10; i >= 1; i--)
|
||||||
|
{
|
||||||
|
customOrderContent.Current.IsSome.Should().BeTrue();
|
||||||
|
customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
|
||||||
|
customOrderContent.MoveNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void State_Index_Should_Increment()
|
||||||
|
{
|
||||||
|
Collection collection = CreateCollection(10);
|
||||||
|
List<MediaItem> contents = Episodes(10);
|
||||||
|
var state = new CollectionEnumeratorState();
|
||||||
|
|
||||||
|
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state);
|
||||||
|
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
customOrderContent.State.Index.Should().Be(i % 10);
|
||||||
|
customOrderContent.MoveNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void State_Should_Impact_Iterator_Start()
|
||||||
|
{
|
||||||
|
Collection collection = CreateCollection(10);
|
||||||
|
List<MediaItem> contents = Episodes(10);
|
||||||
|
var state = new CollectionEnumeratorState { Index = 5 };
|
||||||
|
|
||||||
|
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state);
|
||||||
|
|
||||||
|
for (var i = 5; i >= 1; i--)
|
||||||
|
{
|
||||||
|
customOrderContent.Current.IsSome.Should().BeTrue();
|
||||||
|
customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
|
||||||
|
customOrderContent.State.Index.Should().Be(5 - i + 5); // 5 through 10
|
||||||
|
customOrderContent.MoveNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Collection CreateCollection(int episodeCount)
|
||||||
|
{
|
||||||
|
var collection = new Collection { CollectionItems = new List<CollectionItem>() };
|
||||||
|
|
||||||
|
for (var i = 1; i <= episodeCount; i++)
|
||||||
|
{
|
||||||
|
collection.CollectionItems.Add(
|
||||||
|
new CollectionItem
|
||||||
|
{
|
||||||
|
MediaItemId = i,
|
||||||
|
// reverse order
|
||||||
|
CustomIndex = episodeCount - i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static List<MediaItem> Episodes(int count) =>
|
||||||
|
Range(1, count).Map(
|
||||||
|
i => (MediaItem) new Episode
|
||||||
|
{
|
||||||
|
Id = i,
|
||||||
|
EpisodeMetadata = new List<EpisodeMetadata>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ReleaseDate = new DateTime(2020, 1, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Reverse()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Domain
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public bool UseCustomPlaybackOrder { get; set; }
|
||||||
public List<MediaItem> MediaItems { get; set; }
|
public List<MediaItem> MediaItems { get; set; }
|
||||||
public List<CollectionItem> CollectionItems { get; set; }
|
public List<CollectionItem> CollectionItems { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,6 @@
|
|||||||
public Collection Collection { get; set; }
|
public Collection Collection { get; set; }
|
||||||
public int MediaItemId { get; set; }
|
public int MediaItemId { get; set; }
|
||||||
public MediaItem MediaItem { get; set; }
|
public MediaItem MediaItem { get; set; }
|
||||||
|
public int? CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
Task<Option<Collection>> Get(int id);
|
Task<Option<Collection>> Get(int id);
|
||||||
Task<Option<Collection>> GetCollectionWithItems(int id);
|
Task<Option<Collection>> GetCollectionWithItems(int id);
|
||||||
Task<Option<Collection>> GetCollectionWithItemsUntracked(int id);
|
Task<Option<Collection>> GetCollectionWithItemsUntracked(int id);
|
||||||
|
Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id);
|
||||||
Task<List<Collection>> GetAll();
|
Task<List<Collection>> GetAll();
|
||||||
Task<Option<List<MediaItem>>> GetItems(int id);
|
Task<Option<List<MediaItem>>> GetItems(int id);
|
||||||
Task<bool> Update(Collection collection);
|
Task<bool> Update(Collection collection);
|
||||||
Task Delete(int collectionId);
|
Task Delete(int collectionId);
|
||||||
Task<List<int>> PlayoutIdsUsingCollection(int collectionId);
|
Task<List<int>> PlayoutIdsUsingCollection(int collectionId);
|
||||||
|
Task<bool> IsCustomPlaybackOrder(int collectionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||||
|
using LanguageExt;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Scheduling
|
||||||
|
{
|
||||||
|
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||||
|
{
|
||||||
|
private readonly IList<MediaItem> _sortedMediaItems;
|
||||||
|
|
||||||
|
public CustomOrderCollectionEnumerator(
|
||||||
|
Collection collection,
|
||||||
|
List<MediaItem> mediaItems,
|
||||||
|
CollectionEnumeratorState state)
|
||||||
|
{
|
||||||
|
// TODO: this will break if we allow shows and seasons
|
||||||
|
_sortedMediaItems = collection.CollectionItems
|
||||||
|
.OrderBy(ci => ci.CustomIndex)
|
||||||
|
.Map(ci => mediaItems.First(mi => mi.Id == ci.MediaItemId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||||
|
while (State.Index < state.Index)
|
||||||
|
{
|
||||||
|
MoveNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CollectionEnumeratorState State { get; }
|
||||||
|
|
||||||
|
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||||
|
|
||||||
|
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,7 +78,8 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
playout.Channel.Number,
|
playout.Channel.Number,
|
||||||
playout.Channel.Name);
|
playout.Channel.Name);
|
||||||
|
|
||||||
Option<CollectionKey> emptyCollection = collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
|
Option<CollectionKey> emptyCollection =
|
||||||
|
collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
|
||||||
if (emptyCollection.IsSome)
|
if (emptyCollection.IsSome)
|
||||||
{
|
{
|
||||||
_logger.LogError(
|
_logger.LogError(
|
||||||
@@ -117,9 +118,15 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
playout.ProgramScheduleAnchors.Clear();
|
playout.ProgramScheduleAnchors.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
|
var sortedScheduleItems =
|
||||||
Map<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators =
|
playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
|
||||||
MapExtensions.Map(collectionMediaItems, (c, i) => GetMediaCollectionEnumerator(playout, c, i));
|
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
|
||||||
|
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
|
||||||
|
{
|
||||||
|
IMediaCollectionEnumerator enumerator =
|
||||||
|
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems);
|
||||||
|
collectionEnumerators.Add(collectionKey, enumerator);
|
||||||
|
}
|
||||||
|
|
||||||
// find start anchor
|
// find start anchor
|
||||||
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, sortedScheduleItems);
|
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, sortedScheduleItems);
|
||||||
@@ -357,20 +364,20 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
|
|
||||||
private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors(
|
private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors(
|
||||||
Playout playout,
|
Playout playout,
|
||||||
Map<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
|
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
|
||||||
{
|
{
|
||||||
var result = new List<PlayoutProgramScheduleAnchor>();
|
var result = new List<PlayoutProgramScheduleAnchor>();
|
||||||
|
|
||||||
foreach (CollectionKey collectionKey in collectionEnumerators.Keys)
|
foreach (CollectionKey collectionKey in collectionEnumerators.Keys)
|
||||||
{
|
{
|
||||||
Option<PlayoutProgramScheduleAnchor> maybeExisting = playout.ProgramScheduleAnchors
|
Option<PlayoutProgramScheduleAnchor> maybeExisting = playout.ProgramScheduleAnchors.FirstOrDefault(
|
||||||
.FirstOrDefault(
|
a => a.CollectionType == collectionKey.CollectionType
|
||||||
a => a.CollectionType == collectionKey.CollectionType
|
&& a.CollectionId == collectionKey.CollectionId
|
||||||
&& a.CollectionId == collectionKey.CollectionId
|
&& a.MediaItemId == collectionKey.MediaItemId);
|
||||||
&& a.MediaItemId == collectionKey.MediaItemId);
|
|
||||||
|
|
||||||
var maybeEnumeratorState = collectionEnumerators.GroupBy(e => e.Key, e => e.Value.State)
|
var maybeEnumeratorState = collectionEnumerators.GroupBy(e => e.Key, e => e.Value.State).ToDictionary(
|
||||||
.ToDictionary(mcs => mcs.Key, mcs => mcs.Head());
|
mcs => mcs.Key,
|
||||||
|
mcs => mcs.Head());
|
||||||
|
|
||||||
PlayoutProgramScheduleAnchor scheduleAnchor = maybeExisting.Match(
|
PlayoutProgramScheduleAnchor scheduleAnchor = maybeExisting.Match(
|
||||||
existing =>
|
existing =>
|
||||||
@@ -396,22 +403,37 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IMediaCollectionEnumerator GetMediaCollectionEnumerator(
|
private async Task<IMediaCollectionEnumerator> GetMediaCollectionEnumerator(
|
||||||
Playout playout,
|
Playout playout,
|
||||||
CollectionKey collectionKey,
|
CollectionKey collectionKey,
|
||||||
List<MediaItem> mediaItems)
|
List<MediaItem> mediaItems)
|
||||||
{
|
{
|
||||||
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors
|
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors.FirstOrDefault(
|
||||||
.FirstOrDefault(
|
a => a.ProgramScheduleId == playout.ProgramScheduleId
|
||||||
a => a.ProgramScheduleId == playout.ProgramScheduleId
|
&& a.CollectionType == collectionKey.CollectionType
|
||||||
&& a.CollectionType == collectionKey.CollectionType
|
&& a.CollectionId == collectionKey.CollectionId
|
||||||
&& a.CollectionId == collectionKey.CollectionId
|
&& a.MediaItemId == collectionKey.MediaItemId);
|
||||||
&& a.MediaItemId == collectionKey.MediaItemId);
|
|
||||||
|
|
||||||
CollectionEnumeratorState state = maybeAnchor.Match(
|
CollectionEnumeratorState state = maybeAnchor.Match(
|
||||||
anchor => anchor.EnumeratorState,
|
anchor => anchor.EnumeratorState,
|
||||||
() => new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 });
|
() => new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 });
|
||||||
|
|
||||||
|
if (await _mediaCollectionRepository.IsCustomPlaybackOrder(collectionKey.CollectionId ?? 0))
|
||||||
|
{
|
||||||
|
Option<Collection> collectionWithItems =
|
||||||
|
await _mediaCollectionRepository.GetCollectionWithCollectionItemsUntracked(
|
||||||
|
collectionKey.CollectionId ?? 0);
|
||||||
|
|
||||||
|
if (collectionKey.CollectionType == ProgramScheduleItemCollectionType.Collection &&
|
||||||
|
collectionWithItems.IsSome)
|
||||||
|
{
|
||||||
|
return new CustomOrderCollectionEnumerator(
|
||||||
|
collectionWithItems.ValueUnsafe(),
|
||||||
|
mediaItems,
|
||||||
|
state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (playout.ProgramSchedule.MediaCollectionPlaybackOrder)
|
switch (playout.ProgramSchedule.MediaCollectionPlaybackOrder)
|
||||||
{
|
{
|
||||||
case PlaybackOrder.Chronological:
|
case PlaybackOrder.Chronological:
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||||
|
{
|
||||||
|
public class CollectionItemConfiguration : IEntityTypeConfiguration<CollectionItem>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<CollectionItem> builder) => builder.ToTable("CollectionItem");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public Task<Option<Collection>> Get(int id) =>
|
public Task<Option<Collection>> Get(int id) =>
|
||||||
_dbContext.Collections
|
_dbContext.Collections
|
||||||
|
.Include(c => c.CollectionItems)
|
||||||
.OrderBy(c => c.Id)
|
.OrderBy(c => c.Id)
|
||||||
.SingleOrDefaultAsync(c => c.Id == id)
|
.SingleOrDefaultAsync(c => c.Id == id)
|
||||||
.Map(Optional);
|
.Map(Optional);
|
||||||
@@ -120,6 +121,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) =>
|
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) =>
|
||||||
_dbContext.Collections
|
_dbContext.Collections
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
.Include(c => c.CollectionItems)
|
||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
.ThenInclude(i => i.LibraryPath)
|
.ThenInclude(i => i.LibraryPath)
|
||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
@@ -145,6 +147,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.SingleOrDefaultAsync(c => c.Id == id)
|
.SingleOrDefaultAsync(c => c.Id == id)
|
||||||
.Map(Optional);
|
.Map(Optional);
|
||||||
|
|
||||||
|
public Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id) =>
|
||||||
|
_dbContext.Collections
|
||||||
|
.Include(c => c.CollectionItems)
|
||||||
|
.OrderBy(c => c.Id)
|
||||||
|
.SingleOrDefaultAsync(c => c.Id == id)
|
||||||
|
.Map(Optional);
|
||||||
|
|
||||||
public Task<List<Collection>> GetAll() =>
|
public Task<List<Collection>> GetAll() =>
|
||||||
_dbContext.Collections.ToListAsync();
|
_dbContext.Collections.ToListAsync();
|
||||||
|
|
||||||
@@ -174,6 +183,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
new { CollectionId = collectionId })
|
new { CollectionId = collectionId })
|
||||||
.Map(result => result.ToList());
|
.Map(result => result.ToList());
|
||||||
|
|
||||||
|
public Task<bool> IsCustomPlaybackOrder(int collectionId) =>
|
||||||
|
_dbConnection.QuerySingleAsync<bool>(
|
||||||
|
@"SELECT UseCustomPlaybackOrder FROM Collection WHERE Id = @CollectionId",
|
||||||
|
new { CollectionId = collectionId });
|
||||||
|
|
||||||
private async Task<List<MediaItem>> GetItemsForCollection(Collection collection)
|
private async Task<List<MediaItem>> GetItemsForCollection(Collection collection)
|
||||||
{
|
{
|
||||||
var result = new List<MediaItem>();
|
var result = new List<MediaItem>();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace ErsatzTV.Infrastructure.Data
|
|||||||
public DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
|
public DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
|
||||||
public DbSet<PlexMovie> PlexMovies { get; set; }
|
public DbSet<PlexMovie> PlexMovies { get; set; }
|
||||||
public DbSet<Collection> Collections { get; set; }
|
public DbSet<Collection> Collections { get; set; }
|
||||||
|
public DbSet<CollectionItem> CollectionItems { get; set; }
|
||||||
public DbSet<ProgramSchedule> ProgramSchedules { get; set; }
|
public DbSet<ProgramSchedule> ProgramSchedules { get; set; }
|
||||||
public DbSet<Playout> Playouts { get; set; }
|
public DbSet<Playout> Playouts { get; set; }
|
||||||
public DbSet<PlayoutItem> PlayoutItems { get; set; }
|
public DbSet<PlayoutItem> PlayoutItems { get; set; }
|
||||||
|
|||||||
Generated
+1626
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
public partial class Add_CollectionItem_CustomIndex : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) =>
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
"CustomIndex",
|
||||||
|
"CollectionItem",
|
||||||
|
"INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) =>
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
"CustomIndex",
|
||||||
|
"CollectionItem");
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1629
File diff suppressed because it is too large
Load Diff
+20
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
public partial class Add_Collection_UseCustomPlaybackOrder : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) =>
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
"UseCustomPlaybackOrder",
|
||||||
|
"Collection",
|
||||||
|
"INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) =>
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
"UseCustomPlaybackOrder",
|
||||||
|
"Collection");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("UseCustomPlaybackOrder")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Collection");
|
b.ToTable("Collection");
|
||||||
@@ -125,6 +128,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
b.Property<int>("MediaItemId")
|
b.Property<int>("MediaItemId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("CustomIndex")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("CollectionId", "MediaItemId");
|
b.HasKey("CollectionId", "MediaItemId");
|
||||||
|
|
||||||
b.HasIndex("MediaItemId");
|
b.HasIndex("MediaItemId");
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.MediaCollections.Commands;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
public class SortController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public SortController(IMediator mediator) => _mediator = mediator;
|
||||||
|
|
||||||
|
[HttpPost("media/collections/{collectionId}/items")]
|
||||||
|
public Task SortCollectionItems(
|
||||||
|
int collectionId,
|
||||||
|
[FromForm]
|
||||||
|
SortedMediaItemIds sortedMediaItemIds)
|
||||||
|
{
|
||||||
|
var ids = sortedMediaItemIds.Item.Map(int.Parse).ToList();
|
||||||
|
|
||||||
|
var request = new UpdateCollectionCustomOrder(
|
||||||
|
collectionId,
|
||||||
|
ids.Map(i => new MediaItemCustomOrder(i, ids.IndexOf(i))).ToList());
|
||||||
|
|
||||||
|
return _mediator.Send(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SortedMediaItemIds(List<string> Item);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
@inherits MultiSelectBase<CollectionItems>
|
@inherits MultiSelectBase<CollectionItems>
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject ChannelWriter<IBackgroundServiceRequest> Channel
|
@inject ChannelWriter<IBackgroundServiceRequest> Channel
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
|
||||||
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;">
|
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;">
|
||||||
<div style="align-items: center; display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%;" class="ml-6 mr-6">
|
<div style="align-items: center; display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%;" class="ml-6 mr-6">
|
||||||
@@ -51,6 +52,16 @@
|
|||||||
{
|
{
|
||||||
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")">@_data.EpisodeCards.Count Episodes</MudLink>
|
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")">@_data.EpisodeCards.Count Episodes</MudLink>
|
||||||
}
|
}
|
||||||
|
@if (SupportsCustomOrdering())
|
||||||
|
{
|
||||||
|
<div style="margin-left: auto">
|
||||||
|
<MudSwitch T="bool"
|
||||||
|
Checked="@_data.UseCustomPlaybackOrder"
|
||||||
|
Color="Color.Primary"
|
||||||
|
CheckedChanged="@OnUseCustomOrderChanged"
|
||||||
|
Label="Use Custom Playback Order"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
@@ -65,8 +76,8 @@
|
|||||||
Movies
|
Movies
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid" UserAttributes="@(new Dictionary<string, object> { { "id", "sortable-collection" } })">
|
||||||
@foreach (MovieCardViewModel card in _data.MovieCards.OrderBy(m => m.SortTitle))
|
@foreach (MovieCardViewModel card in OrderMovies(_data.MovieCards))
|
||||||
{
|
{
|
||||||
<MediaCard Data="@card"
|
<MediaCard Data="@card"
|
||||||
Link="@($"/media/movies/{card.MovieId}")"
|
Link="@($"/media/movies/{card.MovieId}")"
|
||||||
@@ -163,6 +174,9 @@
|
|||||||
|
|
||||||
private CollectionCardResultsViewModel _data;
|
private CollectionCardResultsViewModel _data;
|
||||||
|
|
||||||
|
private bool SupportsCustomOrdering() =>
|
||||||
|
_data.MovieCards.Any() && !_data.ShowCards.Any() && !_data.SeasonCards.Any() && !_data.EpisodeCards.Any();
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync() => await RefreshData();
|
protected override async Task OnParametersSetAsync() => await RefreshData();
|
||||||
|
|
||||||
protected override async Task RefreshData()
|
protected override async Task RefreshData()
|
||||||
@@ -175,6 +189,30 @@
|
|||||||
error => NavigationManager.NavigateTo("404"));
|
error => NavigationManager.NavigateTo("404"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IOrderedEnumerable<MovieCardViewModel> OrderMovies(List<MovieCardViewModel> movies)
|
||||||
|
{
|
||||||
|
if (_data.UseCustomPlaybackOrder)
|
||||||
|
{
|
||||||
|
return movies.OrderBy(m => m.CustomIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return movies.OrderBy(m => m.SortTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("sortableCollection", Id);
|
||||||
|
if (_data.UseCustomPlaybackOrder)
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("enableSorting");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("disableSorting");
|
||||||
|
}
|
||||||
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
|
}
|
||||||
|
|
||||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
List<MediaCardViewModel> GetSortedItems()
|
List<MediaCardViewModel> GetSortedItems()
|
||||||
@@ -255,4 +293,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnUseCustomOrderChanged()
|
||||||
|
{
|
||||||
|
_data.UseCustomPlaybackOrder = !_data.UseCustomPlaybackOrder;
|
||||||
|
var request = new UpdateCollection(Id, _data.Name) { UseCustomPlaybackOrder = _data.UseCustomPlaybackOrder };
|
||||||
|
await Mediator.Send(request);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,33 @@
|
|||||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet"/>
|
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet"/>
|
||||||
<link href="css/site.css" rel="stylesheet"/>
|
<link href="css/site.css" rel="stylesheet"/>
|
||||||
<link href="ErsatzTV.styles.css" rel="stylesheet"/>
|
<link href="ErsatzTV.styles.css" rel="stylesheet"/>
|
||||||
|
<link href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet">
|
||||||
|
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
|
||||||
|
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||||||
@await Html.PartialAsync("../Shared/_Favicons")
|
@await Html.PartialAsync("../Shared/_Favicons")
|
||||||
|
<script>
|
||||||
|
function sortableCollection(collectionId) {
|
||||||
|
$("#sortable-collection").sortable({
|
||||||
|
update: function(event, ui) {
|
||||||
|
const data = $(this).sortable('serialize');
|
||||||
|
$.ajax({
|
||||||
|
data: data,
|
||||||
|
type: 'POST',
|
||||||
|
url: `/media/collections/${collectionId}/items`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("#sortable-collection").disableSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableSorting() {
|
||||||
|
$("#sortable-collection").sortable("option", "disabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableSorting() {
|
||||||
|
$("#sortable-collection").sortable("option", "disabled", false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<component type="typeof(App)" render-mode="ServerPrerendered"/>
|
<component type="typeof(App)" render-mode="ServerPrerendered"/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@using Unit = LanguageExt.Unit
|
@using Unit = LanguageExt.Unit
|
||||||
@inject IMediator Mediator
|
@inject IMediator Mediator
|
||||||
|
|
||||||
<div class="@((ContainerClass ?? "media-card-container mr-6") + " pb-3")">
|
<div class="@((ContainerClass ?? "media-card-container mr-6") + " pb-3")" id="@($"item_{Data.MediaItemId}")">
|
||||||
@if (!string.IsNullOrWhiteSpace(Link))
|
@if (!string.IsNullOrWhiteSpace(Link))
|
||||||
{
|
{
|
||||||
<div class="@(IsSelected ? DeleteClicked.HasDelegate ? "media-card-selected-delete" : "media-card-selected" : "")"
|
<div class="@(IsSelected ? DeleteClicked.HasDelegate ? "media-card-selected-delete" : "media-card-selected" : "")"
|
||||||
|
|||||||
Reference in New Issue
Block a user