preview block playout in block editor (#1558)
* block editor cleanup * preview block playout * cleanup
This commit is contained in:
@@ -22,7 +22,7 @@ internal static class Mapper
|
|||||||
programScheduleAlternate.DaysOfMonth,
|
programScheduleAlternate.DaysOfMonth,
|
||||||
programScheduleAlternate.MonthsOfYear);
|
programScheduleAlternate.MonthsOfYear);
|
||||||
|
|
||||||
private static string GetDisplayTitle(PlayoutItem playoutItem)
|
internal static string GetDisplayTitle(PlayoutItem playoutItem)
|
||||||
{
|
{
|
||||||
switch (playoutItem.MediaItem)
|
switch (playoutItem.MediaItem)
|
||||||
{
|
{
|
||||||
@@ -80,7 +80,7 @@ internal static class Mapper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetDisplayDuration(TimeSpan duration) =>
|
internal static string GetDisplayDuration(TimeSpan duration) =>
|
||||||
string.Format(
|
string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Application.Scheduling;
|
||||||
|
|
||||||
|
public record PreviewBlockPlayout(ReplaceBlockItems Data) : IRequest<List<PlayoutItemPreviewViewModel>>;
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Scheduling;
|
||||||
|
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||||
|
using ErsatzTV.Core.Scheduling;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling;
|
||||||
|
|
||||||
|
public class PreviewBlockPlayoutHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IBlockPlayoutBuilder blockPlayoutBuilder)
|
||||||
|
: IRequestHandler<PreviewBlockPlayout, List<PlayoutItemPreviewViewModel>>
|
||||||
|
{
|
||||||
|
public async Task<List<PlayoutItemPreviewViewModel>> Handle(
|
||||||
|
PreviewBlockPlayout request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var template = new Template
|
||||||
|
{
|
||||||
|
Items = []
|
||||||
|
};
|
||||||
|
|
||||||
|
template.Items.Add(
|
||||||
|
new TemplateItem
|
||||||
|
{
|
||||||
|
Block = MapToBlock(request.Data),
|
||||||
|
StartTime = TimeSpan.Zero,
|
||||||
|
Template = template
|
||||||
|
});
|
||||||
|
|
||||||
|
var playout = new Playout
|
||||||
|
{
|
||||||
|
Channel = new Channel(Guid.NewGuid())
|
||||||
|
{
|
||||||
|
Number = "1",
|
||||||
|
Name = "Block Preview"
|
||||||
|
},
|
||||||
|
Items = [],
|
||||||
|
ProgramSchedulePlayoutType = ProgramSchedulePlayoutType.Block,
|
||||||
|
PlayoutHistory = [],
|
||||||
|
Templates =
|
||||||
|
[
|
||||||
|
new PlayoutTemplate
|
||||||
|
{
|
||||||
|
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
|
||||||
|
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
|
||||||
|
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
|
||||||
|
Template = template
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await blockPlayoutBuilder.Build(
|
||||||
|
playout,
|
||||||
|
PlayoutBuildMode.Reset,
|
||||||
|
NullLogger.Instance,
|
||||||
|
1,
|
||||||
|
randomizeStartPoints: true,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
// load playout item details for title
|
||||||
|
foreach (PlayoutItem playoutItem in playout.Items)
|
||||||
|
{
|
||||||
|
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(mi => (mi as Movie).MovieMetadata)
|
||||||
|
.Include(mi => (mi as Movie).MediaVersions)
|
||||||
|
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
|
||||||
|
.Include(mi => (mi as MusicVideo).MediaVersions)
|
||||||
|
.Include(mi => (mi as MusicVideo).Artist)
|
||||||
|
.ThenInclude(mm => mm.ArtistMetadata)
|
||||||
|
.Include(mi => (mi as Episode).EpisodeMetadata)
|
||||||
|
.Include(mi => (mi as Episode).MediaVersions)
|
||||||
|
.Include(mi => (mi as Episode).Season)
|
||||||
|
.ThenInclude(s => s.SeasonMetadata)
|
||||||
|
.Include(mi => (mi as Episode).Season.Show)
|
||||||
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
|
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||||
|
.Include(mi => (mi as OtherVideo).MediaVersions)
|
||||||
|
.Include(mi => (mi as Song).SongMetadata)
|
||||||
|
.Include(mi => (mi as Song).MediaVersions)
|
||||||
|
.SelectOneAsync(mi => mi.Id, mi => mi.Id == playoutItem.MediaItemId);
|
||||||
|
|
||||||
|
foreach (MediaItem mediaItem in maybeMediaItem)
|
||||||
|
{
|
||||||
|
playoutItem.MediaItem = mediaItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playout.Items.Map(Mapper.ProjectToViewModel).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Block MapToBlock(ReplaceBlockItems request) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Minutes = request.Minutes,
|
||||||
|
Name = request.Name,
|
||||||
|
Items = request.Items.Map(MapToBlockItem).ToList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static BlockItem MapToBlockItem(int id, ReplaceBlockItem request) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Index = request.Index,
|
||||||
|
CollectionType = request.CollectionType,
|
||||||
|
CollectionId = request.CollectionId,
|
||||||
|
MultiCollectionId = request.MultiCollectionId,
|
||||||
|
SmartCollectionId = request.SmartCollectionId,
|
||||||
|
MediaItemId = request.MediaItemId,
|
||||||
|
PlaybackOrder = request.PlaybackOrder
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -53,4 +53,11 @@ internal static class Mapper
|
|||||||
playoutTemplate.DaysOfWeek,
|
playoutTemplate.DaysOfWeek,
|
||||||
playoutTemplate.DaysOfMonth,
|
playoutTemplate.DaysOfMonth,
|
||||||
playoutTemplate.MonthsOfYear);
|
playoutTemplate.MonthsOfYear);
|
||||||
|
|
||||||
|
internal static PlayoutItemPreviewViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
|
||||||
|
new(
|
||||||
|
Playouts.Mapper.GetDisplayTitle(playoutItem),
|
||||||
|
playoutItem.StartOffset.TimeOfDay,
|
||||||
|
playoutItem.FinishOffset.TimeOfDay,
|
||||||
|
Playouts.Mapper.GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Application.Scheduling;
|
||||||
|
|
||||||
|
public record PlayoutItemPreviewViewModel(string Title, TimeSpan Start, TimeSpan Finish, string Duration);
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Scheduling;
|
using ErsatzTV.Core.Scheduling;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Interfaces.Scheduling;
|
namespace ErsatzTV.Core.Interfaces.Scheduling;
|
||||||
|
|
||||||
public interface IBlockPlayoutBuilder
|
public interface IBlockPlayoutBuilder
|
||||||
{
|
{
|
||||||
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
|
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<Playout> Build(
|
||||||
|
Playout playout,
|
||||||
|
PlayoutBuildMode mode,
|
||||||
|
ILogger customLogger,
|
||||||
|
int daysToBuild,
|
||||||
|
bool randomizeStartPoints,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,24 @@ public class BlockPlayoutBuilder(
|
|||||||
{
|
{
|
||||||
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
|
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
logger.LogDebug(
|
int daysToBuild = await configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
|
||||||
|
.IfNoneAsync(2);
|
||||||
|
|
||||||
|
return await Build(playout, mode, logger, daysToBuild, randomizeStartPoints: false, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Playout> Build(
|
||||||
|
Playout playout,
|
||||||
|
PlayoutBuildMode mode,
|
||||||
|
ILogger customLogger,
|
||||||
|
int daysToBuild,
|
||||||
|
bool randomizeStartPoints,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// ReSharper disable once LocalVariableHidesPrimaryConstructorParameter
|
||||||
|
ILogger log = customLogger ?? logger;
|
||||||
|
|
||||||
|
log.LogDebug(
|
||||||
"Building block playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
|
"Building block playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
|
||||||
playout.Id,
|
playout.Id,
|
||||||
playout.Channel.Number,
|
playout.Channel.Number,
|
||||||
@@ -34,8 +51,6 @@ public class BlockPlayoutBuilder(
|
|||||||
|
|
||||||
DateTimeOffset start = DateTimeOffset.Now;
|
DateTimeOffset start = DateTimeOffset.Now;
|
||||||
|
|
||||||
int daysToBuild = await configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
|
|
||||||
.IfNoneAsync(2);
|
|
||||||
|
|
||||||
// get blocks to schedule
|
// get blocks to schedule
|
||||||
List<EffectiveBlock> blocksToSchedule = EffectiveBlock.GetEffectiveBlocks(playout, start, daysToBuild);
|
List<EffectiveBlock> blocksToSchedule = EffectiveBlock.GetEffectiveBlocks(playout, start, daysToBuild);
|
||||||
@@ -43,8 +58,9 @@ public class BlockPlayoutBuilder(
|
|||||||
// get all collection items for the playout
|
// get all collection items for the playout
|
||||||
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(blocksToSchedule);
|
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(blocksToSchedule);
|
||||||
|
|
||||||
Dictionary<PlayoutItem, BlockKey> itemBlockKeys = BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(playout);
|
Dictionary<PlayoutItem, BlockKey> itemBlockKeys =
|
||||||
|
BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(playout);
|
||||||
|
|
||||||
// remove items without a block key (shouldn't happen often, just upgrades)
|
// remove items without a block key (shouldn't happen often, just upgrades)
|
||||||
playout.Items.RemoveAll(i => !itemBlockKeys.ContainsKey(i));
|
playout.Items.RemoveAll(i => !itemBlockKeys.ContainsKey(i));
|
||||||
|
|
||||||
@@ -64,14 +80,14 @@ public class BlockPlayoutBuilder(
|
|||||||
{
|
{
|
||||||
currentTime = effectiveBlock.Start;
|
currentTime = effectiveBlock.Start;
|
||||||
|
|
||||||
logger.LogDebug(
|
log.LogDebug(
|
||||||
"Will schedule block {Block} at {Start}",
|
"Will schedule block {Block} at {Start}",
|
||||||
effectiveBlock.Block.Name,
|
effectiveBlock.Block.Name,
|
||||||
effectiveBlock.Start);
|
effectiveBlock.Start);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug(
|
log.LogDebug(
|
||||||
"Will schedule block {Block} with start {Start} at {ActualStart}",
|
"Will schedule block {Block} with start {Start} at {ActualStart}",
|
||||||
effectiveBlock.Block.Name,
|
effectiveBlock.Block.Name,
|
||||||
effectiveBlock.Start,
|
effectiveBlock.Start,
|
||||||
@@ -90,7 +106,7 @@ public class BlockPlayoutBuilder(
|
|||||||
|
|
||||||
if (currentTime >= blockFinish)
|
if (currentTime >= blockFinish)
|
||||||
{
|
{
|
||||||
logger.LogDebug(
|
log.LogDebug(
|
||||||
"Current time {Time} for block {Block} is beyond block finish {Finish}; will stop with this block's items",
|
"Current time {Time} for block {Block} is beyond block finish {Finish}; will stop with this block's items",
|
||||||
currentTime,
|
currentTime,
|
||||||
effectiveBlock.Block.Name,
|
effectiveBlock.Block.Name,
|
||||||
@@ -129,7 +145,7 @@ public class BlockPlayoutBuilder(
|
|||||||
// seek to the appropriate place in the collection enumerator
|
// seek to the appropriate place in the collection enumerator
|
||||||
foreach (PlayoutHistory history in maybeHistory)
|
foreach (PlayoutHistory history in maybeHistory)
|
||||||
{
|
{
|
||||||
logger.LogDebug("History is applicable: {When}: {History}", history.When, history.Details);
|
log.LogDebug("History is applicable: {When}: {History}", history.When, history.Details);
|
||||||
|
|
||||||
HistoryDetails.MoveToNextItem(
|
HistoryDetails.MoveToNextItem(
|
||||||
collectionItems,
|
collectionItems,
|
||||||
@@ -138,9 +154,19 @@ public class BlockPlayoutBuilder(
|
|||||||
blockItem.PlaybackOrder);
|
blockItem.PlaybackOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (maybeHistory.IsNone && randomizeStartPoints)
|
||||||
|
{
|
||||||
|
enumerator.ResetState(
|
||||||
|
new CollectionEnumeratorState
|
||||||
|
{
|
||||||
|
Seed = new Random().Next(),
|
||||||
|
Index = new Random().Next(collectionItems.Count)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
foreach (MediaItem mediaItem in enumerator.Current)
|
foreach (MediaItem mediaItem in enumerator.Current)
|
||||||
{
|
{
|
||||||
logger.LogDebug(
|
log.LogDebug(
|
||||||
"current item: {Id} / {Title}",
|
"current item: {Id} / {Title}",
|
||||||
mediaItem.Id,
|
mediaItem.Id,
|
||||||
mediaItem is Episode e ? GetTitle(e) : string.Empty);
|
mediaItem is Episode e ? GetTitle(e) : string.Empty);
|
||||||
@@ -223,7 +249,7 @@ public class BlockPlayoutBuilder(
|
|||||||
group.Add(history);
|
group.Add(history);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ((string key, List<PlayoutHistory> group) in groups)
|
foreach ((string _, List<PlayoutHistory> group) in groups)
|
||||||
{
|
{
|
||||||
//logger.LogDebug("History key {Key} has {Count} items in group", key, group.Count);
|
//logger.LogDebug("History key {Key} has {Count} items in group", key, group.Count);
|
||||||
|
|
||||||
|
|||||||
@@ -25,67 +25,80 @@
|
|||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="6">
|
<MudItem xs="6">
|
||||||
<MudSelect T="int" @bind-Value="_durationMinutes" Adornment="Adornment.End" AdornmentText="minutes">
|
<MudSelect T="int" @bind-Value="_durationMinutes" Adornment="Adornment.End" AdornmentText="minutes">
|
||||||
<MudSelectItem Value="0" />
|
<MudSelectItem Value="0"/>
|
||||||
<MudSelectItem Value="15" />
|
<MudSelectItem Value="15"/>
|
||||||
<MudSelectItem Value="30" />
|
<MudSelectItem Value="30"/>
|
||||||
<MudSelectItem Value="45" />
|
<MudSelectItem Value="45"/>
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
</div>
|
</div>
|
||||||
<MudTable Class="mt-6" Hover="true" Items="_block.Items.OrderBy(i => i.Index)" Dense="true" @bind-SelectedItem="_selectedItem">
|
|
||||||
<ColGroup>
|
|
||||||
<col/>
|
|
||||||
<col style="width: 60px;"/>
|
|
||||||
<col style="width: 60px;"/>
|
|
||||||
<col style="width: 60px;"/>
|
|
||||||
</ColGroup>
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>Collection</MudTh>
|
|
||||||
<MudTh/>
|
|
||||||
<MudTh/>
|
|
||||||
<MudTh/>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd DataLabel="Collection">
|
|
||||||
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
||||||
@context.CollectionName
|
|
||||||
</MudText>
|
|
||||||
</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
|
|
||||||
OnClick="@(_ => MoveItemUp(context))"
|
|
||||||
Disabled="@(_block.Items.All(x => x.Index >= context.Index))">
|
|
||||||
</MudIconButton>
|
|
||||||
</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
|
|
||||||
OnClick="@(_ => MoveItemDown(context))"
|
|
||||||
Disabled="@(_block.Items.All(x => x.Index <= context.Index))">
|
|
||||||
</MudIconButton>
|
|
||||||
</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
OnClick="@(_ => RemoveBlockItem(context))">
|
|
||||||
</MudIconButton>
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
</MudTable>
|
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddBlockItem())" Class="mt-4">
|
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddBlockItem())" Class="mt-4">
|
||||||
Add Block Item
|
Add Block Item
|
||||||
</MudButton>
|
</MudButton>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
|
||||||
Save Changes
|
Save Changes
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="@(_ => PreviewPlayout())" Class="mt-4 ml-4">
|
||||||
@if (_selectedItem is not null)
|
Preview Block Playout
|
||||||
{
|
</MudButton>
|
||||||
<EditForm Model="_selectedItem">
|
<MudGrid>
|
||||||
<FluentValidationValidator/>
|
<MudItem xs="8">
|
||||||
<div style="display: flex; flex-direction: row;" class="mt-6">
|
<MudTable Class="mt-6" Hover="true" Items="_block.Items.OrderBy(i => i.Index)" Dense="true" @bind-SelectedItem="_selectedItem">
|
||||||
<div style="flex-grow: 1; max-width: 400px;" class="mr-6">
|
<ColGroup>
|
||||||
|
<col/>
|
||||||
|
<col/>
|
||||||
|
<col style="width: 60px;"/>
|
||||||
|
<col style="width: 60px;"/>
|
||||||
|
<col style="width: 60px;"/>
|
||||||
|
</ColGroup>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Collection</MudTh>
|
||||||
|
<MudTh>Playback Order</MudTh>
|
||||||
|
<MudTh/>
|
||||||
|
<MudTh/>
|
||||||
|
<MudTh/>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd DataLabel="Collection">
|
||||||
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
||||||
|
@context.CollectionName
|
||||||
|
</MudText>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Playback Order">
|
||||||
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
||||||
|
@context.PlaybackOrder
|
||||||
|
</MudText>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
|
||||||
|
OnClick="@(_ => MoveItemUp(context))"
|
||||||
|
Disabled="@(_block.Items.All(x => x.Index >= context.Index))">
|
||||||
|
</MudIconButton>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
|
||||||
|
OnClick="@(_ => MoveItemDown(context))"
|
||||||
|
Disabled="@(_block.Items.All(x => x.Index <= context.Index))">
|
||||||
|
</MudIconButton>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
OnClick="@(_ => RemoveBlockItem(context))">
|
||||||
|
</MudIconButton>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
<div class="mt-4">
|
||||||
|
@if (_selectedItem is not null)
|
||||||
|
{
|
||||||
|
<EditForm Model="_selectedItem">
|
||||||
|
<FluentValidationValidator/>
|
||||||
|
<div style="max-width: 400px;" class="mr-6">
|
||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
<MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
|
<MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
|
||||||
@@ -108,6 +121,7 @@
|
|||||||
</MoreItemsTemplate>
|
</MoreItemsTemplate>
|
||||||
</MudAutocomplete>
|
</MudAutocomplete>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.MultiCollection)
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.MultiCollection)
|
||||||
{
|
{
|
||||||
<MudAutocomplete Class="mt-3" T="MultiCollectionViewModel" Label="Multi Collection"
|
<MudAutocomplete Class="mt-3" T="MultiCollectionViewModel" Label="Multi Collection"
|
||||||
@@ -120,6 +134,7 @@
|
|||||||
</MoreItemsTemplate>
|
</MoreItemsTemplate>
|
||||||
</MudAutocomplete>
|
</MudAutocomplete>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.SmartCollection)
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.SmartCollection)
|
||||||
{
|
{
|
||||||
<MudAutocomplete Class="mt-3" T="SmartCollectionViewModel" Label="Smart Collection"
|
<MudAutocomplete Class="mt-3" T="SmartCollectionViewModel" Label="Smart Collection"
|
||||||
@@ -132,6 +147,7 @@
|
|||||||
</MoreItemsTemplate>
|
</MoreItemsTemplate>
|
||||||
</MudAutocomplete>
|
</MudAutocomplete>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
|
||||||
{
|
{
|
||||||
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Television Show"
|
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Television Show"
|
||||||
@@ -144,6 +160,7 @@
|
|||||||
</MoreItemsTemplate>
|
</MoreItemsTemplate>
|
||||||
</MudAutocomplete>
|
</MudAutocomplete>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
|
||||||
{
|
{
|
||||||
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Television Season"
|
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Television Season"
|
||||||
@@ -157,6 +174,7 @@
|
|||||||
</MoreItemsTemplate>
|
</MoreItemsTemplate>
|
||||||
</MudAutocomplete>
|
</MudAutocomplete>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Artist)
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Artist)
|
||||||
{
|
{
|
||||||
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Artist"
|
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Artist"
|
||||||
@@ -170,6 +188,7 @@
|
|||||||
</MoreItemsTemplate>
|
</MoreItemsTemplate>
|
||||||
</MudAutocomplete>
|
</MudAutocomplete>
|
||||||
}
|
}
|
||||||
|
|
||||||
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
|
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
|
||||||
@switch (_selectedItem.CollectionType)
|
@switch (_selectedItem.CollectionType)
|
||||||
{
|
{
|
||||||
@@ -204,8 +223,31 @@
|
|||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</EditForm>
|
||||||
</EditForm>
|
}
|
||||||
|
</div>
|
||||||
|
@if (_previewItems != null)
|
||||||
|
{
|
||||||
|
<MudTable Class="mt-8"
|
||||||
|
Hover="true"
|
||||||
|
Dense="true"
|
||||||
|
Items="_previewItems">
|
||||||
|
<ToolBarContent>
|
||||||
|
<MudText Typo="Typo.h6">Block Playout Preview</MudText>
|
||||||
|
</ToolBarContent>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Start</MudTh>
|
||||||
|
<MudTh>Finish</MudTh>
|
||||||
|
<MudTh>Media Item</MudTh>
|
||||||
|
<MudTh>Duration</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd DataLabel="Start">@context.Start.ToString(@"hh\:mm\:ss")</MudTd>
|
||||||
|
<MudTd DataLabel="Finish">@context.Finish.ToString(@"hh\:mm\:ss")</MudTd>
|
||||||
|
<MudTd DataLabel="Media Item">@context.Title</MudTd>
|
||||||
|
<MudTd DataLabel="Duration">@context.Duration</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
}
|
}
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@@ -217,6 +259,7 @@
|
|||||||
|
|
||||||
private BlockItemsEditViewModel _block = new() { Items = [] };
|
private BlockItemsEditViewModel _block = new() { Items = [] };
|
||||||
private BlockItemEditViewModel _selectedItem;
|
private BlockItemEditViewModel _selectedItem;
|
||||||
|
private List<PlayoutItemPreviewViewModel> _previewItems;
|
||||||
private int _durationHours = 0;
|
private int _durationHours = 0;
|
||||||
private int _durationMinutes = 15;
|
private int _durationMinutes = 15;
|
||||||
|
|
||||||
@@ -368,6 +411,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveChanges()
|
private async Task SaveChanges()
|
||||||
|
{
|
||||||
|
Seq<BaseError> errorMessages = await Mediator
|
||||||
|
.Send(GenerateReplaceRequest(), _cts.Token)
|
||||||
|
.Map(e => e.LeftToSeq());
|
||||||
|
|
||||||
|
errorMessages.HeadOrNone().Match(
|
||||||
|
error =>
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Unexpected error saving block: {error.Value}", Severity.Error);
|
||||||
|
Logger.LogError("Unexpected error saving block: {Error}", error.Value);
|
||||||
|
},
|
||||||
|
() => NavigationManager.NavigateTo("/blocks"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReplaceBlockItems GenerateReplaceRequest()
|
||||||
{
|
{
|
||||||
var items = _block.Items.Map(
|
var items = _block.Items.Map(
|
||||||
item => new ReplaceBlockItem(
|
item => new ReplaceBlockItem(
|
||||||
@@ -381,16 +439,12 @@
|
|||||||
|
|
||||||
_block.Minutes = _durationHours * 60 + _durationMinutes;
|
_block.Minutes = _durationHours * 60 + _durationMinutes;
|
||||||
|
|
||||||
Seq<BaseError> errorMessages = await Mediator
|
return new ReplaceBlockItems(Id, _block.Name, _block.Minutes, items);
|
||||||
.Send(new ReplaceBlockItems(Id, _block.Name, _block.Minutes, items), _cts.Token)
|
|
||||||
.Map(e => e.LeftToSeq());
|
|
||||||
|
|
||||||
errorMessages.HeadOrNone().Match(
|
|
||||||
error =>
|
|
||||||
{
|
|
||||||
Snackbar.Add($"Unexpected error saving block: {error.Value}", Severity.Error);
|
|
||||||
Logger.LogError("Unexpected error saving block: {Error}", error.Value);
|
|
||||||
},
|
|
||||||
() => NavigationManager.NavigateTo("/blocks"));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private async Task PreviewPlayout()
|
||||||
|
{
|
||||||
|
_selectedItem = null;
|
||||||
|
_previewItems = await Mediator.Send(new PreviewBlockPlayout(GenerateReplaceRequest()), _cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user