Browse Source

block scheduling skip unchanged blocks (#1550)

* schedule blocks in order

* block minutes must be multiple of 15

* improve block minutes entry, validation and display

* confirm deleting blocks and block groups

* confirm deleting templates and template groups

* skip unchanged blocks in playout
pull/1551/head
Jason Dove 2 years ago committed by GitHub
parent
commit
caef4a139e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs
  2. 6
      ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplateItemsHandler.cs
  3. 1
      ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItemsHandler.cs
  4. 1
      ErsatzTV.Core/Domain/PlayoutItem.cs
  5. 1
      ErsatzTV.Core/Domain/Scheduling/Block.cs
  6. 1
      ErsatzTV.Core/Domain/Scheduling/PlayoutTemplate.cs
  7. 1
      ErsatzTV.Core/Domain/Scheduling/Template.cs
  8. 175
      ErsatzTV.Core/Scheduling/BlockPlayoutBuilder.cs
  9. 4895
      ErsatzTV.Infrastructure.MySql/Migrations/20240114104626_Fix_BlockMinutes.Designer.cs
  10. 21
      ErsatzTV.Infrastructure.MySql/Migrations/20240114104626_Fix_BlockMinutes.cs
  11. 4907
      ErsatzTV.Infrastructure.MySql/Migrations/20240114120807_Add_PlayoutItemBlockKey.Designer.cs
  12. 63
      ErsatzTV.Infrastructure.MySql/Migrations/20240114120807_Add_PlayoutItemBlockKey.cs
  13. 4907
      ErsatzTV.Infrastructure.MySql/Migrations/20240114120924_Reset_BlockPlayouts_BlockKey.Designer.cs
  14. 26
      ErsatzTV.Infrastructure.MySql/Migrations/20240114120924_Reset_BlockPlayouts_BlockKey.cs
  15. 12
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  16. 4893
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240114103704_Fix_BlockMinutes.Designer.cs
  17. 21
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240114103704_Fix_BlockMinutes.cs
  18. 4905
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240114113426_Add_PlayoutItemBlockKey.Designer.cs
  19. 62
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240114113426_Add_PlayoutItemBlockKey.cs
  20. 4905
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240114120205_Reset_BlockPlayouts_BlockKey.Designer.cs
  21. 26
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240114120205_Reset_BlockPlayouts_BlockKey.cs
  22. 38
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  23. 33
      ErsatzTV/Pages/BlockEditor.razor
  24. 39
      ErsatzTV/Pages/Blocks.razor
  25. 39
      ErsatzTV/Pages/Templates.razor
  26. 15
      ErsatzTV/ViewModels/BlockTreeItemViewModel.cs

9
ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs

@ -26,6 +26,7 @@ public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFact
{ {
block.Name = request.Name; block.Name = request.Name;
block.Minutes = request.Minutes; block.Minutes = request.Minutes;
block.DateUpdated = DateTime.UtcNow;
dbContext.RemoveRange(block.Items); dbContext.RemoveRange(block.Items);
block.Items = request.Items.Map(i => BuildItem(block, i.Index, i)).ToList(); block.Items = request.Items.Map(i => BuildItem(block, i.Index, i)).ToList();
@ -56,7 +57,8 @@ public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFact
private static Task<Validation<BaseError, Block>> Validate(TvContext dbContext, ReplaceBlockItems request) => private static Task<Validation<BaseError, Block>> Validate(TvContext dbContext, ReplaceBlockItems request) =>
BlockMustExist(dbContext, request.BlockId) BlockMustExist(dbContext, request.BlockId)
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule)); .BindT(block => MinutesMustBeValid(request, block))
.BindT(block => CollectionTypesMustBeValid(request, block));
private static Task<Validation<BaseError, Block>> BlockMustExist(TvContext dbContext, int blockId) => private static Task<Validation<BaseError, Block>> BlockMustExist(TvContext dbContext, int blockId) =>
dbContext.Blocks dbContext.Blocks
@ -64,6 +66,11 @@ public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFact
.SelectOneAsync(b => b.Id, b => b.Id == blockId) .SelectOneAsync(b => b.Id, b => b.Id == blockId)
.Map(o => o.ToValidation<BaseError>("[BlockId] does not exist.")); .Map(o => o.ToValidation<BaseError>("[BlockId] does not exist."));
private static Validation<BaseError, Block> MinutesMustBeValid(ReplaceBlockItems request, Block block) =>
Optional(block)
.Filter(_ => request.Minutes > 0 && request.Minutes % 15 == 0)
.ToValidation<BaseError>("Block Minutes must be a positive multiple of 15");
private static Validation<BaseError, Block> CollectionTypesMustBeValid(ReplaceBlockItems request, Block block) => private static Validation<BaseError, Block> CollectionTypesMustBeValid(ReplaceBlockItems request, Block block) =>
request.Items.Map(item => CollectionTypeMustBeValid(item, block)).Sequence().Map(_ => block); request.Items.Map(item => CollectionTypeMustBeValid(item, block)).Sequence().Map(_ => block);

6
ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplateItemsHandler.cs

@ -42,6 +42,8 @@ public class ReplacePlayoutTemplateItemsHandler(
playout.Templates.Remove(remove); playout.Templates.Remove(remove);
} }
var now = DateTime.UtcNow;
foreach (ReplacePlayoutTemplate add in toAdd) foreach (ReplacePlayoutTemplate add in toAdd)
{ {
playout.Templates.Add( playout.Templates.Add(
@ -52,7 +54,8 @@ public class ReplacePlayoutTemplateItemsHandler(
TemplateId = add.TemplateId, TemplateId = add.TemplateId,
DaysOfWeek = add.DaysOfWeek, DaysOfWeek = add.DaysOfWeek,
DaysOfMonth = add.DaysOfMonth, DaysOfMonth = add.DaysOfMonth,
MonthsOfYear = add.MonthsOfYear MonthsOfYear = add.MonthsOfYear,
DateUpdated = now
}); });
} }
@ -65,6 +68,7 @@ public class ReplacePlayoutTemplateItemsHandler(
ex.DaysOfWeek = update.DaysOfWeek; ex.DaysOfWeek = update.DaysOfWeek;
ex.DaysOfMonth = update.DaysOfMonth; ex.DaysOfMonth = update.DaysOfMonth;
ex.MonthsOfYear = update.MonthsOfYear; ex.MonthsOfYear = update.MonthsOfYear;
ex.DateUpdated = now;
} }
} }

1
ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItemsHandler.cs

@ -24,6 +24,7 @@ public class ReplaceTemplateItemsHandler(IDbContextFactory<TvContext> dbContextF
Template template) Template template)
{ {
template.Name = request.Name; template.Name = request.Name;
template.DateUpdated = DateTime.UtcNow;
dbContext.RemoveRange(template.Items); dbContext.RemoveRange(template.Items);
template.Items = request.Items.Map(i => BuildItem(template, i)).ToList(); template.Items = request.Items.Map(i => BuildItem(template, i)).ToList();

1
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -27,6 +27,7 @@ public class PlayoutItem
public string PreferredAudioTitle { get; set; } public string PreferredAudioTitle { get; set; }
public string PreferredSubtitleLanguageCode { get; set; } public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode? SubtitleMode { get; set; } public ChannelSubtitleMode? SubtitleMode { get; set; }
public string BlockKey { get; set; }
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();

1
ErsatzTV.Core/Domain/Scheduling/Block.cs

@ -10,4 +10,5 @@ public class Block
public ICollection<BlockItem> Items { get; set; } public ICollection<BlockItem> Items { get; set; }
public ICollection<TemplateItem> TemplateItems { get; set; } public ICollection<TemplateItem> TemplateItems { get; set; }
public ICollection<PlayoutHistory> PlayoutHistory { get; set; } public ICollection<PlayoutHistory> PlayoutHistory { get; set; }
public DateTime DateUpdated { get; set; }
} }

1
ErsatzTV.Core/Domain/Scheduling/PlayoutTemplate.cs

@ -13,6 +13,7 @@ public class PlayoutTemplate
public ICollection<int> MonthsOfYear { get; set; } public ICollection<int> MonthsOfYear { get; set; }
public DateTimeOffset StartDate { get; set; } public DateTimeOffset StartDate { get; set; }
public DateTimeOffset EndDate { get; set; } public DateTimeOffset EndDate { get; set; }
public DateTime DateUpdated { get; set; }
// TODO: ICollection<DateTimeOffset> AdditionalDays { get; set; } // TODO: ICollection<DateTimeOffset> AdditionalDays { get; set; }

1
ErsatzTV.Core/Domain/Scheduling/Template.cs

@ -8,4 +8,5 @@ public class Template
public string Name { get; set; } public string Name { get; set; }
public ICollection<TemplateItem> Items { get; set; } public ICollection<TemplateItem> Items { get; set; }
public ICollection<PlayoutTemplate> PlayoutTemplates { get; set; } public ICollection<PlayoutTemplate> PlayoutTemplates { get; set; }
public DateTime DateUpdated { get; set; }
} }

175
ErsatzTV.Core/Scheduling/BlockPlayoutBuilder.cs

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
@ -17,6 +18,11 @@ public class BlockPlayoutBuilder(
ILogger<BlockPlayoutBuilder> logger) ILogger<BlockPlayoutBuilder> logger)
: IBlockPlayoutBuilder : IBlockPlayoutBuilder
{ {
private static readonly JsonSerializerSettings JsonSettings = new()
{
NullValueHandling = NullValueHandling.Ignore
};
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
{ {
logger.LogDebug( logger.LogDebug(
@ -26,6 +32,9 @@ public class BlockPlayoutBuilder(
playout.Channel.Name); playout.Channel.Name);
DateTimeOffset start = DateTimeOffset.Now; DateTimeOffset start = DateTimeOffset.Now;
DateTimeOffset lastScheduledItem = playout.Items.Count == 0
? SystemTime.MinValueUtc
: playout.Items.Max(i => i.StartOffset);
// get blocks to schedule // get blocks to schedule
List<RealBlock> blocksToSchedule = await GetBlocksToSchedule(playout, start); List<RealBlock> blocksToSchedule = await GetBlocksToSchedule(playout, start);
@ -33,20 +42,49 @@ 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);
// TODO: REMOVE THIS !!! var itemBlockKeys = new Dictionary<PlayoutItem, BlockKey>();
playout.Items.Clear(); foreach (PlayoutItem item in playout.Items)
// TODO: REMOVE THIS !!!
var historyToRemove = playout.PlayoutHistory
.Filter(h => h.When > start.UtcDateTime)
.ToList();
foreach (PlayoutHistory remove in historyToRemove)
{ {
playout.PlayoutHistory.Remove(remove); if (!string.IsNullOrWhiteSpace(item.BlockKey))
{
BlockKey blockKey = JsonConvert.DeserializeObject<BlockKey>(item.BlockKey);
itemBlockKeys.Add(item, blockKey);
}
} }
// remove items without a block key (shouldn't happen often, just upgrades)
playout.Items.RemoveAll(i => !itemBlockKeys.ContainsKey(i));
// remove playout items with block keys that aren't part of blocksToSchedule
// this could happen if block, template or playout template were updated
foreach ((PlayoutItem item, BlockKey blockKey) in itemBlockKeys)
{
if (blocksToSchedule.All(realBlock => realBlock.BlockKey != blockKey))
{
logger.LogDebug(
"Removing playout item {Title} with block key {@Key} that is no longer present",
GetDisplayTitle(item),
blockKey);
playout.Items.Remove(item);
}
}
foreach (RealBlock realBlock in blocksToSchedule) foreach (RealBlock realBlock in blocksToSchedule)
{ {
// skip blocks to schedule that are covered by existing playout items
// this means the block, template and playout template has NOT been updated
// importantly, only skip BEFORE the last scheduled playout item
if (realBlock.Start <= lastScheduledItem && itemBlockKeys.ContainsValue(realBlock.BlockKey))
{
logger.LogDebug(
"Skipping unchanged block {Block} at {Start}",
realBlock.Block.Name,
realBlock.Start);
continue;
}
logger.LogDebug( logger.LogDebug(
"Will schedule block {Block} at {Start}", "Will schedule block {Block} at {Start}",
realBlock.Block.Name, realBlock.Block.Name,
@ -62,14 +100,9 @@ public class BlockPlayoutBuilder(
continue; continue;
} }
// TODO: check if change is needed - if not, skip building // check for playout history for this collection
// - block can change
// - template can change
// - playout templates can change
// TODO: check for playout history for this collection
string historyKey = HistoryKey.ForBlockItem(blockItem); string historyKey = HistoryKey.ForBlockItem(blockItem);
logger.LogDebug("History key for block item {Item} is {Key}", blockItem.Id, historyKey); //logger.LogDebug("History key for block item {Item} is {Key}", blockItem.Id, historyKey);
DateTime historyTime = currentTime.UtcDateTime; DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
@ -144,8 +177,8 @@ public class BlockPlayoutBuilder(
logger.LogDebug("current item: {Id} / {Title}", mediaItem.Id, mediaItem is Episode e ? GetTitle(e) : string.Empty); logger.LogDebug("current item: {Id} / {Title}", mediaItem.Id, mediaItem is Episode e ? GetTitle(e) : string.Empty);
TimeSpan itemDuration = DurationForMediaItem(mediaItem); TimeSpan itemDuration = DurationForMediaItem(mediaItem);
// TODO: create a playout item // create a playout item
var playoutItem = new PlayoutItem var playoutItem = new PlayoutItem
{ {
MediaItemId = mediaItem.Id, MediaItemId = mediaItem.Id,
@ -160,11 +193,12 @@ public class BlockPlayoutBuilder(
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle, //PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, //PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
//SubtitleMode = scheduleItem.SubtitleMode //SubtitleMode = scheduleItem.SubtitleMode
BlockKey = JsonConvert.SerializeObject(realBlock.BlockKey)
}; };
playout.Items.Add(playoutItem); playout.Items.Add(playoutItem);
// TODO: create a playout history record // create a playout history record
var nextHistory = new PlayoutHistory var nextHistory = new PlayoutHistory
{ {
PlayoutId = playout.Id, PlayoutId = playout.Id,
@ -174,6 +208,7 @@ public class BlockPlayoutBuilder(
Details = HistoryDetails.ForMediaItem(mediaItem) Details = HistoryDetails.ForMediaItem(mediaItem)
}; };
//logger.LogDebug("Adding history item: {When}: {History}", nextHistory.When, nextHistory.Details);
playout.PlayoutHistory.Add(nextHistory); playout.PlayoutHistory.Add(nextHistory);
currentTime += itemDuration; currentTime += itemDuration;
@ -203,6 +238,64 @@ public class BlockPlayoutBuilder(
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}"; return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
} }
private static string GetDisplayTitle(PlayoutItem playoutItem)
{
switch (playoutItem.MediaItem)
{
case Episode e:
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty);
var episodeNumbers = e.EpisodeMetadata.Map(em => em.EpisodeNumber).ToList();
var episodeTitles = e.EpisodeMetadata.Map(em => em.Title).ToList();
if (episodeNumbers.Count == 0 || episodeTitles.Count == 0)
{
return "[unknown episode]";
}
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}";
var titlesString = $"{string.Join('/', episodeTitles)}";
if (!string.IsNullOrWhiteSpace(playoutItem.ChapterTitle))
{
titlesString += $" ({playoutItem.ChapterTitle})";
}
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
case Movie m:
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
case MusicVideo mv:
string artistName = mv.Artist.ArtistMetadata.HeadOrNone()
.Map(am => $"{am.Title} - ").IfNone(string.Empty);
return mv.MusicVideoMetadata.HeadOrNone()
.Map(mvm => $"{artistName}{mvm.Title}")
.Map(
s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
? s
: $"{s} ({playoutItem.ChapterTitle})")
.IfNone("[unknown music video]");
case OtherVideo ov:
return ov.OtherVideoMetadata.HeadOrNone()
.Map(ovm => ovm.Title ?? string.Empty)
.Map(
s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
? s
: $"{s} ({playoutItem.ChapterTitle})")
.IfNone("[unknown video]");
case Song s:
string songArtist = s.SongMetadata.HeadOrNone()
.Map(sm => string.IsNullOrWhiteSpace(sm.Artist) ? string.Empty : $"{sm.Artist} - ")
.IfNone(string.Empty);
return s.SongMetadata.HeadOrNone()
.Map(sm => $"{songArtist}{sm.Title ?? string.Empty}")
.Map(
t => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
? t
: $"{s} ({playoutItem.ChapterTitle})")
.IfNone("[unknown song]");
default:
return string.Empty;
}
}
private async Task<List<RealBlock>> GetBlocksToSchedule(Playout playout, DateTimeOffset start) private async Task<List<RealBlock>> GetBlocksToSchedule(Playout playout, DateTimeOffset start)
{ {
@ -226,6 +319,7 @@ public class BlockPlayoutBuilder(
{ {
var realBlock = new RealBlock( var realBlock = new RealBlock(
templateItem.Block, templateItem.Block,
new BlockKey(templateItem.Block, templateItem.Template, playoutTemplate),
new DateTimeOffset( new DateTimeOffset(
current.Year, current.Year,
current.Month, current.Month,
@ -243,6 +337,7 @@ public class BlockPlayoutBuilder(
} }
realBlocks.RemoveAll(b => b.Start.AddMinutes(b.Block.Minutes) < start || b.Start > finish); realBlocks.RemoveAll(b => b.Start.AddMinutes(b.Block.Minutes) < start || b.Start > finish);
realBlocks = realBlocks.OrderBy(rb => rb.Start).ToList();
return realBlocks; return realBlocks;
} }
@ -264,7 +359,7 @@ public class BlockPlayoutBuilder(
foreach ((string key, List<PlayoutHistory> group) in groups) foreach ((string key, 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);
IEnumerable<PlayoutHistory> toDelete = group IEnumerable<PlayoutHistory> toDelete = group
.Filter(h => h.When < start.UtcDateTime) .Filter(h => h.When < start.UtcDateTime)
@ -305,15 +400,36 @@ public class BlockPlayoutBuilder(
return version.Duration; return version.Duration;
} }
private record RealBlock(Block Block, DateTimeOffset Start); private record RealBlock(Block Block, BlockKey BlockKey, DateTimeOffset Start);
private static class HistoryKey [SuppressMessage("ReSharper", "InconsistentNaming")]
private record BlockKey
{ {
private static readonly JsonSerializerSettings Settings = new() public BlockKey()
{ {
NullValueHandling = NullValueHandling.Ignore }
};
public BlockKey(Block block, Template template, PlayoutTemplate playoutTemplate)
{
b = block.Id;
bt = block.DateUpdated.Ticks;
t = template.Id;
tt = template.DateUpdated.Ticks;
pt = playoutTemplate.Id;
ptt = playoutTemplate.DateUpdated.Ticks;
}
public int b { get; set; }
public int t { get; set; }
public int pt { get; set; }
public long bt { get; set; }
public long tt { get; set; }
public long ptt { get; set; }
}
private static class HistoryKey
{
public static string ForBlockItem(BlockItem blockItem) public static string ForBlockItem(BlockItem blockItem)
{ {
dynamic key = new dynamic key = new
@ -327,17 +443,12 @@ public class BlockPlayoutBuilder(
blockItem.MediaItemId blockItem.MediaItemId
}; };
return JsonConvert.SerializeObject(key, Formatting.None, Settings); return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
} }
} }
private static class HistoryDetails private static class HistoryDetails
{ {
private static readonly JsonSerializerSettings Settings = new()
{
NullValueHandling = NullValueHandling.Ignore
};
public static string ForMediaItem(MediaItem mediaItem) public static string ForMediaItem(MediaItem mediaItem)
{ {
Details details = mediaItem switch Details details = mediaItem switch
@ -346,7 +457,7 @@ public class BlockPlayoutBuilder(
_ => new Details(mediaItem.Id, null, null, null) _ => new Details(mediaItem.Id, null, null, null)
}; };
return JsonConvert.SerializeObject(details, Formatting.None, Settings); return JsonConvert.SerializeObject(details, Formatting.None, JsonSettings);
} }
private static Details ForEpisode(Episode e) private static Details ForEpisode(Episode e)

4895
ErsatzTV.Infrastructure.MySql/Migrations/20240114104626_Fix_BlockMinutes.Designer.cs generated

File diff suppressed because it is too large Load Diff

21
ErsatzTV.Infrastructure.MySql/Migrations/20240114104626_Fix_BlockMinutes.cs

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Fix_BlockMinutes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"UPDATE Block SET Minutes = CAST(CEILING(Minutes / 15.0) * 15 AS INT)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

4907
ErsatzTV.Infrastructure.MySql/Migrations/20240114120807_Add_PlayoutItemBlockKey.Designer.cs generated

File diff suppressed because it is too large Load Diff

63
ErsatzTV.Infrastructure.MySql/Migrations/20240114120807_Add_PlayoutItemBlockKey.cs

@ -0,0 +1,63 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutItemBlockKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdated",
table: "Template",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdated",
table: "PlayoutTemplate",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<string>(
name: "BlockKey",
table: "PlayoutItem",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdated",
table: "Block",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DateUpdated",
table: "Template");
migrationBuilder.DropColumn(
name: "DateUpdated",
table: "PlayoutTemplate");
migrationBuilder.DropColumn(
name: "BlockKey",
table: "PlayoutItem");
migrationBuilder.DropColumn(
name: "DateUpdated",
table: "Block");
}
}
}

4907
ErsatzTV.Infrastructure.MySql/Migrations/20240114120924_Reset_BlockPlayouts_BlockKey.Designer.cs generated

File diff suppressed because it is too large Load Diff

26
ErsatzTV.Infrastructure.MySql/Migrations/20240114120924_Reset_BlockPlayouts_BlockKey.cs

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Reset_BlockPlayouts_BlockKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"DELETE FROM PlayoutHistory");
migrationBuilder.Sql(
"""
DELETE FROM PlayoutItem
WHERE PlayoutId IN (SELECT Id FROM Playout WHERE ProgramSchedulePlayoutType = 2)
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

12
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -1437,6 +1437,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("BlockKey")
.HasColumnType("longtext");
b.Property<string>("ChapterTitle") b.Property<string>("ChapterTitle")
.HasColumnType("longtext"); .HasColumnType("longtext");
@ -1829,6 +1832,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("BlockGroupId") b.Property<int>("BlockGroupId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime>("DateUpdated")
.HasColumnType("datetime(6)");
b.Property<int>("Minutes") b.Property<int>("Minutes")
.HasColumnType("int"); .HasColumnType("int");
@ -1943,6 +1949,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime>("DateUpdated")
.HasColumnType("datetime(6)");
b.Property<string>("DaysOfMonth") b.Property<string>("DaysOfMonth")
.HasColumnType("longtext"); .HasColumnType("longtext");
@ -1982,6 +1991,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime>("DateUpdated")
.HasColumnType("datetime(6)");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("varchar(255)"); .HasColumnType("varchar(255)");

4893
ErsatzTV.Infrastructure.Sqlite/Migrations/20240114103704_Fix_BlockMinutes.Designer.cs generated

File diff suppressed because it is too large Load Diff

21
ErsatzTV.Infrastructure.Sqlite/Migrations/20240114103704_Fix_BlockMinutes.cs

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Fix_BlockMinutes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"UPDATE Block SET Minutes = CAST(CEILING(Minutes / 15.0) * 15 AS INT)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

4905
ErsatzTV.Infrastructure.Sqlite/Migrations/20240114113426_Add_PlayoutItemBlockKey.Designer.cs generated

File diff suppressed because it is too large Load Diff

62
ErsatzTV.Infrastructure.Sqlite/Migrations/20240114113426_Add_PlayoutItemBlockKey.cs

@ -0,0 +1,62 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutItemBlockKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdated",
table: "Template",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdated",
table: "PlayoutTemplate",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<string>(
name: "BlockKey",
table: "PlayoutItem",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdated",
table: "Block",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DateUpdated",
table: "Template");
migrationBuilder.DropColumn(
name: "DateUpdated",
table: "PlayoutTemplate");
migrationBuilder.DropColumn(
name: "BlockKey",
table: "PlayoutItem");
migrationBuilder.DropColumn(
name: "DateUpdated",
table: "Block");
}
}
}

4905
ErsatzTV.Infrastructure.Sqlite/Migrations/20240114120205_Reset_BlockPlayouts_BlockKey.Designer.cs generated

File diff suppressed because it is too large Load Diff

26
ErsatzTV.Infrastructure.Sqlite/Migrations/20240114120205_Reset_BlockPlayouts_BlockKey.cs

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Reset_BlockPlayouts_BlockKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"DELETE FROM PlayoutHistory");
migrationBuilder.Sql(
"""
DELETE FROM PlayoutItem
WHERE PlayoutId IN (SELECT Id FROM Playout WHERE ProgramSchedulePlayoutType = 2)
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

38
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -715,7 +715,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("SongMetadataId"); b.HasIndex("SongMetadataId");
b.ToTable("Genre", (string)null); b.ToTable("Genre");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b => modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b =>
@ -1168,7 +1168,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("ArtistMetadataId"); b.HasIndex("ArtistMetadataId");
b.ToTable("Mood", (string)null); b.ToTable("Mood");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b =>
@ -1276,7 +1276,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("SmartCollectionId"); b.HasIndex("SmartCollectionId");
b.ToTable("MultiCollectionSmartItem", (string)null); b.ToTable("MultiCollectionSmartItem");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoArtist", b => modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoArtist", b =>
@ -1295,7 +1295,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("MusicVideoMetadataId"); b.HasIndex("MusicVideoMetadataId");
b.ToTable("MusicVideoArtist", (string)null); b.ToTable("MusicVideoArtist");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b =>
@ -1435,6 +1435,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("BlockKey")
.HasColumnType("TEXT");
b.Property<string>("ChapterTitle") b.Property<string>("ChapterTitle")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -1827,6 +1830,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("BlockGroupId") b.Property<int>("BlockGroupId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<DateTime>("DateUpdated")
.HasColumnType("TEXT");
b.Property<int>("Minutes") b.Property<int>("Minutes")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1923,7 +1929,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("PlayoutId") b.Property<int>("PlayoutId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<DateTimeOffset>("When") b.Property<DateTime>("When")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");
@ -1941,6 +1947,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<DateTime>("DateUpdated")
.HasColumnType("TEXT");
b.Property<string>("DaysOfMonth") b.Property<string>("DaysOfMonth")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -1980,6 +1989,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<DateTime>("DateUpdated")
.HasColumnType("TEXT");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -2274,7 +2286,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("ArtistMetadataId"); b.HasIndex("ArtistMetadataId");
b.ToTable("Style", (string)null); b.ToTable("Style");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.Subtitle", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Subtitle", b =>
@ -2422,7 +2434,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("SongMetadataId"); b.HasIndex("SongMetadataId");
b.ToTable("Tag", (string)null); b.ToTable("Tag");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b =>
@ -2563,7 +2575,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("EmbyLibraryId"); b.HasIndex("EmbyLibraryId");
b.ToTable("EmbyPathInfo", (string)null); b.ToTable("EmbyPathInfo");
}); });
modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b => modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b =>
@ -2585,7 +2597,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("JellyfinLibraryId"); b.HasIndex("JellyfinLibraryId");
b.ToTable("JellyfinPathInfo", (string)null); b.ToTable("JellyfinPathInfo");
}); });
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b =>
@ -3545,7 +3557,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.HasForeignKey("ProgramScheduleId") .HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.OwnsOne("ErsatzTV.Core.Domain.Playout.Anchor#ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 =>
{ {
b1.Property<int>("PlayoutId") b1.Property<int>("PlayoutId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -3575,7 +3587,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b1.WithOwner() b1.WithOwner()
.HasForeignKey("PlayoutId"); .HasForeignKey("PlayoutId");
b1.OwnsOne("ErsatzTV.Core.Domain.Playout.Anchor#ErsatzTV.Core.Domain.PlayoutAnchor.ScheduleItemsEnumeratorState#ErsatzTV.Core.Domain.CollectionEnumeratorState", "ScheduleItemsEnumeratorState", b2 => b1.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "ScheduleItemsEnumeratorState", b2 =>
{ {
b2.Property<int>("PlayoutAnchorPlayoutId") b2.Property<int>("PlayoutAnchorPlayoutId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -3658,7 +3670,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.HasForeignKey("SmartCollectionId") .HasForeignKey("SmartCollectionId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor.EnumeratorState#ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 =>
{ {
b1.Property<int>("PlayoutProgramScheduleAnchorId") b1.Property<int>("PlayoutProgramScheduleAnchorId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -3704,7 +3716,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutScheduleItemFillGroupIndex.EnumeratorState#ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 =>
{ {
b1.Property<int>("PlayoutScheduleItemFillGroupIndexId") b1.Property<int>("PlayoutScheduleItemFillGroupIndexId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

33
ErsatzTV/Pages/BlockEditor.razor

@ -15,10 +15,23 @@
<MudCard> <MudCard>
<MudCardContent> <MudCardContent>
<MudTextField Label="Name" @bind-Value="_block.Name" For="@(() => _block.Name)"/> <MudTextField Label="Name" @bind-Value="_block.Name" For="@(() => _block.Name)"/>
<MudTextField Label="Duration" @bind-Value="_block.Minutes" <MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
For="@(() => _block.Minutes)" <MudItem xs="6">
Adornment="Adornment.End" <MudTextField T="int"
AdornmentText="minutes" /> Label="Duration"
@bind-Value="_durationHours"
Adornment="Adornment.End"
AdornmentText="hours"/>
</MudItem>
<MudItem xs="6">
<MudSelect T="int" @bind-Value="_durationMinutes" Adornment="Adornment.End" AdornmentText="minutes">
<MudSelectItem Value="0" />
<MudSelectItem Value="15" />
<MudSelectItem Value="30" />
<MudSelectItem Value="45" />
</MudSelect>
</MudItem>
</MudGrid>
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
</div> </div>
@ -202,8 +215,10 @@
[Parameter] [Parameter]
public int Id { get; set; } public int Id { get; set; }
private BlockItemsEditViewModel _block = new(); private BlockItemsEditViewModel _block = new() { Items = [] };
private BlockItemEditViewModel _selectedItem; private BlockItemEditViewModel _selectedItem;
private int _durationHours = 0;
private int _durationMinutes = 15;
public void Dispose() public void Dispose()
{ {
@ -230,6 +245,9 @@
Minutes = block.Minutes, Minutes = block.Minutes,
Items = [] Items = []
}; };
_durationHours = _block.Minutes / 60;
_durationMinutes = _block.Minutes % 60;
} }
Option<IEnumerable<BlockItemViewModel>> maybeResults = await Mediator.Send(new GetBlockItems(Id), _cts.Token); Option<IEnumerable<BlockItemViewModel>> maybeResults = await Mediator.Send(new GetBlockItems(Id), _cts.Token);
@ -361,7 +379,10 @@
item.MediaItem?.MediaItemId, item.MediaItem?.MediaItemId,
item.PlaybackOrder)).ToList(); item.PlaybackOrder)).ToList();
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceBlockItems(Id, _block.Name, _block.Minutes, items), _cts.Token) _block.Minutes = _durationHours * 60 + _durationMinutes;
Seq<BaseError> errorMessages = await Mediator
.Send(new ReplaceBlockItems(Id, _block.Name, _block.Minutes, items), _cts.Token)
.Map(e => e.LeftToSeq()); .Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match( errorMessages.HeadOrNone().Match(

39
ErsatzTV/Pages/Blocks.razor

@ -5,6 +5,7 @@
@inject ILogger<Blocks> Logger @inject ILogger<Blocks> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Blocks</MudText> <MudText Typo="Typo.h4" Class="mb-4">Blocks</MudText>
@ -53,7 +54,7 @@
<BodyContent> <BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%"> <div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart"> <MudGrid Justify="Justify.FlexStart">
<MudItem xs="3"> <MudItem xs="5">
<MudText>@item.Text</MudText> <MudText>@item.Text</MudText>
</MudItem> </MudItem>
@if (!string.IsNullOrWhiteSpace(item.EndText)) @if (!string.IsNullOrWhiteSpace(item.EndText))
@ -172,26 +173,38 @@
{ {
foreach (int blockGroupId in Optional(treeItem.BlockGroupId)) foreach (int blockGroupId in Optional(treeItem.BlockGroupId))
{ {
// TODO: confirmation var parameters = new DialogParameters { { "EntityType", "block group" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
await Mediator.Send(new DeleteBlockGroup(blockGroupId), _cts.Token);
TreeItems.RemoveWhere(i => i.BlockGroupId == blockGroupId); IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Block Group", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
{
await Mediator.Send(new DeleteBlockGroup(blockGroupId), _cts.Token);
TreeItems.RemoveWhere(i => i.BlockGroupId == blockGroupId);
_blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token); _blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
}
} }
foreach (int blockId in Optional(treeItem.BlockId)) foreach (int blockId in Optional(treeItem.BlockId))
{ {
// TODO: confirmation var parameters = new DialogParameters { { "EntityType", "block" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
await Mediator.Send(new DeleteBlock(blockId), _cts.Token); IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Block", parameters, options);
foreach (BlockTreeItemViewModel parent in TreeItems) DialogResult result = await dialog.Result;
if (!result.Canceled)
{ {
parent.TreeItems.Remove(treeItem); await Mediator.Send(new DeleteBlock(blockId), _cts.Token);
} foreach (BlockTreeItemViewModel parent in TreeItems)
{
parent.TreeItems.Remove(treeItem);
}
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
}
} }
} }
} }

39
ErsatzTV/Pages/Templates.razor

@ -5,6 +5,7 @@
@inject ILogger<Templates> Logger @inject ILogger<Templates> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Templates</MudText> <MudText Typo="Typo.h4" Class="mb-4">Templates</MudText>
@ -166,26 +167,38 @@
{ {
foreach (int templateGroupId in Optional(treeItem.TemplateGroupId)) foreach (int templateGroupId in Optional(treeItem.TemplateGroupId))
{ {
// TODO: confirmation var parameters = new DialogParameters { { "EntityType", "template group" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
await Mediator.Send(new DeleteTemplateGroup(templateGroupId), _cts.Token);
TreeItems.RemoveWhere(i => i.TemplateGroupId == templateGroupId); IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Template Group", parameters, options);
DialogResult result = await dialog.Result;
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token); if (!result.Canceled)
await InvokeAsync(StateHasChanged); {
await Mediator.Send(new DeleteTemplateGroup(templateGroupId), _cts.Token);
TreeItems.RemoveWhere(i => i.TemplateGroupId == templateGroupId);
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token);
await InvokeAsync(StateHasChanged);
}
} }
foreach (int templateId in Optional(treeItem.TemplateId)) foreach (int templateId in Optional(treeItem.TemplateId))
{ {
// TODO: confirmation var parameters = new DialogParameters { { "EntityType", "template" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
await Mediator.Send(new DeleteTemplate(templateId), _cts.Token); IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Template", parameters, options);
foreach (TemplateTreeItemViewModel parent in TreeItems) DialogResult result = await dialog.Result;
if (!result.Canceled)
{ {
parent.TreeItems.Remove(treeItem); await Mediator.Send(new DeleteTemplate(templateId), _cts.Token);
} foreach (TemplateTreeItemViewModel parent in TreeItems)
{
parent.TreeItems.Remove(treeItem);
}
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
}
} }
} }
} }

15
ErsatzTV/ViewModels/BlockTreeItemViewModel.cs

@ -19,7 +19,20 @@ public class BlockTreeItemViewModel
public BlockTreeItemViewModel(BlockViewModel block) public BlockTreeItemViewModel(BlockViewModel block)
{ {
Text = block.Name; Text = block.Name;
EndText = $"{block.Minutes} minutes"; if (block.Minutes / 60 >= 1)
{
string plural = block.Minutes / 60 >= 2 ? "s" : string.Empty;
EndText = $"{block.Minutes / 60} hour{plural}";
if (block.Minutes % 60 != 0)
{
EndText += $", {block.Minutes % 60} minutes";
}
}
else
{
EndText = $"{block.Minutes} minutes";
}
TreeItems = []; TreeItems = [];
CanExpand = false; CanExpand = false;
BlockId = block.Id; BlockId = block.Id;

Loading…
Cancel
Save