add deco system (#1665)
* add deco groups and decos; set default deco for block playout * use block playout default deco for watermark * add deco templates, groups and deco template editor * associate deco template with playout template * use deco template item watermark for playback * update changelog for decos
This commit is contained in:
@@ -8,6 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Add `Active Date Range` to block playout template editor to allow limiting templates to a specific date range
|
||||
- This is year-agnostic, meaning the Month/Day range will apply to every year
|
||||
- This also supports wrapping the end of the year (e.g., start 12/1 and end 1/15)
|
||||
- Add new `Deco` system for "decorating" channels with non-primary content
|
||||
- Decos currently only contain Watermarks, but future work will add other functionality, including filler
|
||||
- Similar to blocks, decos have deco groups for organization
|
||||
- Similar to blocks, decos have deco templates for filling a "day" with decos
|
||||
- In the playout template editor, playout template items can have *both* a block template and a deco template
|
||||
- This allows watermarks to change at different times than primary content
|
||||
- Block playouts can also have a default deco
|
||||
- This will apply whenever a deco template is missing, or when a deco template item cannot be found for the current time
|
||||
- Effectively, this sets a default watermark for the entire playout
|
||||
|
||||
## [0.8.6-beta] - 2024-04-03
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record CreateDeco(int DecoGroupId, string Name) : IRequest<Either<BaseError, DecoViewModel>>;
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record CreateDecoGroup(string Name) : IRequest<Either<BaseError, DecoGroupViewModel>>;
|
||||
@@ -0,0 +1,33 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class CreateDecoGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<CreateDecoGroup, Either<BaseError, DecoGroupViewModel>>
|
||||
{
|
||||
public async Task<Either<BaseError, DecoGroupViewModel>> Handle(
|
||||
CreateDecoGroup request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, DecoGroup> validation = await Validate(request);
|
||||
return await validation.Apply(profile => PersistDecoGroup(dbContext, profile));
|
||||
}
|
||||
|
||||
private static async Task<DecoGroupViewModel> PersistDecoGroup(TvContext dbContext, DecoGroup decoGroup)
|
||||
{
|
||||
await dbContext.DecoGroups.AddAsync(decoGroup);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return Mapper.ProjectToViewModel(decoGroup);
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, DecoGroup>> Validate(CreateDecoGroup request) =>
|
||||
Task.FromResult(ValidateName(request).Map(name => new DecoGroup { Name = name, Decos = [] }));
|
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateDecoGroup createDecoGroup) =>
|
||||
createDecoGroup.NotEmpty(x => x.Name)
|
||||
.Bind(_ => createDecoGroup.NotLongerThan(50)(x => x.Name));
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<CreateDeco, Either<BaseError, DecoViewModel>>
|
||||
{
|
||||
public async Task<Either<BaseError, DecoViewModel>> Handle(
|
||||
CreateDeco request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Deco> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(profile => PersistDeco(dbContext, profile));
|
||||
}
|
||||
|
||||
private static async Task<DecoViewModel> PersistDeco(TvContext dbContext, Deco deco)
|
||||
{
|
||||
await dbContext.Decos.AddAsync(deco);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return Mapper.ProjectToViewModel(deco);
|
||||
}
|
||||
|
||||
private static async Task<Validation<BaseError, Deco>> Validate(TvContext dbContext, CreateDeco request) =>
|
||||
await ValidateDecoName(dbContext, request).MapT(
|
||||
name => new Deco
|
||||
{
|
||||
DecoGroupId = request.DecoGroupId,
|
||||
Name = name
|
||||
});
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidateDecoName(
|
||||
TvContext dbContext,
|
||||
CreateDeco request)
|
||||
{
|
||||
if (request.Name.Length > 50)
|
||||
{
|
||||
return BaseError.New($"Deco name \"{request.Name}\" is invalid");
|
||||
}
|
||||
|
||||
Option<Deco> maybeExisting = await dbContext.Decos
|
||||
.FirstOrDefaultAsync(r => r.DecoGroupId == request.DecoGroupId && r.Name == request.Name)
|
||||
.Map(Optional);
|
||||
|
||||
return maybeExisting.IsSome
|
||||
? BaseError.New($"A deco named \"{request.Name}\" already exists in that deco group")
|
||||
: Success<BaseError, string>(request.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record CreateDecoTemplate(int DecoTemplateGroupId, string Name) : IRequest<Either<BaseError, DecoTemplateViewModel>>;
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record CreateDecoTemplateGroup(string Name) : IRequest<Either<BaseError, DecoTemplateGroupViewModel>>;
|
||||
@@ -0,0 +1,35 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class CreateDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<CreateDecoTemplateGroup, Either<BaseError, DecoTemplateGroupViewModel>>
|
||||
{
|
||||
public async Task<Either<BaseError, DecoTemplateGroupViewModel>> Handle(
|
||||
CreateDecoTemplateGroup request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, DecoTemplateGroup> validation = await Validate(request);
|
||||
return await validation.Apply(profile => PersistDecoTemplateGroup(dbContext, profile));
|
||||
}
|
||||
|
||||
private static async Task<DecoTemplateGroupViewModel> PersistDecoTemplateGroup(
|
||||
TvContext dbContext,
|
||||
DecoTemplateGroup decoDecoTemplateGroup)
|
||||
{
|
||||
await dbContext.DecoTemplateGroups.AddAsync(decoDecoTemplateGroup);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return Mapper.ProjectToViewModel(decoDecoTemplateGroup);
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, DecoTemplateGroup>> Validate(CreateDecoTemplateGroup request) =>
|
||||
Task.FromResult(ValidateName(request).Map(name => new DecoTemplateGroup { Name = name, DecoTemplates = [] }));
|
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateDecoTemplateGroup createDecoTemplateGroup) =>
|
||||
createDecoTemplateGroup.NotEmpty(x => x.Name)
|
||||
.Bind(_ => createDecoTemplateGroup.NotLongerThan(50)(x => x.Name));
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class CreateDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<CreateDecoTemplate, Either<BaseError, DecoTemplateViewModel>>
|
||||
{
|
||||
public async Task<Either<BaseError, DecoTemplateViewModel>> Handle(
|
||||
CreateDecoTemplate request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, DecoTemplate> validation = await Validate(request);
|
||||
return await validation.Apply(profile => PersistDecoTemplate(dbContext, profile));
|
||||
}
|
||||
|
||||
private static async Task<DecoTemplateViewModel> PersistDecoTemplate(TvContext dbContext, DecoTemplate decoTemplate)
|
||||
{
|
||||
await dbContext.DecoTemplates.AddAsync(decoTemplate);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return Mapper.ProjectToViewModel(decoTemplate);
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, DecoTemplate>> Validate(CreateDecoTemplate request) =>
|
||||
Task.FromResult(
|
||||
ValidateName(request).Map(
|
||||
name => new DecoTemplate
|
||||
{
|
||||
DecoTemplateGroupId = request.DecoTemplateGroupId,
|
||||
Name = name
|
||||
}));
|
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateDecoTemplate createDecoTemplate) =>
|
||||
createDecoTemplate.NotEmpty(x => x.Name)
|
||||
.Bind(_ => createDecoTemplate.NotLongerThan(50)(x => x.Name));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DeleteDeco(int DecoId) : IRequest<Option<BaseError>>;
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DeleteDecoGroup(int DecoGroupId) : IRequest<Option<BaseError>>;
|
||||
@@ -0,0 +1,29 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class DeleteDecoGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<DeleteDecoGroup, Option<BaseError>>
|
||||
{
|
||||
public async Task<Option<BaseError>> Handle(DeleteDecoGroup request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Option<DecoGroup> maybeDecoGroup = await dbContext.DecoGroups
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.DecoGroupId);
|
||||
|
||||
foreach (DecoGroup decoGroup in maybeDecoGroup)
|
||||
{
|
||||
dbContext.DecoGroups.Remove(decoGroup);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return maybeDecoGroup.Match(
|
||||
_ => Option<BaseError>.None,
|
||||
() => BaseError.New($"DecoGroup {request.DecoGroupId} does not exist."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class DeleteDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<DeleteDeco, Option<BaseError>>
|
||||
{
|
||||
public async Task<Option<BaseError>> Handle(DeleteDeco request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Option<Deco> maybeDeco = await dbContext.Decos
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.DecoId);
|
||||
|
||||
foreach (Deco deco in maybeDeco)
|
||||
{
|
||||
dbContext.Decos.Remove(deco);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return maybeDeco.Match(
|
||||
_ => Option<BaseError>.None,
|
||||
() => BaseError.New($"Deco {request.DecoId} does not exist."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DeleteDecoTemplate(int DecoTemplateId) : IRequest<Option<BaseError>>;
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DeleteDecoTemplateGroup(int DecoTemplateGroupId) : IRequest<Option<BaseError>>;
|
||||
@@ -0,0 +1,29 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class DeleteDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<DeleteDecoTemplateGroup, Option<BaseError>>
|
||||
{
|
||||
public async Task<Option<BaseError>> Handle(DeleteDecoTemplateGroup request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Option<DecoTemplateGroup> maybeDecoTemplateGroup = await dbContext.DecoTemplateGroups
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.DecoTemplateGroupId);
|
||||
|
||||
foreach (DecoTemplateGroup decoTemplateGroup in maybeDecoTemplateGroup)
|
||||
{
|
||||
dbContext.DecoTemplateGroups.Remove(decoTemplateGroup);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return maybeDecoTemplateGroup.Match(
|
||||
_ => Option<BaseError>.None,
|
||||
() => BaseError.New($"DecoTemplateGroup {request.DecoTemplateGroupId} does not exist."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class DeleteDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<DeleteDecoTemplate, Option<BaseError>>
|
||||
{
|
||||
public async Task<Option<BaseError>> Handle(DeleteDecoTemplate request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Option<DecoTemplate> maybeDecoTemplate = await dbContext.DecoTemplates
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.DecoTemplateId);
|
||||
|
||||
foreach (DecoTemplate decoTemplate in maybeDecoTemplate)
|
||||
{
|
||||
dbContext.DecoTemplates.Remove(decoTemplate);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return maybeDecoTemplate.Match(
|
||||
_ => Option<BaseError>.None,
|
||||
() => BaseError.New($"DecoTemplate {request.DecoTemplateId} does not exist."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record ReplaceDecoTemplateItem(int DecoId, TimeSpan StartTime, TimeSpan EndTime);
|
||||
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record ReplaceDecoTemplateItems(int DecoTemplateId, string Name, List<ReplaceDecoTemplateItem> Items)
|
||||
: IRequest<Either<BaseError, List<DecoTemplateItemViewModel>>>;
|
||||
@@ -0,0 +1,66 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class ReplaceDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<ReplaceDecoTemplateItems, Either<BaseError, List<DecoTemplateItemViewModel>>>
|
||||
{
|
||||
public async Task<Either<BaseError, List<DecoTemplateItemViewModel>>> Handle(
|
||||
ReplaceDecoTemplateItems request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, DecoTemplate> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(ps => Persist(dbContext, request, ps));
|
||||
}
|
||||
|
||||
private static async Task<List<DecoTemplateItemViewModel>> Persist(
|
||||
TvContext dbContext,
|
||||
ReplaceDecoTemplateItems request,
|
||||
DecoTemplate decoTemplate)
|
||||
{
|
||||
decoTemplate.Name = request.Name;
|
||||
decoTemplate.DateUpdated = DateTime.UtcNow;
|
||||
|
||||
dbContext.RemoveRange(decoTemplate.Items);
|
||||
decoTemplate.Items = request.Items.Map(i => BuildItem(decoTemplate, i)).ToList();
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// TODO: refresh any playouts that use this schedule
|
||||
// foreach (Playout playout in programSchedule.Playouts)
|
||||
// {
|
||||
// await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh));
|
||||
// }
|
||||
|
||||
await dbContext.Entry(decoTemplate)
|
||||
.Collection(t => t.Items)
|
||||
.Query()
|
||||
.Include(i => i.Deco)
|
||||
.LoadAsync();
|
||||
|
||||
return decoTemplate.Items.Map(Mapper.ProjectToViewModel).ToList();
|
||||
}
|
||||
|
||||
private static DecoTemplateItem BuildItem(DecoTemplate decoTemplate, ReplaceDecoTemplateItem item) =>
|
||||
new()
|
||||
{
|
||||
DecoTemplateId = decoTemplate.Id,
|
||||
DecoId = item.DecoId,
|
||||
StartTime = item.StartTime,
|
||||
EndTime = item.EndTime
|
||||
};
|
||||
|
||||
private static Task<Validation<BaseError, DecoTemplate>> Validate(TvContext dbContext, ReplaceDecoTemplateItems request) =>
|
||||
DecoTemplateMustExist(dbContext, request.DecoTemplateId);
|
||||
|
||||
private static Task<Validation<BaseError, DecoTemplate>> DecoTemplateMustExist(TvContext dbContext, int decoTemplateId) =>
|
||||
dbContext.DecoTemplates
|
||||
.Include(b => b.Items)
|
||||
.SelectOneAsync(b => b.Id, b => b.Id == decoTemplateId)
|
||||
.Map(o => o.ToValidation<BaseError>("[DecoTemplateId] does not exist."));
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public record ReplacePlayoutTemplate(
|
||||
int Id,
|
||||
int Index,
|
||||
int TemplateId,
|
||||
int? DecoTemplateId,
|
||||
List<DayOfWeek> DaysOfWeek,
|
||||
List<int> DaysOfMonth,
|
||||
List<int> MonthsOfYear,
|
||||
|
||||
@@ -52,6 +52,7 @@ public class ReplacePlayoutTemplateItemsHandler(
|
||||
PlayoutId = playout.Id,
|
||||
Index = add.Index,
|
||||
TemplateId = add.TemplateId,
|
||||
DecoTemplateId = add.DecoTemplateId,
|
||||
DaysOfWeek = add.DaysOfWeek,
|
||||
DaysOfMonth = add.DaysOfMonth,
|
||||
MonthsOfYear = add.MonthsOfYear,
|
||||
@@ -70,6 +71,7 @@ public class ReplacePlayoutTemplateItemsHandler(
|
||||
{
|
||||
ex.Index = update.Index;
|
||||
ex.TemplateId = update.TemplateId;
|
||||
ex.DecoTemplateId = update.DecoTemplateId;
|
||||
ex.DaysOfWeek = update.DaysOfWeek;
|
||||
ex.DaysOfMonth = update.DaysOfMonth;
|
||||
ex.MonthsOfYear = update.MonthsOfYear;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record UpdateDeco(int DecoId, int DecoGroupId, string Name, int? WatermarkId) : IRequest<Either<BaseError, DecoViewModel>>;
|
||||
@@ -0,0 +1,61 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<UpdateDeco, Either<BaseError, DecoViewModel>>
|
||||
{
|
||||
public async Task<Either<BaseError, DecoViewModel>> Handle(UpdateDeco request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Deco> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
|
||||
}
|
||||
|
||||
private static async Task<DecoViewModel> ApplyUpdateRequest(
|
||||
TvContext dbContext,
|
||||
Deco existing,
|
||||
UpdateDeco request)
|
||||
{
|
||||
existing.Name = request.Name;
|
||||
existing.WatermarkId = request.WatermarkId;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return Mapper.ProjectToViewModel(existing);
|
||||
}
|
||||
|
||||
private static async Task<Validation<BaseError, Deco>> Validate(TvContext dbContext, UpdateDeco request) =>
|
||||
(await DecoMustExist(dbContext, request), await ValidateDecoName(dbContext, request))
|
||||
.Apply((deco, _) => deco);
|
||||
|
||||
private static Task<Validation<BaseError, Deco>> DecoMustExist(
|
||||
TvContext dbContext,
|
||||
UpdateDeco request) =>
|
||||
dbContext.Decos
|
||||
.SelectOneAsync(d => d.Id, d => d.Id == request.DecoId)
|
||||
.Map(o => o.ToValidation<BaseError>("Deco does not exist"));
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidateDecoName(
|
||||
TvContext dbContext,
|
||||
UpdateDeco request)
|
||||
{
|
||||
if (request.Name.Length > 50)
|
||||
{
|
||||
return BaseError.New($"Deco name \"{request.Name}\" is invalid");
|
||||
}
|
||||
|
||||
Option<Deco> maybeExisting = await dbContext.Decos
|
||||
.FirstOrDefaultAsync(
|
||||
d => d.Id != request.DecoId && d.DecoGroupId == request.DecoGroupId && d.Name == request.Name)
|
||||
.Map(Optional);
|
||||
|
||||
return maybeExisting.IsSome
|
||||
? BaseError.New($"A deco named \"{request.Name}\" already exists in that deco group")
|
||||
: Success<BaseError, string>(request.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record UpdateDefaultDeco(int PlayoutId, int? DecoId) : IRequest;
|
||||
@@ -0,0 +1,16 @@
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class UpdateDefaultDecoHandler(IDbContextFactory<TvContext> dbContextFactory) : IRequestHandler<UpdateDefaultDeco>
|
||||
{
|
||||
public async Task Handle(UpdateDefaultDeco request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
await dbContext.Playouts
|
||||
.Where(p => p.Id == request.PlayoutId)
|
||||
.ExecuteUpdateAsync(u => u.SetProperty(p => p.DecoId, p => request.DecoId), cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DecoGroupViewModel(int Id, string Name, int DecoCount);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DecoTemplateGroupViewModel(int Id, string Name, int DecoTemplateCount);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DecoTemplateItemViewModel(int DecoId, string DecoName, DateTime StartTime, DateTime EndTime);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DecoTemplateViewModel(int Id, int DecoTemplateGroupId, string Name);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record DecoViewModel(int Id, int DecoGroupId, string Name, int? WatermarkId);
|
||||
@@ -46,10 +46,37 @@ internal static class Mapper
|
||||
return new TemplateItemViewModel(templateItem.BlockId, templateItem.Block.Name, startTime, endTime);
|
||||
}
|
||||
|
||||
internal static DecoGroupViewModel ProjectToViewModel(DecoGroup decoGroup) =>
|
||||
new(decoGroup.Id, decoGroup.Name, decoGroup.Decos.Count);
|
||||
|
||||
internal static DecoViewModel ProjectToViewModel(Deco deco) =>
|
||||
new(deco.Id, deco.DecoGroupId, deco.Name, deco.WatermarkId);
|
||||
|
||||
internal static DecoTemplateGroupViewModel ProjectToViewModel(DecoTemplateGroup decoTemplateGroup) =>
|
||||
new(decoTemplateGroup.Id, decoTemplateGroup.Name, decoTemplateGroup.DecoTemplates.Count);
|
||||
|
||||
internal static DecoTemplateViewModel ProjectToViewModel(DecoTemplate decoTemplate)
|
||||
{
|
||||
if (decoTemplate is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DecoTemplateViewModel(decoTemplate.Id, decoTemplate.DecoTemplateGroupId, decoTemplate.Name);
|
||||
}
|
||||
|
||||
internal static DecoTemplateItemViewModel ProjectToViewModel(DecoTemplateItem decoTemplateItem)
|
||||
{
|
||||
DateTime startTime = DateTime.Today.Add(decoTemplateItem.StartTime);
|
||||
DateTime endTime = DateTime.Today.Add(decoTemplateItem.EndTime);
|
||||
return new DecoTemplateItemViewModel(decoTemplateItem.DecoId, decoTemplateItem.Deco.Name, startTime, endTime);
|
||||
}
|
||||
|
||||
internal static PlayoutTemplateViewModel ProjectToViewModel(PlayoutTemplate playoutTemplate) =>
|
||||
new(
|
||||
playoutTemplate.Id,
|
||||
ProjectToViewModel(playoutTemplate.Template),
|
||||
ProjectToViewModel(playoutTemplate.DecoTemplate),
|
||||
playoutTemplate.Index,
|
||||
playoutTemplate.DaysOfWeek,
|
||||
playoutTemplate.DaysOfMonth,
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace ErsatzTV.Application.Scheduling;
|
||||
public record PlayoutTemplateViewModel(
|
||||
int Id,
|
||||
TemplateViewModel Template,
|
||||
DecoTemplateViewModel DecoTemplate,
|
||||
int Index,
|
||||
ICollection<DayOfWeek> DaysOfWeek,
|
||||
ICollection<int> DaysOfMonth,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record GetAllDecoGroups : IRequest<List<DecoGroupViewModel>>;
|
||||
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class GetAllDecoGroupsHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetAllDecoGroups, List<DecoGroupViewModel>>
|
||||
{
|
||||
public async Task<List<DecoGroupViewModel>> Handle(GetAllDecoGroups request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
List<DecoGroup> decoGroups = await dbContext.DecoGroups
|
||||
.AsNoTracking()
|
||||
.Include(g => g.Decos)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return decoGroups.Map(Mapper.ProjectToViewModel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record GetAllDecoTemplateGroups : IRequest<List<DecoTemplateGroupViewModel>>;
|
||||
@@ -0,0 +1,23 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class GetAllDecoTemplateGroupsHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetAllDecoTemplateGroups, List<DecoTemplateGroupViewModel>>
|
||||
{
|
||||
public async Task<List<DecoTemplateGroupViewModel>> Handle(
|
||||
GetAllDecoTemplateGroups request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
List<DecoTemplateGroup> blockGroups = await dbContext.DecoTemplateGroups
|
||||
.AsNoTracking()
|
||||
.Include(g => g.DecoTemplates)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return blockGroups.Map(Mapper.ProjectToViewModel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record GetDecoById(int DecoId) : IRequest<Option<DecoViewModel>>;
|
||||
@@ -0,0 +1,17 @@
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class GetDecoByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetDecoById, Option<DecoViewModel>>
|
||||
{
|
||||
public async Task<Option<DecoViewModel>> Handle(GetDecoById request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
return await dbContext.Decos
|
||||
.SelectOneAsync(b => b.Id, b => b.Id == request.DecoId)
|
||||
.MapT(Mapper.ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record GetDecoByPlayoutId(int PlayoutId) : IRequest<Option<DecoViewModel>>;
|
||||
@@ -0,0 +1,18 @@
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class GetDecoByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetDecoByPlayoutId, Option<DecoViewModel>>
|
||||
{
|
||||
public async Task<Option<DecoViewModel>> Handle(GetDecoByPlayoutId request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
return await dbContext.Playouts
|
||||
.Include(p => p.Deco)
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId && p.DecoId != null)
|
||||
.MapT(p => Mapper.ProjectToViewModel(p.Deco));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record GetDecoTemplateById(int DecoTemplateId) : IRequest<Option<DecoTemplateViewModel>>;
|
||||
@@ -0,0 +1,17 @@
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class GetDecoTemplateByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetDecoTemplateById, Option<DecoTemplateViewModel>>
|
||||
{
|
||||
public async Task<Option<DecoTemplateViewModel>> Handle(GetDecoTemplateById request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
return await dbContext.DecoTemplates
|
||||
.SelectOneAsync(b => b.Id, b => b.Id == request.DecoTemplateId)
|
||||
.MapT(Mapper.ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record GetDecoTemplateItems(int DecoTemplateId) : IRequest<List<DecoTemplateItemViewModel>>;
|
||||
@@ -0,0 +1,20 @@
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class GetDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetDecoTemplateItems, List<DecoTemplateItemViewModel>>
|
||||
{
|
||||
public async Task<List<DecoTemplateItemViewModel>> Handle(GetDecoTemplateItems request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
return await dbContext.DecoTemplateItems
|
||||
.AsNoTracking()
|
||||
.Filter(i => i.DecoTemplateId == request.DecoTemplateId)
|
||||
.Include(i => i.Deco)
|
||||
.ToListAsync(cancellationToken)
|
||||
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record GetDecoTemplatesByDecoTemplateGroupId(int DecoTemplateGroupId) : IRequest<List<DecoTemplateViewModel>>;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class GetDecoTemplatesByDecoTemplateGroupIdHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetDecoTemplatesByDecoTemplateGroupId, List<DecoTemplateViewModel>>
|
||||
{
|
||||
public async Task<List<DecoTemplateViewModel>> Handle(
|
||||
GetDecoTemplatesByDecoTemplateGroupId request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
return await dbContext.DecoTemplates
|
||||
.AsNoTracking()
|
||||
.Filter(i => i.DecoTemplateGroupId == request.DecoTemplateGroupId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public record GetDecosByDecoGroupId(int DecoGroupId) : IRequest<List<DecoViewModel>>;
|
||||
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Scheduling;
|
||||
|
||||
public class GetDecosByDecoGroupIdHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetDecosByDecoGroupId, List<DecoViewModel>>
|
||||
{
|
||||
public async Task<List<DecoViewModel>> Handle(GetDecosByDecoGroupId request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
List<Deco> decos = await dbContext.Decos
|
||||
.Filter(b => b.DecoGroupId == request.DecoGroupId)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return decos.Map(Mapper.ProjectToViewModel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public class GetPlayoutTemplatesHandler(IDbContextFactory<TvContext> dbContextFa
|
||||
.AsNoTracking()
|
||||
.Filter(t => t.PlayoutId == request.PlayoutId)
|
||||
.Include(t => t.Template)
|
||||
.Include(t => t.DecoTemplate)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return playoutTemplates.Map(Mapper.ProjectToViewModel).ToList();
|
||||
|
||||
+64
-2
@@ -4,6 +4,7 @@ using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
@@ -78,6 +79,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
DateTimeOffset now = request.Now;
|
||||
|
||||
Either<BaseError, PlayoutItemWithPath> maybePlayoutItem = await dbContext.PlayoutItems
|
||||
.Include(i => i.Playout)
|
||||
.ThenInclude(p => p.Deco)
|
||||
.ThenInclude(d => d.Watermark)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
|
||||
.ThenInclude(em => em.Subtitles)
|
||||
@@ -194,12 +198,70 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
watermarkId => dbContext.ChannelWatermarks
|
||||
.SelectOneAsync(w => w.Id, w => w.Id == watermarkId));
|
||||
|
||||
Option<ChannelWatermark> playoutItemWatermark = Optional(playoutItemWithPath.PlayoutItem.Watermark);
|
||||
if (playoutItemWatermark.IsNone &&
|
||||
playoutItemWithPath.PlayoutItem.Playout.ProgramSchedulePlayoutType is ProgramSchedulePlayoutType.Block)
|
||||
{
|
||||
_logger.LogDebug("Block playout item has no watermark; checking for deco");
|
||||
|
||||
// check for playout template deco
|
||||
// load all playout templates
|
||||
// get playout template for start time
|
||||
// check for deco
|
||||
// load all templates that have decos
|
||||
List<PlayoutTemplate> playoutTemplates = await dbContext.PlayoutTemplates
|
||||
.AsNoTracking()
|
||||
.Filter(t => t.PlayoutId == playoutItemWithPath.PlayoutItem.PlayoutId)
|
||||
.Filter(t => t.DecoTemplateId != null)
|
||||
.Include(t => t.DecoTemplate)
|
||||
.ThenInclude(t => t.Items)
|
||||
.ThenInclude(i => i.Deco)
|
||||
.ThenInclude(d => d.Watermark)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
Option<PlayoutTemplate> maybeActiveTemplate = PlayoutTemplateSelector.GetPlayoutTemplateFor(
|
||||
playoutTemplates,
|
||||
playoutItemWithPath.PlayoutItem.StartOffset);
|
||||
|
||||
foreach (PlayoutTemplate activeTemplate in maybeActiveTemplate)
|
||||
{
|
||||
_logger.LogDebug("Block playout has active playout template; checking for deco template items");
|
||||
Option<DecoTemplateItem> maybeItem = activeTemplate.DecoTemplate.Items
|
||||
.Find(i => i.StartTime <= now.TimeOfDay && i.EndTime > now.TimeOfDay);
|
||||
foreach (DecoTemplateItem item in maybeItem)
|
||||
{
|
||||
_logger.LogDebug("Block playout has active deco template item; checking for watermark");
|
||||
foreach (ChannelWatermark watermark in Optional(item.Deco.Watermark))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Block playout has active deco template item with watermark; will use for this playout item");
|
||||
playoutItemWatermark = watermark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playoutItemWatermark.IsNone)
|
||||
{
|
||||
// check for playout deco
|
||||
foreach (Deco deco in Optional(playoutItemWithPath.PlayoutItem.Playout.Deco))
|
||||
{
|
||||
_logger.LogDebug("Block playout item has default deco; checking for watermark");
|
||||
foreach (ChannelWatermark watermark in Optional(deco.Watermark))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Block playout has default deco with watermark; will use for this playout item");
|
||||
playoutItemWatermark = watermark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playoutItemWithPath.PlayoutItem.MediaItem is Song song)
|
||||
{
|
||||
(videoPath, videoVersion) = await _songVideoGenerator.GenerateSongVideo(
|
||||
song,
|
||||
channel,
|
||||
Optional(playoutItemWithPath.PlayoutItem.Watermark),
|
||||
playoutItemWatermark,
|
||||
maybeGlobalWatermark,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
@@ -239,7 +301,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
start,
|
||||
finish,
|
||||
request.StartAtZero ? start : now,
|
||||
Optional(playoutItemWithPath.PlayoutItem.Watermark),
|
||||
playoutItemWatermark,
|
||||
maybeGlobalWatermark,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
|
||||
@@ -20,4 +20,6 @@ public class Playout
|
||||
public ICollection<PlayoutHistory> PlayoutHistory { get; set; }
|
||||
public int Seed { get; set; }
|
||||
public TimeSpan? DailyRebuildTime { get; set; }
|
||||
public int? DecoId { get; set; }
|
||||
public Deco Deco { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling;
|
||||
|
||||
public class Deco
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DecoGroupId { get; set; }
|
||||
public DecoGroup DecoGroup { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public int? WatermarkId { get; set; }
|
||||
public ChannelWatermark Watermark { get; set; }
|
||||
|
||||
// can be added directly to playouts
|
||||
public ICollection<Playout> Playouts { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling;
|
||||
|
||||
public class DecoGroup
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public ICollection<Deco> Decos { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling;
|
||||
|
||||
public class DecoTemplate
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DecoTemplateGroupId { get; set; }
|
||||
public DecoTemplateGroup DecoTemplateGroup { get; set; }
|
||||
public string Name { get; set; }
|
||||
public ICollection<DecoTemplateItem> Items { get; set; }
|
||||
public ICollection<PlayoutTemplate> PlayoutTemplates { get; set; }
|
||||
public DateTime DateUpdated { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling;
|
||||
|
||||
public class DecoTemplateGroup
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public ICollection<DecoTemplate> DecoTemplates { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling;
|
||||
|
||||
public class DecoTemplateItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DecoTemplateId { get; set; }
|
||||
public DecoTemplate DecoTemplate { get; set; }
|
||||
public int DecoId { get; set; }
|
||||
public Deco Deco { get; set; }
|
||||
public TimeSpan StartTime { get; set; }
|
||||
public TimeSpan EndTime { get; set; }
|
||||
}
|
||||
@@ -7,6 +7,8 @@ public class PlayoutTemplate
|
||||
public Playout Playout { get; set; }
|
||||
public int TemplateId { get; set; }
|
||||
public Template Template { get; set; }
|
||||
public int? DecoTemplateId { get; set; }
|
||||
public DecoTemplate DecoTemplate { get; set; }
|
||||
public int Index { get; set; }
|
||||
public ICollection<DayOfWeek> DaysOfWeek { get; set; }
|
||||
public ICollection<int> DaysOfMonth { get; set; }
|
||||
|
||||
+5404
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_Deco_Group_Deco : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DecoId",
|
||||
table: "Playout",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DecoGroup",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Name = table.Column<string>(type: "varchar(255)", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DecoGroup", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Deco",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
DecoGroupId = table.Column<int>(type: "int", nullable: false),
|
||||
Name = table.Column<string>(type: "varchar(255)", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
WatermarkId = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Deco", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Deco_ChannelWatermark_WatermarkId",
|
||||
column: x => x.WatermarkId,
|
||||
principalTable: "ChannelWatermark",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_Deco_DecoGroup_DecoGroupId",
|
||||
column: x => x.DecoGroupId,
|
||||
principalTable: "DecoGroup",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Playout_DecoId",
|
||||
table: "Playout",
|
||||
column: "DecoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Deco_DecoGroupId_Name",
|
||||
table: "Deco",
|
||||
columns: new[] { "DecoGroupId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Deco_WatermarkId",
|
||||
table: "Deco",
|
||||
column: "WatermarkId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoGroup_Name",
|
||||
table: "DecoGroup",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Playout_Deco_DecoId",
|
||||
table: "Playout",
|
||||
column: "DecoId",
|
||||
principalTable: "Deco",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Playout_Deco_DecoId",
|
||||
table: "Playout");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Deco");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DecoGroup");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Playout_DecoId",
|
||||
table: "Playout");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DecoId",
|
||||
table: "Playout");
|
||||
}
|
||||
}
|
||||
}
|
||||
+5533
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_DecoTemplate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DecoTemplateId",
|
||||
table: "PlayoutTemplate",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DecoTemplateGroup",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Name = table.Column<string>(type: "varchar(255)", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DecoTemplateGroup", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DecoTemplate",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
DecoTemplateGroupId = table.Column<int>(type: "int", nullable: false),
|
||||
Name = table.Column<string>(type: "varchar(255)", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DateUpdated = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DecoTemplate", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DecoTemplate_DecoTemplateGroup_DecoTemplateGroupId",
|
||||
column: x => x.DecoTemplateGroupId,
|
||||
principalTable: "DecoTemplateGroup",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DecoTemplateItem",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
DecoTemplateId = table.Column<int>(type: "int", nullable: false),
|
||||
DecoId = table.Column<int>(type: "int", nullable: false),
|
||||
StartTime = table.Column<TimeSpan>(type: "time(6)", nullable: false),
|
||||
EndTime = table.Column<TimeSpan>(type: "time(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DecoTemplateItem", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DecoTemplateItem_DecoTemplate_DecoTemplateId",
|
||||
column: x => x.DecoTemplateId,
|
||||
principalTable: "DecoTemplate",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DecoTemplateItem_Deco_DecoId",
|
||||
column: x => x.DecoId,
|
||||
principalTable: "Deco",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PlayoutTemplate_DecoTemplateId",
|
||||
table: "PlayoutTemplate",
|
||||
column: "DecoTemplateId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplate_DecoTemplateGroupId",
|
||||
table: "DecoTemplate",
|
||||
column: "DecoTemplateGroupId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplate_Name",
|
||||
table: "DecoTemplate",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplateGroup_Name",
|
||||
table: "DecoTemplateGroup",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplateItem_DecoId",
|
||||
table: "DecoTemplateItem",
|
||||
column: "DecoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplateItem_DecoTemplateId",
|
||||
table: "DecoTemplateItem",
|
||||
column: "DecoTemplateId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PlayoutTemplate_DecoTemplate_DecoTemplateId",
|
||||
table: "PlayoutTemplate",
|
||||
column: "DecoTemplateId",
|
||||
principalTable: "DecoTemplate",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PlayoutTemplate_DecoTemplate_DecoTemplateId",
|
||||
table: "PlayoutTemplate");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DecoTemplateItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DecoTemplate");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DecoTemplateGroup");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PlayoutTemplate_DecoTemplateId",
|
||||
table: "PlayoutTemplate");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DecoTemplateId",
|
||||
table: "PlayoutTemplate");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1590,6 +1590,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<TimeSpan?>("DailyRebuildTime")
|
||||
.HasColumnType("time(6)");
|
||||
|
||||
b.Property<int?>("DecoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ExternalJsonFile")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
@@ -1606,6 +1609,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("DecoId");
|
||||
|
||||
b.HasIndex("ProgramScheduleId");
|
||||
|
||||
b.ToTable("Playout", (string)null);
|
||||
@@ -2132,6 +2137,127 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.ToTable("BlockItem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DecoGroupId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.Property<int?>("WatermarkId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WatermarkId");
|
||||
|
||||
b.HasIndex("DecoGroupId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Deco", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DecoGroup", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("DateUpdated")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("DecoTemplateGroupId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DecoTemplateGroupId");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DecoTemplate", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DecoTemplateGroup", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DecoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DecoTemplateId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<TimeSpan>("EndTime")
|
||||
.HasColumnType("time(6)");
|
||||
|
||||
b.Property<TimeSpan>("StartTime")
|
||||
.HasColumnType("time(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DecoId");
|
||||
|
||||
b.HasIndex("DecoTemplateId");
|
||||
|
||||
b.ToTable("DecoTemplateItem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -2187,6 +2313,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<string>("DaysOfWeek")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int?>("DecoTemplateId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("EndDay")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -2216,6 +2345,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DecoTemplateId");
|
||||
|
||||
b.HasIndex("PlayoutId");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
@@ -3908,6 +4039,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
|
||||
.WithMany("Playouts")
|
||||
.HasForeignKey("DecoId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
|
||||
.WithMany("Playouts")
|
||||
.HasForeignKey("ProgramScheduleId")
|
||||
@@ -3969,6 +4105,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
|
||||
b.Navigation("Channel");
|
||||
|
||||
b.Navigation("Deco");
|
||||
|
||||
b.Navigation("ProgramSchedule");
|
||||
});
|
||||
|
||||
@@ -4269,6 +4407,54 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Navigation("SmartCollection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoGroup", "DecoGroup")
|
||||
.WithMany("Decos")
|
||||
.HasForeignKey("DecoGroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
||||
.WithMany()
|
||||
.HasForeignKey("WatermarkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DecoGroup");
|
||||
|
||||
b.Navigation("Watermark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", "DecoTemplateGroup")
|
||||
.WithMany("DecoTemplates")
|
||||
.HasForeignKey("DecoTemplateGroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DecoTemplateGroup");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
|
||||
.WithMany()
|
||||
.HasForeignKey("DecoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("DecoTemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Deco");
|
||||
|
||||
b.Navigation("DecoTemplate");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
|
||||
@@ -4290,6 +4476,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate")
|
||||
.WithMany("PlayoutTemplates")
|
||||
.HasForeignKey("DecoTemplateId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
|
||||
.WithMany("Templates")
|
||||
.HasForeignKey("PlayoutId")
|
||||
@@ -4302,6 +4493,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DecoTemplate");
|
||||
|
||||
b.Navigation("Playout");
|
||||
|
||||
b.Navigation("Template");
|
||||
@@ -5138,6 +5331,28 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Navigation("Blocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
|
||||
{
|
||||
b.Navigation("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b =>
|
||||
{
|
||||
b.Navigation("Decos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
b.Navigation("PlayoutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b =>
|
||||
{
|
||||
b.Navigation("DecoTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
+5255
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,112 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_Deco_Group_Deco : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DecoId",
|
||||
table: "Playout",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DecoGroup",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DecoGroup", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Deco",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DecoGroupId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
WatermarkId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Deco", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Deco_ChannelWatermark_WatermarkId",
|
||||
column: x => x.WatermarkId,
|
||||
principalTable: "ChannelWatermark",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_Deco_DecoGroup_DecoGroupId",
|
||||
column: x => x.DecoGroupId,
|
||||
principalTable: "DecoGroup",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Playout_DecoId",
|
||||
table: "Playout",
|
||||
column: "DecoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Deco_DecoGroupId_Name",
|
||||
table: "Deco",
|
||||
columns: new[] { "DecoGroupId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Deco_WatermarkId",
|
||||
table: "Deco",
|
||||
column: "WatermarkId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoGroup_Name",
|
||||
table: "DecoGroup",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Playout_Deco_DecoId",
|
||||
table: "Playout",
|
||||
column: "DecoId",
|
||||
principalTable: "Deco",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Playout_Deco_DecoId",
|
||||
table: "Playout");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Deco");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DecoGroup");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Playout_DecoId",
|
||||
table: "Playout");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DecoId",
|
||||
table: "Playout");
|
||||
}
|
||||
}
|
||||
}
|
||||
+5375
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_DecoTemplate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DecoTemplateId",
|
||||
table: "PlayoutTemplate",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DecoTemplateGroup",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DecoTemplateGroup", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DecoTemplate",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DecoTemplateGroupId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DateUpdated = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DecoTemplate", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DecoTemplate_DecoTemplateGroup_DecoTemplateGroupId",
|
||||
column: x => x.DecoTemplateGroupId,
|
||||
principalTable: "DecoTemplateGroup",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DecoTemplateItem",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DecoTemplateId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DecoId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
StartTime = table.Column<TimeSpan>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DecoTemplateItem", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DecoTemplateItem_DecoTemplate_DecoTemplateId",
|
||||
column: x => x.DecoTemplateId,
|
||||
principalTable: "DecoTemplate",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DecoTemplateItem_Deco_DecoId",
|
||||
column: x => x.DecoId,
|
||||
principalTable: "Deco",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PlayoutTemplate_DecoTemplateId",
|
||||
table: "PlayoutTemplate",
|
||||
column: "DecoTemplateId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplate_DecoTemplateGroupId",
|
||||
table: "DecoTemplate",
|
||||
column: "DecoTemplateGroupId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplate_Name",
|
||||
table: "DecoTemplate",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplateGroup_Name",
|
||||
table: "DecoTemplateGroup",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplateItem_DecoId",
|
||||
table: "DecoTemplateItem",
|
||||
column: "DecoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DecoTemplateItem_DecoTemplateId",
|
||||
table: "DecoTemplateItem",
|
||||
column: "DecoTemplateId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PlayoutTemplate_DecoTemplate_DecoTemplateId",
|
||||
table: "PlayoutTemplate",
|
||||
column: "DecoTemplateId",
|
||||
principalTable: "DecoTemplate",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PlayoutTemplate_DecoTemplate_DecoTemplateId",
|
||||
table: "PlayoutTemplate");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DecoTemplateItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DecoTemplate");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DecoTemplateGroup");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PlayoutTemplate_DecoTemplateId",
|
||||
table: "PlayoutTemplate");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DecoTemplateId",
|
||||
table: "PlayoutTemplate");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+5378
File diff suppressed because it is too large
Load Diff
+30
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_DecoTemplateItem_EndTime : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<TimeSpan>(
|
||||
name: "EndTime",
|
||||
table: "DecoTemplateItem",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new TimeSpan(0, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EndTime",
|
||||
table: "DecoTemplateItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1509,6 +1509,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<TimeSpan?>("DailyRebuildTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DecoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ExternalJsonFile")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -1525,6 +1528,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("DecoId");
|
||||
|
||||
b.HasIndex("ProgramScheduleId");
|
||||
|
||||
b.ToTable("Playout", (string)null);
|
||||
@@ -2025,6 +2030,117 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.ToTable("BlockItem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DecoGroupId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("WatermarkId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WatermarkId");
|
||||
|
||||
b.HasIndex("DecoGroupId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Deco", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DecoGroup", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateUpdated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DecoTemplateGroupId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DecoTemplateGroupId");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DecoTemplate", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DecoTemplateGroup", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DecoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DecoTemplateId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeSpan>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeSpan>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DecoId");
|
||||
|
||||
b.HasIndex("DecoTemplateId");
|
||||
|
||||
b.ToTable("DecoTemplateItem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -2076,6 +2192,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<string>("DaysOfWeek")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DecoTemplateId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EndDay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -2105,6 +2224,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DecoTemplateId");
|
||||
|
||||
b.HasIndex("PlayoutId");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
@@ -3763,6 +3884,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
|
||||
.WithMany("Playouts")
|
||||
.HasForeignKey("DecoId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
|
||||
.WithMany("Playouts")
|
||||
.HasForeignKey("ProgramScheduleId")
|
||||
@@ -3824,6 +3950,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
|
||||
b.Navigation("Channel");
|
||||
|
||||
b.Navigation("Deco");
|
||||
|
||||
b.Navigation("ProgramSchedule");
|
||||
});
|
||||
|
||||
@@ -4124,6 +4252,54 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Navigation("SmartCollection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoGroup", "DecoGroup")
|
||||
.WithMany("Decos")
|
||||
.HasForeignKey("DecoGroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
||||
.WithMany()
|
||||
.HasForeignKey("WatermarkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DecoGroup");
|
||||
|
||||
b.Navigation("Watermark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", "DecoTemplateGroup")
|
||||
.WithMany("DecoTemplates")
|
||||
.HasForeignKey("DecoTemplateGroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DecoTemplateGroup");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
|
||||
.WithMany()
|
||||
.HasForeignKey("DecoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("DecoTemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Deco");
|
||||
|
||||
b.Navigation("DecoTemplate");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
|
||||
@@ -4145,6 +4321,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate")
|
||||
.WithMany("PlayoutTemplates")
|
||||
.HasForeignKey("DecoTemplateId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
|
||||
.WithMany("Templates")
|
||||
.HasForeignKey("PlayoutId")
|
||||
@@ -4157,6 +4338,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DecoTemplate");
|
||||
|
||||
b.Navigation("Playout");
|
||||
|
||||
b.Navigation("Template");
|
||||
@@ -4993,6 +5176,28 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Navigation("Blocks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
|
||||
{
|
||||
b.Navigation("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b =>
|
||||
{
|
||||
b.Navigation("Decos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
b.Navigation("PlayoutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b =>
|
||||
{
|
||||
b.Navigation("DecoTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
|
||||
|
||||
public class DecoConfiguration : IEntityTypeConfiguration<Deco>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Deco> builder)
|
||||
{
|
||||
builder.ToTable("Deco");
|
||||
|
||||
builder.HasIndex(d => new { d.DecoGroupId, d.Name })
|
||||
.IsUnique();
|
||||
|
||||
builder.HasMany(d => d.Playouts)
|
||||
.WithOne(p => p.Deco)
|
||||
.HasForeignKey(p => p.DecoId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(d => d.Watermark)
|
||||
.WithMany()
|
||||
.HasForeignKey(d => d.WatermarkId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
|
||||
|
||||
public class DecoGroupConfiguration : IEntityTypeConfiguration<DecoGroup>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<DecoGroup> builder)
|
||||
{
|
||||
builder.ToTable("DecoGroup");
|
||||
|
||||
builder.HasIndex(dg => dg.Name)
|
||||
.IsUnique();
|
||||
|
||||
builder.HasMany(dg => dg.Decos)
|
||||
.WithOne(d => d.DecoGroup)
|
||||
.HasForeignKey(d => d.DecoGroupId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
|
||||
|
||||
public class DecoTemplateConfiguration : IEntityTypeConfiguration<DecoTemplate>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<DecoTemplate> builder)
|
||||
{
|
||||
builder.ToTable("DecoTemplate");
|
||||
|
||||
builder.HasIndex(b => b.Name)
|
||||
.IsUnique();
|
||||
|
||||
builder.HasMany(b => b.Items)
|
||||
.WithOne(i => i.DecoTemplate)
|
||||
.HasForeignKey(i => i.DecoTemplateId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(t => t.PlayoutTemplates)
|
||||
.WithOne(t => t.DecoTemplate)
|
||||
.HasForeignKey(t => t.DecoTemplateId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
|
||||
|
||||
public class DecoTemplateGroupConfiguration : IEntityTypeConfiguration<DecoTemplateGroup>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<DecoTemplateGroup> builder)
|
||||
{
|
||||
builder.ToTable("DecoTemplateGroup");
|
||||
|
||||
builder.HasIndex(b => b.Name)
|
||||
.IsUnique();
|
||||
|
||||
builder.HasMany(b => b.DecoTemplates)
|
||||
.WithOne(i => i.DecoTemplateGroup)
|
||||
.HasForeignKey(i => i.DecoTemplateGroupId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
|
||||
|
||||
public class DecoTemplateItemConfiguration : IEntityTypeConfiguration<DecoTemplateItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<DecoTemplateItem> builder) => builder.ToTable("DecoTemplateItem");
|
||||
}
|
||||
@@ -120,7 +120,22 @@ public class MediaSourceRepository : IMediaSourceRepository
|
||||
}
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
// TODO: possibly caused by https://github.com/dotnet/efcore/issues/33133
|
||||
var success = false;
|
||||
var attempts = 0;
|
||||
while (!success && attempts < 3)
|
||||
{
|
||||
try
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
success = true;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
// do nothing
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<int>> UpdateLibraries(
|
||||
|
||||
@@ -94,6 +94,11 @@ public class TvContext : DbContext
|
||||
public DbSet<TemplateGroup> TemplateGroups { get; set; }
|
||||
public DbSet<Template> Templates { get; set; }
|
||||
public DbSet<TemplateItem> TemplateItems { get; set; }
|
||||
public DbSet<DecoGroup> DecoGroups { get; set; }
|
||||
public DbSet<Deco> Decos { get; set; }
|
||||
public DbSet<DecoTemplateGroup> DecoTemplateGroups { get; set; }
|
||||
public DbSet<DecoTemplate> DecoTemplates { get; set; }
|
||||
public DbSet<DecoTemplateItem> DecoTemplateItems { get; set; }
|
||||
public DbSet<FFmpegProfile> FFmpegProfiles { get; set; }
|
||||
public DbSet<Resolution> Resolutions { get; set; }
|
||||
public DbSet<LanguageCode> LanguageCodes { get; set; }
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
@page "/playouts/block/{Id:int}"
|
||||
@using ErsatzTV.Application.Scheduling
|
||||
@using ErsatzTV.Application.Channels
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ILogger<BlockPlayoutEditor> Logger
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IMediator Mediator
|
||||
@inject IEntityLocker EntityLocker;
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Edit Block Playout - @_channelName</MudText>
|
||||
<MudGrid Class="mt-4">
|
||||
<MudCard Class="mr-6 mb-6" Style="width: 400px">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h5">Playout Templates</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" Link="@($"playouts/{Id}/templates")" Class="mt-4">
|
||||
Edit Templates
|
||||
</MudButton>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
<MudCard Class="mr-6 mb-6" Style="width: 400px">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h5">Playout Items and History</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<div>
|
||||
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Warning" OnClick="@(_ => EraseItems(eraseHistory: false))" Class="mt-4">
|
||||
Erase Items
|
||||
</MudButton>
|
||||
</div>
|
||||
<div>
|
||||
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))" Class="mt-4">
|
||||
Erase Items and History
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
<MudCard Class="mr-6 mb-6" Style="width: 400px">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h5">Default Deco</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSwitch T="bool" Label="Enable Default Deco" @bind-Value="_enableDefaultDeco" Color="Color.Primary"/>
|
||||
</MudElement>
|
||||
@if (_enableDefaultDeco)
|
||||
{
|
||||
<MudElement HtmlTag="div" Class="mt-2">
|
||||
<MudSelect T="DecoGroupViewModel"
|
||||
Label="Deco Group"
|
||||
Value="@_selectedDefaultDecoGroup"
|
||||
ValueChanged="@(vm => UpdateDefaultDecoTemplateGroupItems(vm))">
|
||||
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
|
||||
{
|
||||
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-2">
|
||||
<MudSelect Label="Deco"
|
||||
@bind-Value="_defaultDeco"
|
||||
For="@(() => _defaultDeco)">
|
||||
@foreach (DecoViewModel deco in _decos)
|
||||
{
|
||||
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudElement>
|
||||
}
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveDefaultDeco())">
|
||||
Save Changes
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private readonly List<DecoGroupViewModel> _decoGroups = [];
|
||||
private readonly List<DecoViewModel> _decos = [];
|
||||
|
||||
private string _channelName;
|
||||
private bool _enableDefaultDeco;
|
||||
private DecoGroupViewModel _selectedDefaultDecoGroup;
|
||||
private DecoViewModel _defaultDeco;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token);
|
||||
if (maybeName.IsNone)
|
||||
{
|
||||
NavigationManager.NavigateTo("/playouts");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string name in maybeName)
|
||||
{
|
||||
_channelName = name;
|
||||
}
|
||||
|
||||
_decoGroups.Clear();
|
||||
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), _cts.Token));
|
||||
|
||||
Option<DecoViewModel> maybeDefaultDeco = await Mediator.Send(new GetDecoByPlayoutId(Id), _cts.Token);
|
||||
foreach (DecoViewModel defaultDeco in maybeDefaultDeco)
|
||||
{
|
||||
_enableDefaultDeco = true;
|
||||
_selectedDefaultDecoGroup = _decoGroups.SingleOrDefault(dg => dg.Id == defaultDeco.DecoGroupId);
|
||||
await UpdateDefaultDecoTemplateGroupItems(_selectedDefaultDecoGroup);
|
||||
_defaultDeco = defaultDeco;
|
||||
}
|
||||
}
|
||||
private async Task UpdateDefaultDecoTemplateGroupItems(DecoGroupViewModel decoGroup)
|
||||
{
|
||||
_selectedDefaultDecoGroup = decoGroup;
|
||||
|
||||
_decos.Clear();
|
||||
_decos.AddRange(await Mediator.Send(new GetDecosByDecoGroupId(_selectedDefaultDecoGroup.Id), _cts.Token));
|
||||
}
|
||||
|
||||
private async Task EraseItems(bool eraseHistory)
|
||||
{
|
||||
IRequest request = eraseHistory ? new EraseBlockPlayoutHistory(Id) : new EraseBlockPlayoutItems(Id);
|
||||
await Mediator.Send(request, _cts.Token);
|
||||
|
||||
string message = eraseHistory ? "Erased playout items and history" : "Erased playout items";
|
||||
Snackbar.Add(message, Severity.Info);
|
||||
}
|
||||
|
||||
private async Task SaveDefaultDeco()
|
||||
{
|
||||
int? decoId = _enableDefaultDeco ? _defaultDeco?.Id : null;
|
||||
await Mediator.Send(new UpdateDefaultDeco(Id, decoId), _cts.Token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
@page "/decos/{Id:int}"
|
||||
@using ErsatzTV.Application.Scheduling
|
||||
@using ErsatzTV.Application.Watermarks
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ILogger<DecoEditor> Logger
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Edit Deco</MudText>
|
||||
<div style="max-width: 400px">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudTextField Label="Name" @bind-Value="_deco.Name" For="@(() => _deco.Name)"/>
|
||||
<MudSelect Class="mt-3" Label="Watermark" @bind-Value="_deco.WatermarkId" For="@(() => _deco.WatermarkId)"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
|
||||
@foreach (WatermarkViewModel watermark in _watermarks)
|
||||
{
|
||||
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
|
||||
Save Changes
|
||||
</MudButton>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private DecoEditViewModel _deco = new();
|
||||
private List<WatermarkViewModel> _watermarks = [];
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await LoadWatermarks();
|
||||
await LoadDeco();
|
||||
}
|
||||
|
||||
private async Task LoadWatermarks() =>
|
||||
_watermarks = await Mediator.Send(new GetAllWatermarks(), _cts.Token);
|
||||
|
||||
private async Task LoadDeco()
|
||||
{
|
||||
Option<DecoViewModel> maybeDeco = await Mediator.Send(new GetDecoById(Id), _cts.Token);
|
||||
if (maybeDeco.IsNone)
|
||||
{
|
||||
NavigationManager.NavigateTo("decos");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (DecoViewModel deco in maybeDeco)
|
||||
{
|
||||
_deco = new DecoEditViewModel
|
||||
{
|
||||
Name = deco.Name,
|
||||
DecoGroupId = deco.DecoGroupId,
|
||||
WatermarkId = deco.WatermarkId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveChanges()
|
||||
{
|
||||
Seq<BaseError> errorMessages = await Mediator
|
||||
.Send(new UpdateDeco(Id, _deco.DecoGroupId, _deco.Name, _deco.WatermarkId), _cts.Token)
|
||||
.Map(e => e.LeftToSeq());
|
||||
|
||||
errorMessages.HeadOrNone().Match(
|
||||
error =>
|
||||
{
|
||||
Snackbar.Add($"Unexpected error saving deco: {error.Value}", Severity.Error);
|
||||
Logger.LogError("Unexpected error saving deco: {Error}", error.Value);
|
||||
},
|
||||
() => NavigationManager.NavigateTo("/decos"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
@page "/deco-templates/{Id:int}"
|
||||
@using ErsatzTV.Application.Scheduling
|
||||
@using System.Globalization
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ILogger<DecoTemplateEditor> Logger
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Edit Deco Template</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="4">
|
||||
<div style="max-width: 400px">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudTextField Label="Name" @bind-Value="_decoTemplate.Name" For="@(() => _decoTemplate.Name)"/>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4">
|
||||
Save Changes
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="4">
|
||||
<div style="max-width: 400px">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudSelect T="DecoGroupViewModel"
|
||||
Label="Deco Group"
|
||||
ValueChanged="@(vm => UpdateDecoGroupItems(vm))">
|
||||
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
|
||||
{
|
||||
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3"
|
||||
T="DecoViewModel"
|
||||
Label="Deco"
|
||||
@bind-value="_selectedDeco">
|
||||
@foreach (DecoViewModel deco in _decos)
|
||||
{
|
||||
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3"
|
||||
T="DateTime"
|
||||
Label="Start Time On Or After"
|
||||
@bind-value="_selectedDecoStart">
|
||||
@foreach (DateTime startTime in _startTimes)
|
||||
{
|
||||
<MudSelectItem Value="@startTime">
|
||||
@startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern)
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
|
||||
<MudItem xs="6">
|
||||
<MudTextField T="int"
|
||||
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="5"/>
|
||||
<MudSelectItem Value="10"/>
|
||||
<MudSelectItem Value="15"/>
|
||||
<MudSelectItem Value="20"/>
|
||||
<MudSelectItem Value="25"/>
|
||||
<MudSelectItem Value="30"/>
|
||||
<MudSelectItem Value="35"/>
|
||||
<MudSelectItem Value="40"/>
|
||||
<MudSelectItem Value="45"/>
|
||||
<MudSelectItem Value="50"/>
|
||||
<MudSelectItem Value="55"/>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoToDecoTemplate())" Disabled="@(_selectedDeco is null)">
|
||||
Add Deco To Deco Template
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
<MudItem xs="4">
|
||||
<div style="max-width: 400px">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudSelect T="DecoTemplateItemEditViewModel"
|
||||
Label="Deco To Remove"
|
||||
@bind-Value="_decoToRemove">
|
||||
<MudSelectItem Value="@((DecoTemplateItemEditViewModel)null)">(none)</MudSelectItem>
|
||||
@foreach (DecoTemplateItemEditViewModel item in _decoTemplate.Items.OrderBy(i => i.Start))
|
||||
{
|
||||
<MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => RemoveDecoFromDecoTemplate())" Disabled="@(_decoToRemove is null)">
|
||||
Remove Deco From Deco Template
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
<MudItem xs="8">
|
||||
<MudCalendar Class="mt-4"
|
||||
Items="@_decoTemplate.Items"
|
||||
ShowMonth="false"
|
||||
ShowWeek="false"
|
||||
ShowPrevNextButtons="false"
|
||||
ShowDatePicker="false"
|
||||
ShowTodayButton="false"
|
||||
DayTimeInterval="CalendarTimeInterval.Minutes10"
|
||||
Use24HourClock="@(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern.Contains("H"))"
|
||||
EnableDragItems="true"
|
||||
EnableResizeItems="false"
|
||||
ItemChanged="@(ci => CalendarItemChanged(ci))"/>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly List<DecoGroupViewModel> _decoGroups = [];
|
||||
private readonly List<DecoViewModel> _decos = [];
|
||||
private readonly List<DateTime> _startTimes = [];
|
||||
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private DecoTemplateItemsEditViewModel _decoTemplate = new();
|
||||
private DecoTemplateItemEditViewModel _decoToRemove;
|
||||
private DecoGroupViewModel _selectedDecoGroup;
|
||||
private DecoViewModel _selectedDeco;
|
||||
private DateTime _selectedDecoStart;
|
||||
private int _durationHours;
|
||||
private int _durationMinutes = 15;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await LoadDecoTemplateItems();
|
||||
|
||||
DateTime start = DateTime.Today;
|
||||
_selectedDecoStart = start;
|
||||
while (start.Date == DateTime.Today.Date)
|
||||
{
|
||||
_startTimes.Add(start);
|
||||
start = start.AddMinutes(5);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadDecoTemplateItems()
|
||||
{
|
||||
Option<DecoTemplateViewModel> maybeDecoTemplate = await Mediator.Send(new GetDecoTemplateById(Id), _cts.Token);
|
||||
if (maybeDecoTemplate.IsNone)
|
||||
{
|
||||
NavigationManager.NavigateTo("deco-templates");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (DecoTemplateViewModel template in maybeDecoTemplate)
|
||||
{
|
||||
_decoTemplate = new DecoTemplateItemsEditViewModel
|
||||
{
|
||||
Name = template.Name,
|
||||
Items = []
|
||||
};
|
||||
}
|
||||
|
||||
Option<IEnumerable<DecoTemplateItemViewModel>> maybeResults = await Mediator.Send(new GetDecoTemplateItems(Id), _cts.Token);
|
||||
foreach (IEnumerable<DecoTemplateItemViewModel> items in maybeResults)
|
||||
{
|
||||
_decoTemplate.Items.AddRange(items.Map(ProjectToEditViewModel));
|
||||
}
|
||||
|
||||
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), _cts.Token));
|
||||
}
|
||||
|
||||
private static DecoTemplateItemEditViewModel ProjectToEditViewModel(DecoTemplateItemViewModel item) =>
|
||||
new()
|
||||
{
|
||||
DecoId = item.DecoId,
|
||||
DecoName = item.DecoName,
|
||||
Start = item.StartTime,
|
||||
End = item.EndTime
|
||||
};
|
||||
|
||||
private async Task UpdateDecoGroupItems(DecoGroupViewModel decoGroup)
|
||||
{
|
||||
_selectedDecoGroup = decoGroup;
|
||||
|
||||
_decos.Clear();
|
||||
_decos.AddRange(await Mediator.Send(new GetDecosByDecoGroupId(_selectedDecoGroup.Id), _cts.Token));
|
||||
}
|
||||
|
||||
private void AddDecoToDecoTemplate()
|
||||
{
|
||||
// find first time where this deco will fit
|
||||
DateTime maybeStart = _selectedDecoStart;
|
||||
while (maybeStart.Date == DateTime.Today)
|
||||
{
|
||||
DateTime maybeEnd = maybeStart.AddHours(_durationHours).AddMinutes(_durationMinutes);
|
||||
if (IntersectsOthers(null, maybeStart, maybeEnd) == false)
|
||||
{
|
||||
var item = new DecoTemplateItemEditViewModel
|
||||
{
|
||||
DecoId = _selectedDeco.Id,
|
||||
DecoName = _selectedDeco.Name,
|
||||
Start = maybeStart,
|
||||
End = maybeEnd,
|
||||
LastStart = maybeStart,
|
||||
LastEnd = maybeEnd
|
||||
};
|
||||
|
||||
_decoTemplate.Items.Add(item);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
maybeStart = maybeStart.AddMinutes(5);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveDecoFromDecoTemplate()
|
||||
{
|
||||
if (_decoToRemove is not null)
|
||||
{
|
||||
_decoTemplate.Items.Remove(_decoToRemove);
|
||||
_decoToRemove = null;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void CalendarItemChanged(CalendarItem calendarItem)
|
||||
{
|
||||
// don't allow any overlap
|
||||
if (calendarItem is DecoTemplateItemEditViewModel item)
|
||||
{
|
||||
if (item.End.HasValue && IntersectsOthers(item, item.Start, item.End.Value))
|
||||
{
|
||||
// roll back
|
||||
item.Start = item.LastStart;
|
||||
item.End = item.LastEnd;
|
||||
}
|
||||
else
|
||||
{
|
||||
// commit
|
||||
item.LastStart = item.Start;
|
||||
item.LastEnd = item.End;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IntersectsOthers(DecoTemplateItemEditViewModel item, DateTime start, DateTime end)
|
||||
{
|
||||
var willFit = true;
|
||||
|
||||
foreach (DecoTemplateItemEditViewModel existing in _decoTemplate.Items)
|
||||
{
|
||||
if (existing == item)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (start < existing.End && existing.Start < end)
|
||||
{
|
||||
willFit = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return willFit == false;
|
||||
}
|
||||
|
||||
// private void RemoveDecoTemplateItem(DecoTemplateItemEditViewModel item)
|
||||
// {
|
||||
// _selectedItem = null;
|
||||
// _decoTemplate.Items.Remove(item);
|
||||
// }
|
||||
|
||||
private async Task SaveChanges()
|
||||
{
|
||||
await Task.Delay(10);
|
||||
|
||||
var items = _decoTemplate.Items.Map(item => new ReplaceDecoTemplateItem(item.DecoId, item.Start.TimeOfDay, item.End!.Value.TimeOfDay)).ToList();
|
||||
|
||||
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceDecoTemplateItems(Id, _decoTemplate.Name, items), _cts.Token)
|
||||
.Map(e => e.LeftToSeq());
|
||||
|
||||
errorMessages.HeadOrNone().Match(
|
||||
error =>
|
||||
{
|
||||
Snackbar.Add($"Unexpected error saving template: {error.Value}", Severity.Error);
|
||||
Logger.LogError("Unexpected error saving template: {Error}", error.Value);
|
||||
},
|
||||
() => NavigationManager.NavigateTo("/deco-templates"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
@page "/deco-templates"
|
||||
@using S = System.Collections.Generic
|
||||
@using ErsatzTV.Application.Scheduling
|
||||
@implements IDisposable
|
||||
@inject ILogger<DecoTemplates> Logger
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IMediator Mediator
|
||||
@inject IDialogService Dialog
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Deco Templates</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="4">
|
||||
<div style="max-width: 400px;" class="mr-4">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudTextField Class="mt-3 mx-3" Label="Deco Template Group Name" @bind-Value="_decoTemplateGroupName" For="@(() => _decoTemplateGroupName)"/>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoTemplateGroup())" Class="ml-4 mb-4">
|
||||
Add Deco Template Group
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
<MudItem xs="4">
|
||||
<div style="max-width: 400px;" class="mb-6">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<div class="mx-4">
|
||||
<MudSelect Label="Deco Template Group" @bind-Value="_selectedDecoTemplateGroup" Class="mt-3">
|
||||
@foreach (DecoTemplateGroupViewModel decoTemplateGroup in _decoTemplateGroups)
|
||||
{
|
||||
<MudSelectItem Value="@decoTemplateGroup">@decoTemplateGroup.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField Class="mt-3" Label="Deco Template Name" @bind-Value="_decoTemplateName" For="@(() => _decoTemplateName)"/>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoTemplate())" Class="ml-4 mb-4">
|
||||
Add Deco Template
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
<MudItem xs="8">
|
||||
<MudCard>
|
||||
<MudTreeView ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
|
||||
<ItemTemplate Context="item">
|
||||
<MudTreeViewItem Items="@item.TreeItems" Icon="@item.Icon" CanExpand="@item.CanExpand" Value="@item">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudGrid Justify="Justify.FlexStart">
|
||||
<MudItem xs="8">
|
||||
<MudText>@item.Text</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<div style="justify-self: end;">
|
||||
@foreach (int decoTemplateId in Optional(item.DecoTemplateId))
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Href="@($"deco-templates/{decoTemplateId}")"/>
|
||||
}
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="@(_ => DeleteItem(item))"/>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private S.HashSet<DecoTemplateTreeItemViewModel> TreeItems { get; set; } = [];
|
||||
private List<DecoTemplateGroupViewModel> _decoTemplateGroups = [];
|
||||
private DecoTemplateGroupViewModel _selectedDecoTemplateGroup;
|
||||
private string _decoTemplateGroupName;
|
||||
private string _decoTemplateName;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await ReloadDecoTemplateGroups();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task ReloadDecoTemplateGroups()
|
||||
{
|
||||
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
|
||||
TreeItems = _decoTemplateGroups.Map(g => new DecoTemplateTreeItemViewModel(g)).ToHashSet();
|
||||
}
|
||||
|
||||
private async Task AddDecoTemplateGroup()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_decoTemplateGroupName))
|
||||
{
|
||||
Either<BaseError, DecoTemplateGroupViewModel> result = await Mediator.Send(new CreateDecoTemplateGroup(_decoTemplateGroupName), _cts.Token);
|
||||
|
||||
foreach (BaseError error in result.LeftToSeq())
|
||||
{
|
||||
Snackbar.Add(error.Value, Severity.Error);
|
||||
Logger.LogError("Unexpected error adding deco template group: {Error}", error.Value);
|
||||
}
|
||||
|
||||
foreach (DecoTemplateGroupViewModel decoTemplateGroup in result.RightToSeq())
|
||||
{
|
||||
TreeItems.Add(new DecoTemplateTreeItemViewModel(decoTemplateGroup));
|
||||
_decoTemplateGroupName = null;
|
||||
|
||||
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddDecoTemplate()
|
||||
{
|
||||
if (_selectedDecoTemplateGroup is not null && !string.IsNullOrWhiteSpace(_decoTemplateName))
|
||||
{
|
||||
Either<BaseError, DecoTemplateViewModel> result = await Mediator.Send(new CreateDecoTemplate(_selectedDecoTemplateGroup.Id, _decoTemplateName), _cts.Token);
|
||||
|
||||
foreach (BaseError error in result.LeftToSeq())
|
||||
{
|
||||
Snackbar.Add(error.Value, Severity.Error);
|
||||
Logger.LogError("Unexpected error adding deco template: {Error}", error.Value);
|
||||
}
|
||||
|
||||
foreach (DecoTemplateViewModel decoTemplate in result.RightToSeq())
|
||||
{
|
||||
foreach (DecoTemplateTreeItemViewModel item in TreeItems.Where(item => item.DecoTemplateGroupId == _selectedDecoTemplateGroup.Id))
|
||||
{
|
||||
item.TreeItems.Add(new DecoTemplateTreeItemViewModel(decoTemplate));
|
||||
}
|
||||
|
||||
_decoTemplateName = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<S.HashSet<DecoTemplateTreeItemViewModel>> LoadServerData(DecoTemplateTreeItemViewModel parentNode)
|
||||
{
|
||||
foreach (int decoTemplateGroupId in Optional(parentNode.DecoTemplateGroupId))
|
||||
{
|
||||
List<DecoTemplateViewModel> result = await Mediator.Send(new GetDecoTemplatesByDecoTemplateGroupId(decoTemplateGroupId), _cts.Token);
|
||||
foreach (DecoTemplateViewModel decoTemplate in result)
|
||||
{
|
||||
parentNode.TreeItems.Add(new DecoTemplateTreeItemViewModel(decoTemplate));
|
||||
}
|
||||
}
|
||||
|
||||
return parentNode.TreeItems;
|
||||
}
|
||||
|
||||
private async Task DeleteItem(DecoTemplateTreeItemViewModel treeItem)
|
||||
{
|
||||
foreach (int decoTemplateGroupId in Optional(treeItem.DecoTemplateGroupId))
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "Deco Template group" }, { "EntityName", treeItem.Text } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Deco Template Group", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await Mediator.Send(new DeleteDecoTemplateGroup(decoTemplateGroupId), _cts.Token);
|
||||
TreeItems.RemoveWhere(i => i.DecoTemplateGroupId == decoTemplateGroupId);
|
||||
|
||||
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (int decoTemplateId in Optional(treeItem.DecoTemplateId))
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "Deco Template" }, { "EntityName", treeItem.Text } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Deco Template", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await Mediator.Send(new DeleteDecoTemplate(decoTemplateId), _cts.Token);
|
||||
foreach (DecoTemplateTreeItemViewModel parent in TreeItems)
|
||||
{
|
||||
parent.TreeItems.Remove(treeItem);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
@page "/decos"
|
||||
@using S = System.Collections.Generic
|
||||
@using ErsatzTV.Application.Scheduling
|
||||
@implements IDisposable
|
||||
@inject ILogger<Decos> Logger
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IMediator Mediator
|
||||
@inject IDialogService Dialog
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Decos</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="4">
|
||||
<div style="max-width: 400px;" class="mr-4">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudTextField Class="mt-3 mx-3" Label="Deco Group Name" @bind-Value="_decoGroupName" For="@(() => _decoGroupName)"/>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoGroup())" Class="ml-4 mb-4">
|
||||
Add Deco Group
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
<MudItem xs="4">
|
||||
<div style="max-width: 400px;" class="mb-6">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<div class="mx-4">
|
||||
<MudSelect Label="Deco Group" @bind-Value="_selectedDecoGroup" Class="mt-3">
|
||||
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
|
||||
{
|
||||
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField Class="mt-3" Label="Deco Name" @bind-Value="_decoName" For="@(() => _decoName)"/>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDeco())" Class="ml-4 mb-4">
|
||||
Add Deco
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
<MudItem xs="8">
|
||||
<MudCard>
|
||||
<MudTreeView ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
|
||||
<ItemTemplate Context="item">
|
||||
<MudTreeViewItem Items="@item.TreeItems" Icon="@item.Icon" CanExpand="@item.CanExpand" Value="@item">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudGrid Justify="Justify.FlexStart">
|
||||
<MudItem xs="5">
|
||||
<MudText>@item.Text</MudText>
|
||||
</MudItem>
|
||||
@if (!string.IsNullOrWhiteSpace(item.EndText))
|
||||
{
|
||||
<MudItem xs="6">
|
||||
<MudText>@item.EndText</MudText>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
<div style="justify-self: end;">
|
||||
@foreach (int decoId in Optional(item.DecoId))
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Href="@($"decos/{decoId}")"/>
|
||||
}
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="@(_ => DeleteItem(item))"/>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private S.HashSet<DecoTreeItemViewModel> TreeItems { get; set; } = [];
|
||||
private List<DecoGroupViewModel> _decoGroups = [];
|
||||
private DecoGroupViewModel _selectedDecoGroup;
|
||||
private string _decoGroupName;
|
||||
private string _decoName;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await ReloadDecoGroups();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task ReloadDecoGroups()
|
||||
{
|
||||
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
|
||||
TreeItems = _decoGroups.Map(g => new DecoTreeItemViewModel(g)).ToHashSet();
|
||||
}
|
||||
|
||||
private async Task AddDecoGroup()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_decoGroupName))
|
||||
{
|
||||
Either<BaseError, DecoGroupViewModel> result = await Mediator.Send(new CreateDecoGroup(_decoGroupName), _cts.Token);
|
||||
|
||||
foreach (BaseError error in result.LeftToSeq())
|
||||
{
|
||||
Snackbar.Add(error.Value, Severity.Error);
|
||||
Logger.LogError("Unexpected error adding deco group: {Error}", error.Value);
|
||||
}
|
||||
|
||||
foreach (DecoGroupViewModel decoGroup in result.RightToSeq())
|
||||
{
|
||||
TreeItems.Add(new DecoTreeItemViewModel(decoGroup));
|
||||
_decoGroupName = null;
|
||||
|
||||
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddDeco()
|
||||
{
|
||||
if (_selectedDecoGroup is not null && !string.IsNullOrWhiteSpace(_decoName))
|
||||
{
|
||||
Either<BaseError, DecoViewModel> result = await Mediator.Send(new CreateDeco(_selectedDecoGroup.Id, _decoName), _cts.Token);
|
||||
|
||||
foreach (BaseError error in result.LeftToSeq())
|
||||
{
|
||||
Snackbar.Add(error.Value, Severity.Error);
|
||||
Logger.LogError("Unexpected error adding deco: {Error}", error.Value);
|
||||
}
|
||||
|
||||
foreach (DecoViewModel deco in result.RightToSeq())
|
||||
{
|
||||
foreach (DecoTreeItemViewModel item in TreeItems.Where(item => item.DecoGroupId == _selectedDecoGroup.Id))
|
||||
{
|
||||
item.TreeItems.Add(new DecoTreeItemViewModel(deco));
|
||||
}
|
||||
|
||||
_decoName = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<S.HashSet<DecoTreeItemViewModel>> LoadServerData(DecoTreeItemViewModel parentNode)
|
||||
{
|
||||
foreach (int decoGroupId in Optional(parentNode.DecoGroupId))
|
||||
{
|
||||
List<DecoViewModel> result = await Mediator.Send(new GetDecosByDecoGroupId(decoGroupId), _cts.Token);
|
||||
foreach (DecoViewModel deco in result)
|
||||
{
|
||||
parentNode.TreeItems.Add(new DecoTreeItemViewModel(deco));
|
||||
}
|
||||
}
|
||||
|
||||
return parentNode.TreeItems;
|
||||
}
|
||||
|
||||
private async Task DeleteItem(DecoTreeItemViewModel treeItem)
|
||||
{
|
||||
foreach (int decoGroupId in Optional(treeItem.DecoGroupId))
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "deco group" }, { "EntityName", treeItem.Text } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Deco Group", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await Mediator.Send(new DeleteDecoGroup(decoGroupId), _cts.Token);
|
||||
TreeItems.RemoveWhere(i => i.DecoGroupId == decoGroupId);
|
||||
|
||||
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (int decoId in Optional(treeItem.DecoId))
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "deco" }, { "EntityName", treeItem.Text } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Deco", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await Mediator.Send(new DeleteDeco(decoId), _cts.Token);
|
||||
foreach (DecoTreeItemViewModel parent in TreeItems)
|
||||
{
|
||||
parent.TreeItems.Remove(treeItem);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
<MudTable T="PlayoutTemplateEditViewModel" Hover="true" Items="_items.OrderBy(i => i.Index)" Dense="true" SelectedItem="@_selectedItem" SelectedItemChanged="@(vm => SelectedItemChanged(vm))">
|
||||
<MudTable T="PlayoutTemplateEditViewModel" Class="mt-4" Hover="true" Items="_items.OrderBy(i => i.Index)" Dense="true" SelectedItem="@_selectedItem" SelectedItemChanged="@(vm => SelectedItemChanged(vm))">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">@_channelName Templates</MudText>
|
||||
<MudSpacer/>
|
||||
@@ -109,6 +109,24 @@
|
||||
</MudSelect>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
<MudCard Class="mt-4">
|
||||
<MudCardContent>
|
||||
<MudSelect T="DecoTemplateGroupViewModel" Label="Deco Template Group" Value="@_selectedDecoTemplateGroup" ValueChanged="@(vm => UpdateDecoTemplateGroupItems(vm))">
|
||||
@foreach (DecoTemplateGroupViewModel decoTemplateGroup in _decoTemplateGroups)
|
||||
{
|
||||
<MudSelectItem Value="@decoTemplateGroup">
|
||||
@decoTemplateGroup.Name
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect Label="Deco Template" @bind-Value="_selectedItem.DecoTemplate" For="@(() => _selectedItem.DecoTemplate)" Clearable="true">
|
||||
@foreach (DecoTemplateViewModel decoTemplate in _decoTemplates)
|
||||
{
|
||||
<MudSelectItem Value="@decoTemplate">@decoTemplate.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
<MudCard Class="mt-4">
|
||||
<MudCardContent>
|
||||
<MudCheckBox T="bool" Class="mt-3" Label="Limit To Date Range"
|
||||
@@ -285,9 +303,13 @@
|
||||
private readonly List<TemplateGroupViewModel> _templateGroups = [];
|
||||
private readonly List<TemplateViewModel> _templates = [];
|
||||
|
||||
private readonly List<DecoTemplateGroupViewModel> _decoTemplateGroups = [];
|
||||
private readonly List<DecoTemplateViewModel> _decoTemplates = [];
|
||||
|
||||
private string _channelName;
|
||||
private List<PlayoutTemplateEditViewModel> _items = [];
|
||||
private TemplateGroupViewModel _selectedGroup;
|
||||
private DecoTemplateGroupViewModel _selectedDecoTemplateGroup;
|
||||
private PlayoutTemplateEditViewModel _selectedItem;
|
||||
private readonly List<CalendarItem> _previewItems = [];
|
||||
|
||||
@@ -306,6 +328,9 @@
|
||||
_templateGroups.Clear();
|
||||
_templateGroups.AddRange(await Mediator.Send(new GetAllTemplateGroups(), _cts.Token));
|
||||
|
||||
_decoTemplateGroups.Clear();
|
||||
_decoTemplateGroups.AddRange(await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token));
|
||||
|
||||
List<PlayoutTemplateViewModel> results = await Mediator.Send(new GetPlayoutTemplates(Id), _cts.Token);
|
||||
_items = results.Map(ProjectToEditViewModel).ToList();
|
||||
if (_items.Count == 1)
|
||||
@@ -319,6 +344,7 @@
|
||||
{
|
||||
Id = item.Id,
|
||||
Template = item.Template,
|
||||
DecoTemplate = item.DecoTemplate,
|
||||
Index = item.Index,
|
||||
DaysOfWeek = item.DaysOfWeek.ToList(),
|
||||
DaysOfMonth = item.DaysOfMonth.ToList(),
|
||||
@@ -338,6 +364,14 @@
|
||||
_templates.AddRange(await Mediator.Send(new GetTemplatesByTemplateGroupId(_selectedGroup.Id), _cts.Token));
|
||||
}
|
||||
|
||||
private async Task UpdateDecoTemplateGroupItems(DecoTemplateGroupViewModel decoTemplateGroup)
|
||||
{
|
||||
_selectedDecoTemplateGroup = decoTemplateGroup;
|
||||
|
||||
_decoTemplates.Clear();
|
||||
_decoTemplates.AddRange(await Mediator.Send(new GetDecoTemplatesByDecoTemplateGroupId(_selectedDecoTemplateGroup.Id), _cts.Token));
|
||||
}
|
||||
|
||||
private async Task SelectedItemChanged(PlayoutTemplateEditViewModel template)
|
||||
{
|
||||
_selectedItem = template;
|
||||
@@ -346,6 +380,11 @@
|
||||
{
|
||||
await UpdateTemplateGroupItems(group);
|
||||
}
|
||||
|
||||
foreach (DecoTemplateGroupViewModel group in Optional(_decoTemplateGroups.Find(group => group.Id == _selectedItem.DecoTemplate?.DecoTemplateGroupId)))
|
||||
{
|
||||
await UpdateDecoTemplateGroupItems(group);
|
||||
}
|
||||
}
|
||||
|
||||
private void DayOfWeekChanged(DayOfWeek dayOfWeek, bool isChecked)
|
||||
@@ -486,6 +525,7 @@
|
||||
item.Id,
|
||||
item.Index,
|
||||
item.Template.Id,
|
||||
item.DecoTemplate?.Id,
|
||||
item.DaysOfWeek,
|
||||
item.DaysOfMonth,
|
||||
item.MonthsOfYear,
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
</div>
|
||||
@if (context.PlayoutType == ProgramSchedulePlayoutType.Flood)
|
||||
{
|
||||
<div style="width: 48px"></div>
|
||||
<div style="width: 48px"></div>
|
||||
<MudTooltip Text="Edit Alternate Schedules">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
@@ -105,8 +103,6 @@
|
||||
}
|
||||
else if (context.PlayoutType == ProgramSchedulePlayoutType.ExternalJson)
|
||||
{
|
||||
<div style="width: 48px"></div>
|
||||
<div style="width: 48px"></div>
|
||||
<MudTooltip Text="Edit External Json File">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
@@ -118,22 +114,10 @@
|
||||
}
|
||||
else if (context.PlayoutType == ProgramSchedulePlayoutType.Block)
|
||||
{
|
||||
<MudTooltip Text="Erase Items">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Clear"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
OnClick="@(_ => EraseItems(context))">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Erase Items and History">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ClearAll"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
OnClick="@(_ => EraseHistory(context))">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Edit Templates">
|
||||
<MudTooltip Text="Edit Playout">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
Link="@($"playouts/{context.PlayoutId}/templates")">
|
||||
Link="@($"playouts/block/{context.PlayoutId}")">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Reset Playout">
|
||||
|
||||
@@ -100,13 +100,13 @@
|
||||
<MudNavLink Href="channels">Channels</MudNavLink>
|
||||
<MudNavLink Href="ffmpeg">FFmpeg Profiles</MudNavLink>
|
||||
<MudNavLink Href="watermarks">Watermarks</MudNavLink>
|
||||
<MudNavGroup Title="Media Sources" Expanded="true">
|
||||
<MudNavGroup Title="Media Sources">
|
||||
<MudNavLink Href="media/sources/local">Local</MudNavLink>
|
||||
<MudNavLink Href="media/sources/emby">Emby</MudNavLink>
|
||||
<MudNavLink Href="media/sources/jellyfin">Jellyfin</MudNavLink>
|
||||
<MudNavLink Href="media/sources/plex">Plex</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavGroup Title="Media" Expanded="true">
|
||||
<MudNavGroup Title="Media">
|
||||
<MudNavLink Href="media/libraries">Libraries</MudNavLink>
|
||||
<MudNavLink Href="media/trash">Trash</MudNavLink>
|
||||
<MudNavLink Href="media/tv/shows">TV Shows</MudNavLink>
|
||||
@@ -116,15 +116,17 @@
|
||||
<MudNavLink Href="media/music/songs">Songs</MudNavLink>
|
||||
<MudNavLink Href="media/browser/images">Images</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavGroup Title="Lists" Expanded="true">
|
||||
<MudNavGroup Title="Lists">
|
||||
<MudNavLink Href="media/collections">Collections</MudNavLink>
|
||||
<MudNavLink Href="media/trakt/lists">Trakt Lists</MudNavLink>
|
||||
<MudNavLink Href="media/filler/presets">Filler Presets</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavGroup Title="Scheduling" Expanded="true">
|
||||
<MudNavGroup Title="Scheduling">
|
||||
<MudNavLink Href="schedules">Schedules</MudNavLink>
|
||||
<MudNavLink Href="blocks">Blocks</MudNavLink>
|
||||
<MudNavLink Href="templates">Templates</MudNavLink>
|
||||
<MudNavLink Href="decos">Decos</MudNavLink>
|
||||
<MudNavLink Href="deco-templates">Deco Templates</MudNavLink>
|
||||
<MudNavLink Href="playouts">Playouts</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavLink Href="settings">Settings</MudNavLink>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.ViewModels;
|
||||
|
||||
public class DecoEditViewModel
|
||||
{
|
||||
public int DecoGroupId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int? WatermarkId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Heron.MudCalendar;
|
||||
|
||||
namespace ErsatzTV.ViewModels;
|
||||
|
||||
public class DecoTemplateItemEditViewModel : CalendarItem
|
||||
{
|
||||
private string _blockName;
|
||||
public int DecoId { get; set; }
|
||||
|
||||
public string DecoName
|
||||
{
|
||||
get => _blockName;
|
||||
set
|
||||
{
|
||||
_blockName = value;
|
||||
Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime LastStart { get; set; }
|
||||
public DateTime? LastEnd { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.ViewModels;
|
||||
|
||||
public class DecoTemplateItemsEditViewModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<DecoTemplateItemEditViewModel> Items { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ErsatzTV.Application.Scheduling;
|
||||
using MudBlazor;
|
||||
using S = System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.ViewModels;
|
||||
|
||||
public class DecoTemplateTreeItemViewModel
|
||||
{
|
||||
public DecoTemplateTreeItemViewModel(DecoTemplateGroupViewModel decoTemplateGroup)
|
||||
{
|
||||
Text = decoTemplateGroup.Name;
|
||||
TreeItems = [];
|
||||
CanExpand = decoTemplateGroup.DecoTemplateCount > 0;
|
||||
DecoTemplateGroupId = decoTemplateGroup.Id;
|
||||
Icon = Icons.Material.Filled.Folder;
|
||||
}
|
||||
|
||||
public DecoTemplateTreeItemViewModel(DecoTemplateViewModel decoTemplate)
|
||||
{
|
||||
Text = decoTemplate.Name;
|
||||
TreeItems = [];
|
||||
CanExpand = false;
|
||||
DecoTemplateId = decoTemplate.Id;
|
||||
}
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public string Icon { get; }
|
||||
|
||||
public bool CanExpand { get; }
|
||||
|
||||
public int? DecoTemplateId { get; }
|
||||
|
||||
public int? DecoTemplateGroupId { get; }
|
||||
|
||||
public S.HashSet<DecoTemplateTreeItemViewModel> TreeItems { get; }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using ErsatzTV.Application.Scheduling;
|
||||
using MudBlazor;
|
||||
using S = System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.ViewModels;
|
||||
|
||||
public class DecoTreeItemViewModel
|
||||
{
|
||||
public DecoTreeItemViewModel(DecoGroupViewModel decoGroup)
|
||||
{
|
||||
Text = decoGroup.Name;
|
||||
EndText = string.Empty;
|
||||
TreeItems = [];
|
||||
CanExpand = decoGroup.DecoCount > 0;
|
||||
DecoGroupId = decoGroup.Id;
|
||||
Icon = Icons.Material.Filled.Folder;
|
||||
}
|
||||
|
||||
public DecoTreeItemViewModel(DecoViewModel deco)
|
||||
{
|
||||
Text = deco.Name;
|
||||
TreeItems = [];
|
||||
CanExpand = false;
|
||||
DecoId = deco.Id;
|
||||
}
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public string EndText { get; }
|
||||
|
||||
public string Icon { get; }
|
||||
|
||||
public bool CanExpand { get; }
|
||||
|
||||
public int? DecoId { get; }
|
||||
|
||||
public int? DecoGroupId { get; }
|
||||
|
||||
public S.HashSet<DecoTreeItemViewModel> TreeItems { get; }
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using ErsatzTV.Annotations;
|
||||
using ErsatzTV.Application.Scheduling;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
@@ -13,6 +14,8 @@ public class PlayoutTemplateEditViewModel
|
||||
public int Id { get; set; }
|
||||
public int Index { get; set; }
|
||||
public TemplateViewModel Template { get; set; }
|
||||
[CanBeNull]
|
||||
public DecoTemplateViewModel DecoTemplate { get; set; }
|
||||
public List<DayOfWeek> DaysOfWeek { get; set; }
|
||||
public List<int> DaysOfMonth { get; set; }
|
||||
public List<int> MonthsOfYear { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user