mirror of https://github.com/ErsatzTV/ErsatzTV.git
45 changed files with 7095 additions and 507 deletions
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Commands |
||||
{ |
||||
public record CopyWatermark |
||||
(int WatermarkId, string Name) : IRequest<Either<BaseError, WatermarkViewModel>>; |
||||
} |
||||
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.ChangeTracking; |
||||
using static ErsatzTV.Application.Watermarks.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Commands |
||||
{ |
||||
public class CopyWatermarkHandler : |
||||
IRequestHandler<CopyWatermark, Either<BaseError, WatermarkViewModel>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public CopyWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public Task<Either<BaseError, WatermarkViewModel>> Handle( |
||||
CopyWatermark request, |
||||
CancellationToken cancellationToken) => |
||||
Validate(request) |
||||
.MapT(PerformCopy) |
||||
.Bind(v => v.ToEitherAsync()); |
||||
|
||||
private async Task<WatermarkViewModel> PerformCopy(CopyWatermark request) |
||||
{ |
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||
ChannelWatermark channelWatermark = await dbContext.ChannelWatermarks.FindAsync(request.WatermarkId); |
||||
|
||||
PropertyValues values = dbContext.Entry(channelWatermark).CurrentValues.Clone(); |
||||
values["Id"] = 0; |
||||
|
||||
var clone = new ChannelWatermark(); |
||||
await dbContext.AddAsync(clone); |
||||
dbContext.Entry(clone).CurrentValues.SetValues(values); |
||||
clone.Name = request.Name; |
||||
|
||||
await dbContext.SaveChangesAsync(); |
||||
|
||||
return ProjectToViewModel(clone); |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, CopyWatermark>> Validate(CopyWatermark request) => |
||||
ValidateName(request).AsTask().MapT(_ => request); |
||||
|
||||
private static Validation<BaseError, string> ValidateName(CopyWatermark request) => |
||||
request.NotEmpty(x => x.Name) |
||||
.Bind(_ => request.NotLongerThan(50)(x => x.Name)); |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Commands |
||||
{ |
||||
public record CreateWatermark( |
||||
string Name, |
||||
string Image, |
||||
ChannelWatermarkMode Mode, |
||||
ChannelWatermarkImageSource ImageSource, |
||||
ChannelWatermarkLocation Location, |
||||
ChannelWatermarkSize Size, |
||||
int Width, |
||||
int HorizontalMargin, |
||||
int VerticalMargin, |
||||
int FrequencyMinutes, |
||||
int DurationSeconds, |
||||
int Opacity) : IRequest<Either<BaseError, CreateWatermarkResult>>; |
||||
|
||||
public record CreateWatermarkResult(int WatermarkId) : EntityIdResult(WatermarkId); |
||||
} |
||||
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Commands |
||||
{ |
||||
public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<BaseError, CreateWatermarkResult>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public CreateWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public async Task<Either<BaseError, CreateWatermarkResult>> Handle( |
||||
CreateWatermark request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||
Validation<BaseError, ChannelWatermark> validation = Validate(request); |
||||
return await validation.Apply(profile => PersistChannelWatermark(dbContext, profile)); |
||||
} |
||||
|
||||
private static async Task<CreateWatermarkResult> PersistChannelWatermark( |
||||
TvContext dbContext, |
||||
ChannelWatermark watermark) |
||||
{ |
||||
await dbContext.ChannelWatermarks.AddAsync(watermark); |
||||
await dbContext.SaveChangesAsync(); |
||||
return new CreateWatermarkResult(watermark.Id); |
||||
} |
||||
|
||||
private static Validation<BaseError, ChannelWatermark> Validate(CreateWatermark request) => |
||||
ValidateName(request) |
||||
.Map( |
||||
_ => new ChannelWatermark |
||||
{ |
||||
Name = request.Name, |
||||
Image = request.ImageSource == ChannelWatermarkImageSource.Custom ? request.Image : null, |
||||
Mode = request.Mode, |
||||
ImageSource = request.ImageSource, |
||||
Location = request.Location, |
||||
Size = request.Size, |
||||
WidthPercent = request.Width, |
||||
HorizontalMarginPercent = request.HorizontalMargin, |
||||
VerticalMarginPercent = request.VerticalMargin, |
||||
FrequencyMinutes = request.FrequencyMinutes, |
||||
DurationSeconds = request.DurationSeconds, |
||||
Opacity = request.Opacity |
||||
}); |
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateWatermark request) => |
||||
request.NotEmpty(x => x.Name) |
||||
.Bind(_ => request.NotLongerThan(50)(x => x.Name)); |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Commands |
||||
{ |
||||
public record DeleteWatermark(int WatermarkId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using LanguageExt; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Unit = LanguageExt.Unit; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Commands |
||||
{ |
||||
public class DeleteWatermarkHandler : MediatR.IRequestHandler<DeleteWatermark, Either<BaseError, Unit>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public DeleteWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle( |
||||
DeleteWatermark request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||
Validation<BaseError, ChannelWatermark> validation = await WatermarkMustExist(dbContext, request); |
||||
return await validation.Apply(p => DoDeletion(dbContext, p)); |
||||
} |
||||
|
||||
private static async Task<Unit> DoDeletion(TvContext dbContext, ChannelWatermark watermark) |
||||
{ |
||||
await dbContext.Database.ExecuteSqlRawAsync( |
||||
$"UPDATE Channel SET WatermarkId = NULL WHERE WatermarkId = {watermark.Id}"); |
||||
dbContext.ChannelWatermarks.Remove(watermark); |
||||
await dbContext.SaveChangesAsync(); |
||||
return Unit.Default; |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, ChannelWatermark>> WatermarkMustExist( |
||||
TvContext dbContext, |
||||
DeleteWatermark request) => |
||||
dbContext.ChannelWatermarks |
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.WatermarkId) |
||||
.Map(o => o.ToValidation<BaseError>($"Watermark {request.WatermarkId} does not exist")); |
||||
} |
||||
} |
||||
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Commands |
||||
{ |
||||
public record UpdateWatermark( |
||||
int Id, |
||||
string Name, |
||||
string Image, |
||||
ChannelWatermarkMode Mode, |
||||
ChannelWatermarkImageSource ImageSource, |
||||
ChannelWatermarkLocation Location, |
||||
ChannelWatermarkSize Size, |
||||
int Width, |
||||
int HorizontalMargin, |
||||
int VerticalMargin, |
||||
int FrequencyMinutes, |
||||
int DurationSeconds, |
||||
int Opacity) : IRequest<Either<BaseError, UpdateWatermarkResult>>; |
||||
|
||||
public record UpdateWatermarkResult(int WatermarkId) : EntityIdResult(WatermarkId); |
||||
} |
||||
@ -0,0 +1,67 @@
@@ -0,0 +1,67 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Commands |
||||
{ |
||||
public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<BaseError, UpdateWatermarkResult>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public UpdateWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public async Task<Either<BaseError, UpdateWatermarkResult>> Handle( |
||||
UpdateWatermark request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||
Validation<BaseError, ChannelWatermark> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request)); |
||||
} |
||||
|
||||
private static async Task<UpdateWatermarkResult> ApplyUpdateRequest( |
||||
TvContext dbContext, |
||||
ChannelWatermark p, |
||||
UpdateWatermark update) |
||||
{ |
||||
p.Name = update.Name; |
||||
p.Image = update.ImageSource == ChannelWatermarkImageSource.Custom ? update.Image : null; |
||||
p.Mode = update.Mode; |
||||
p.ImageSource = update.ImageSource; |
||||
p.Location = update.Location; |
||||
p.Size = update.Size; |
||||
p.WidthPercent = update.Width; |
||||
p.HorizontalMarginPercent = update.HorizontalMargin; |
||||
p.VerticalMarginPercent = update.VerticalMargin; |
||||
p.FrequencyMinutes = update.FrequencyMinutes; |
||||
p.DurationSeconds = update.DurationSeconds; |
||||
p.Opacity = update.Opacity; |
||||
await dbContext.SaveChangesAsync(); |
||||
return new UpdateWatermarkResult(p.Id); |
||||
} |
||||
|
||||
private static async Task<Validation<BaseError, ChannelWatermark>> Validate( |
||||
TvContext dbContext, |
||||
UpdateWatermark request) => |
||||
(await WatermarkMustExist(dbContext, request), ValidateName(request)) |
||||
.Apply((watermark, _) => watermark); |
||||
|
||||
private static Task<Validation<BaseError, ChannelWatermark>> WatermarkMustExist( |
||||
TvContext dbContext, |
||||
UpdateWatermark updateWatermark) => |
||||
dbContext.ChannelWatermarks |
||||
.SelectOneAsync(p => p.Id, p => p.Id == updateWatermark.Id) |
||||
.Map(o => o.ToValidation<BaseError>("Watermark does not exist.")); |
||||
|
||||
private static Validation<BaseError, string> ValidateName(UpdateWatermark updateWatermark) => |
||||
updateWatermark.NotEmpty(x => x.Name) |
||||
.Bind(_ => updateWatermark.NotLongerThan(50)(x => x.Name)); |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks |
||||
{ |
||||
internal static class Mapper |
||||
{ |
||||
public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) => |
||||
new( |
||||
watermark.Id, |
||||
watermark.Image, |
||||
watermark.Name, |
||||
watermark.Mode, |
||||
watermark.ImageSource, |
||||
watermark.Location, |
||||
watermark.Size, |
||||
watermark.WidthPercent, |
||||
watermark.HorizontalMarginPercent, |
||||
watermark.VerticalMarginPercent, |
||||
watermark.FrequencyMinutes, |
||||
watermark.DurationSeconds, |
||||
watermark.Opacity); |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Queries |
||||
{ |
||||
public record GetAllWatermarks : IRequest<List<WatermarkViewModel>>; |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using static ErsatzTV.Application.Watermarks.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Queries |
||||
{ |
||||
public class GetAllWatermarksHandler : IRequestHandler<GetAllWatermarks, List<WatermarkViewModel>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public GetAllWatermarksHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public async Task<List<WatermarkViewModel>> Handle( |
||||
GetAllWatermarks request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||
return await dbContext.ChannelWatermarks |
||||
.ToListAsync(cancellationToken) |
||||
.Map(list => list.Map(ProjectToViewModel).ToList()); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
using LanguageExt; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Queries |
||||
{ |
||||
public record GetWatermarkById(int Id) : IRequest<Option<WatermarkViewModel>>; |
||||
} |
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using static ErsatzTV.Application.Watermarks.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks.Queries |
||||
{ |
||||
public class GetWatermarkByIdHandler : IRequestHandler<GetWatermarkById, Option<WatermarkViewModel>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
|
||||
public GetWatermarkByIdHandler(IDbContextFactory<TvContext> dbContextFactory) => |
||||
_dbContextFactory = dbContextFactory; |
||||
|
||||
public async Task<Option<WatermarkViewModel>> Handle( |
||||
GetWatermarkById request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||
return await dbContext.ChannelWatermarks |
||||
.SelectOneAsync(w => w.Id, w => w.Id == request.Id) |
||||
.MapT(ProjectToViewModel); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.Watermarks |
||||
{ |
||||
public record WatermarkViewModel( |
||||
int Id, |
||||
string Image, |
||||
string Name, |
||||
ChannelWatermarkMode Mode, |
||||
ChannelWatermarkImageSource ImageSource, |
||||
ChannelWatermarkLocation Location, |
||||
ChannelWatermarkSize Size, |
||||
int Width, |
||||
int HorizontalMargin, |
||||
int VerticalMargin, |
||||
int FrequencyMinutes, |
||||
int DurationSeconds, |
||||
int Opacity |
||||
); |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations |
||||
{ |
||||
public partial class Add_ChannelWatermarkNameImage : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Channel_ChannelWatermark_WatermarkId", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Channel_WatermarkId", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "Image", |
||||
table: "ChannelWatermark", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "Name", |
||||
table: "ChannelWatermark", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Channel_WatermarkId", |
||||
table: "Channel", |
||||
column: "WatermarkId"); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Channel_ChannelWatermark_WatermarkId", |
||||
table: "Channel", |
||||
column: "WatermarkId", |
||||
principalTable: "ChannelWatermark", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Restrict); |
||||
} |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Channel_ChannelWatermark_WatermarkId", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Channel_WatermarkId", |
||||
table: "Channel"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "Image", |
||||
table: "ChannelWatermark"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "Name", |
||||
table: "ChannelWatermark"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Channel_WatermarkId", |
||||
table: "Channel", |
||||
column: "WatermarkId", |
||||
unique: true); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Channel_ChannelWatermark_WatermarkId", |
||||
table: "Channel", |
||||
column: "WatermarkId", |
||||
principalTable: "ChannelWatermark", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
} |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations |
||||
{ |
||||
public partial class Add_ChannelWatermarkImageSource : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<int>( |
||||
name: "ImageSource", |
||||
table: "ChannelWatermark", |
||||
type: "INTEGER", |
||||
nullable: false, |
||||
defaultValue: 0); |
||||
} |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "ImageSource", |
||||
table: "ChannelWatermark"); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,238 @@
@@ -0,0 +1,238 @@
|
||||
@page "/watermarks/{Id:int}" |
||||
@page "/watermarks/add" |
||||
@using ErsatzTV.Application.Images.Commands |
||||
@using ErsatzTV.Application.Watermarks |
||||
@using ErsatzTV.Application.Watermarks.Queries |
||||
@inject NavigationManager _navigationManager |
||||
@inject ILogger<WatermarkEditor> _logger |
||||
@inject ISnackbar _snackbar |
||||
@inject IMediator _mediator |
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> |
||||
<div style="max-width: 400px;"> |
||||
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> |
||||
<FluentValidator/> |
||||
<MudCard> |
||||
<MudCardHeader> |
||||
<CardHeaderContent> |
||||
<MudText Typo="Typo.h5">@(IsEdit ? "Edit Watermark" : "Add Watermark")</MudText> |
||||
</CardHeaderContent> |
||||
</MudCardHeader> |
||||
<MudCardContent> |
||||
<MudTextField Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> |
||||
<MudSelect Class="mt-3" Label="Mode" @bind-Value="_model.Mode" |
||||
For="@(() => _model.Mode)"> |
||||
<MudSelectItem Value="@(ChannelWatermarkMode.None)">None</MudSelectItem> |
||||
<MudSelectItem Value="@(ChannelWatermarkMode.Permanent)">Permanent</MudSelectItem> |
||||
<MudSelectItem Value="@(ChannelWatermarkMode.Intermittent)">Intermittent</MudSelectItem> |
||||
</MudSelect> |
||||
<MudSelect Class="mt-3" Label="Image Source" @bind-Value="_model.ImageSource" |
||||
For="@(() => _model.ImageSource)" |
||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None)"> |
||||
<MudSelectItem Value="@(ChannelWatermarkImageSource.Custom)">Custom</MudSelectItem> |
||||
<MudSelectItem Value="@(ChannelWatermarkImageSource.ChannelLogo)">Channel Logo</MudSelectItem> |
||||
</MudSelect> |
||||
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center"> |
||||
<MudItem xs="6"> |
||||
<InputFile id="watermarkFileInput" OnChange="UploadWatermark" style="display: none;"/> |
||||
@if (!string.IsNullOrWhiteSpace(_model.Image) && _model.ImageSource != ChannelWatermarkImageSource.ChannelLogo) |
||||
{ |
||||
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Image}")" Style="max-height: 50px"/> |
||||
} |
||||
<ValidationMessage For="@(() => _model.Image)" style="color: #f44336!important;"/> |
||||
</MudItem> |
||||
<MudItem xs="6"> |
||||
<MudButton Class="ml-auto" HtmlTag="label" |
||||
Variant="Variant.Filled" |
||||
Color="Color.Primary" |
||||
StartIcon="@Icons.Material.Filled.CloudUpload" |
||||
Disabled="@(_model.ImageSource == ChannelWatermarkImageSource.ChannelLogo)" |
||||
for="watermarkFileInput"> |
||||
Upload Image |
||||
</MudButton> |
||||
</MudItem> |
||||
</MudGrid> |
||||
<MudSelect Class="mt-3" Label="Location" @bind-Value="_model.Location" |
||||
For="@(() => _model.Location)" |
||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None)"> |
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.BottomRight)">Bottom Right</MudSelectItem> |
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.BottomLeft)">Bottom Left</MudSelectItem> |
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.TopRight)">Top Right</MudSelectItem> |
||||
<MudSelectItem Value="@(ChannelWatermarkLocation.TopLeft)">Top Left</MudSelectItem> |
||||
</MudSelect> |
||||
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center"> |
||||
<MudItem xs="6"> |
||||
<MudSelect Label="Size" @bind-Value="_model.Size" |
||||
For="@(() => _model.Size)" |
||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None)"> |
||||
<MudSelectItem Value="@(ChannelWatermarkSize.Scaled)">Scaled</MudSelectItem> |
||||
<MudSelectItem Value="@(ChannelWatermarkSize.ActualSize)">Actual Size</MudSelectItem> |
||||
</MudSelect> |
||||
</MudItem> |
||||
<MudItem xs="6"> |
||||
<MudTextField Label="Width" @bind-Value="_model.Width" |
||||
For="@(() => _model.Width)" |
||||
Adornment="Adornment.End" |
||||
AdornmentText="%" |
||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None || _model.Size == ChannelWatermarkSize.ActualSize)" |
||||
Immediate="true"/> |
||||
</MudItem> |
||||
</MudGrid> |
||||
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center"> |
||||
<MudItem xs="6"> |
||||
<MudTextField Label="Horizontal Margin" @bind-Value="_model.HorizontalMargin" |
||||
For="@(() => _model.HorizontalMargin)" |
||||
Adornment="Adornment.End" |
||||
AdornmentText="%" |
||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None)" |
||||
Immediate="true"/> |
||||
</MudItem> |
||||
<MudItem xs="6"> |
||||
<MudTextField Label="Vertical Margin" @bind-Value="_model.VerticalMargin" |
||||
For="@(() => _model.VerticalMargin)" |
||||
Adornment="Adornment.End" |
||||
AdornmentText="%" |
||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None)" |
||||
Immediate="true"/> |
||||
</MudItem> |
||||
</MudGrid> |
||||
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center"> |
||||
<MudItem xs="6"> |
||||
<MudSelect Label="Frequency" @bind-Value="_model.FrequencyMinutes" |
||||
For="@(() => _model.FrequencyMinutes)" |
||||
Disabled="@(_model.Mode != ChannelWatermarkMode.Intermittent)"> |
||||
<MudSelectItem Value="5">5 minutes</MudSelectItem> |
||||
<MudSelectItem Value="10">10 minutes</MudSelectItem> |
||||
<MudSelectItem Value="15">15 minutes</MudSelectItem> |
||||
<MudSelectItem Value="20">20 minutes</MudSelectItem> |
||||
<MudSelectItem Value="30">30 minutes</MudSelectItem> |
||||
<MudSelectItem Value="60">60 minutes</MudSelectItem> |
||||
</MudSelect> |
||||
</MudItem> |
||||
<MudItem xs="6"> |
||||
<MudTextField Label="Duration" @bind-Value="_model.DurationSeconds" |
||||
For="@(() => _model.DurationSeconds)" |
||||
Adornment="Adornment.End" |
||||
AdornmentText="seconds" |
||||
Disabled="@(_model.Mode != ChannelWatermarkMode.Intermittent)" |
||||
Immediate="true"/> |
||||
</MudItem> |
||||
</MudGrid> |
||||
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center"> |
||||
<MudItem xs="6"> |
||||
<MudTextField Label="Opacity" @bind-Value="_model.Opacity" |
||||
For="@(() => _model.Opacity)" |
||||
Adornment="Adornment.End" |
||||
AdornmentText="%" |
||||
Disabled="@(_model.Mode == ChannelWatermarkMode.None)" |
||||
Immediate="true"/> |
||||
</MudItem> |
||||
<MudItem xs="6" /> |
||||
</MudGrid> |
||||
</MudCardContent> |
||||
<MudCardActions> |
||||
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary"> |
||||
@(IsEdit ? "Save Changes" : "Add Watermark") |
||||
</MudButton> |
||||
</MudCardActions> |
||||
</MudCard> |
||||
</EditForm> |
||||
</div> |
||||
</MudContainer> |
||||
|
||||
@code { |
||||
|
||||
[Parameter] |
||||
public int Id { get; set; } |
||||
|
||||
private WatermarkEditViewModel _model = new(); |
||||
private EditContext _editContext; |
||||
private ValidationMessageStore _messageStore; |
||||
|
||||
protected override async Task OnParametersSetAsync() |
||||
{ |
||||
if (IsEdit) |
||||
{ |
||||
Option<WatermarkViewModel> watermark = await _mediator.Send(new GetWatermarkById(Id)); |
||||
watermark.Match( |
||||
watermarkViewModel => _model = new WatermarkEditViewModel(watermarkViewModel), |
||||
() => _navigationManager.NavigateTo("404")); |
||||
} |
||||
else |
||||
{ |
||||
_model = new WatermarkEditViewModel |
||||
{ |
||||
Name = string.Empty, |
||||
Mode = ChannelWatermarkMode.Permanent, |
||||
Image = string.Empty, |
||||
Location = ChannelWatermarkLocation.BottomRight, |
||||
Size = ChannelWatermarkSize.Scaled, |
||||
Width = 15, |
||||
HorizontalMargin = 5, |
||||
VerticalMargin = 5, |
||||
FrequencyMinutes = 15, |
||||
DurationSeconds = 15, |
||||
Opacity = 100, |
||||
}; |
||||
} |
||||
|
||||
_editContext = new EditContext(_model); |
||||
_messageStore = new ValidationMessageStore(_editContext); |
||||
} |
||||
|
||||
private bool IsEdit => Id != 0; |
||||
|
||||
private async Task HandleSubmitAsync() |
||||
{ |
||||
_messageStore.Clear(); |
||||
if (_editContext.Validate()) |
||||
{ |
||||
Seq<BaseError> errorMessage = IsEdit ? |
||||
(await _mediator.Send(_model.ToUpdate())).LeftToSeq() : |
||||
(await _mediator.Send(_model.ToCreate())).LeftToSeq(); |
||||
|
||||
errorMessage.HeadOrNone().Match( |
||||
error => |
||||
{ |
||||
_snackbar.Add("Unexpected error saving watermark"); |
||||
_logger.LogError("Unexpected error saving watermark: {Error}", error.Value); |
||||
}, |
||||
() => _navigationManager.NavigateTo("/watermarks")); |
||||
} |
||||
} |
||||
|
||||
private async Task UploadWatermark(InputFileChangeEventArgs e) |
||||
{ |
||||
try |
||||
{ |
||||
var buffer = new byte[e.File.Size]; |
||||
await e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024).ReadAsync(buffer); |
||||
Either<BaseError, string> maybeCacheFileName = await _mediator |
||||
.Send(new SaveArtworkToDisk(buffer, ArtworkKind.Watermark)); |
||||
maybeCacheFileName.Match( |
||||
relativeFileName => |
||||
{ |
||||
_model.Image = relativeFileName; |
||||
_messageStore.Clear(); |
||||
_editContext.Validate(); |
||||
StateHasChanged(); |
||||
}, |
||||
error => |
||||
{ |
||||
_snackbar.Add($"Unexpected error saving watermark: {error.Value}", Severity.Error); |
||||
_logger.LogError("Unexpected error saving watermark: {Error}", error.Value); |
||||
}); |
||||
} |
||||
catch (IOException) |
||||
{ |
||||
_snackbar.Add("Watermark exceeds maximum allowed file size of 10 MB", Severity.Error); |
||||
_logger.LogError("Watermark exceeds maximum allowed file size of 10 MB"); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_snackbar.Add($"Unexpected error saving watermark: {ex.Message}", Severity.Error); |
||||
_logger.LogError("Unexpected error saving watermark: {Error}", ex.Message); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
|
||||
@page "/watermarks" |
||||
@using ErsatzTV.Application.Watermarks |
||||
@using ErsatzTV.Application.Watermarks.Commands |
||||
@using ErsatzTV.Application.Watermarks.Queries |
||||
@inject IDialogService _dialog |
||||
@inject IMediator _mediator |
||||
@inject ILogger<Watermarks> _logger |
||||
@inject ISnackbar _snackbar |
||||
@inject NavigationManager _navigationManager |
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> |
||||
<MudTable Hover="true" Items="_watermarks"> |
||||
<ToolBarContent> |
||||
<MudText Typo="Typo.h6">Watermarks</MudText> |
||||
</ToolBarContent> |
||||
<ColGroup> |
||||
<col/> |
||||
<col/> |
||||
<col/> |
||||
<col/> |
||||
<col style="width: 180px;"/> |
||||
</ColGroup> |
||||
<HeaderContent> |
||||
<MudTh>Name</MudTh> |
||||
<MudTh>Image</MudTh> |
||||
<MudTh>Mode</MudTh> |
||||
<MudTh>Location</MudTh> |
||||
<MudTh/> |
||||
</HeaderContent> |
||||
<RowTemplate> |
||||
<MudTd DataLabel="Name">@context.Name</MudTd> |
||||
<MudTd DataLabel="Image"> |
||||
@if (!string.IsNullOrWhiteSpace(context.Image)) |
||||
{ |
||||
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{context.Image}")" Style="max-height: 50px"/> |
||||
} |
||||
else if (context.ImageSource == ChannelWatermarkImageSource.ChannelLogo) |
||||
{ |
||||
<MudText>[channel logo]</MudText> |
||||
} |
||||
</MudTd> |
||||
<MudTd DataLabel="Mode"> |
||||
@context.Mode |
||||
</MudTd> |
||||
<MudTd DataLabel="Location"> |
||||
@context.Location |
||||
</MudTd> |
||||
<MudTd> |
||||
<div style="align-items: center; display: flex;"> |
||||
<MudTooltip Text="Edit Watermark"> |
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" |
||||
Link="@($"/watermarks/{context.Id}")"> |
||||
</MudIconButton> |
||||
</MudTooltip> |
||||
<MudTooltip Text="Copy Watermark"> |
||||
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" |
||||
OnClick="@(_ => CopyWatermarkAsync(context))"> |
||||
</MudIconButton> |
||||
</MudTooltip> |
||||
<MudTooltip Text="Delete Watermark"> |
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" |
||||
OnClick="@(_ => DeleteWatermarkAsync(context))"> |
||||
</MudIconButton> |
||||
</MudTooltip> |
||||
</div> |
||||
</MudTd> |
||||
</RowTemplate> |
||||
</MudTable> |
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/watermarks/add" Class="mt-4"> |
||||
Add Watermark |
||||
</MudButton> |
||||
</MudContainer> |
||||
|
||||
@code { |
||||
private List<WatermarkViewModel> _watermarks; |
||||
|
||||
protected override async Task OnParametersSetAsync() => await LoadWatermarksAsync(); |
||||
|
||||
private async Task LoadWatermarksAsync() => |
||||
_watermarks = await _mediator.Send(new GetAllWatermarks()); |
||||
|
||||
private async Task DeleteWatermarkAsync(WatermarkViewModel watermark) |
||||
{ |
||||
var parameters = new DialogParameters { { "EntityType", "watermark" }, { "EntityName", watermark.Name } }; |
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
||||
|
||||
IDialogReference dialog = _dialog.Show<DeleteDialog>("Delete Watermark", parameters, options); |
||||
DialogResult result = await dialog.Result; |
||||
if (!result.Cancelled) |
||||
{ |
||||
await _mediator.Send(new DeleteWatermark(watermark.Id)); |
||||
await LoadWatermarksAsync(); |
||||
} |
||||
} |
||||
|
||||
private async Task CopyWatermarkAsync(WatermarkViewModel watermark) |
||||
{ |
||||
var parameters = new DialogParameters { { "WatermarkId", watermark.Id } }; |
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
||||
|
||||
IDialogReference dialog = _dialog.Show<CopyWatermarkDialog>("Copy Watermark", parameters, options); |
||||
DialogResult dialogResult = await dialog.Result; |
||||
if (!dialogResult.Cancelled && dialogResult.Data is WatermarkViewModel data) |
||||
{ |
||||
_navigationManager.NavigateTo($"/watermarks/{data.Id}"); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
@using ErsatzTV.Application.Watermarks |
||||
@using ErsatzTV.Application.Watermarks.Commands |
||||
@inject IMediator _mediator |
||||
@inject ISnackbar _snackbar |
||||
@inject ILogger<AddToCollectionDialog> _logger |
||||
|
||||
<MudDialog> |
||||
<DialogContent> |
||||
<EditForm Model="@_dummyModel" OnSubmit="@(_ => Submit())"> |
||||
<MudContainer Class="mb-6"> |
||||
<MudText> |
||||
Enter a name for the new Watermark |
||||
</MudText> |
||||
</MudContainer> |
||||
<MudTextFieldString Label="New Watermark Name" |
||||
@bind-Text="@_newWatermarkName" |
||||
Class="mb-6 mx-4"> |
||||
</MudTextFieldString> |
||||
</EditForm> |
||||
</DialogContent> |
||||
<DialogActions> |
||||
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton> |
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit"> |
||||
Copy Watermark |
||||
</MudButton> |
||||
</DialogActions> |
||||
</MudDialog> |
||||
|
||||
@code { |
||||
|
||||
[CascadingParameter] |
||||
MudDialogInstance MudDialog { get; set; } |
||||
|
||||
[Parameter] |
||||
public int WatermarkId { get; set; } |
||||
|
||||
private record DummyModel; |
||||
|
||||
private readonly DummyModel _dummyModel = new(); |
||||
|
||||
private string _newWatermarkName; |
||||
|
||||
private bool CanSubmit() => !string.IsNullOrWhiteSpace(_newWatermarkName); |
||||
|
||||
private async Task Submit() |
||||
{ |
||||
if (!CanSubmit()) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
Either<BaseError, WatermarkViewModel> maybeResult = |
||||
await _mediator.Send(new CopyWatermark(WatermarkId, _newWatermarkName)); |
||||
|
||||
maybeResult.Match( |
||||
watermark => { MudDialog.Close(DialogResult.Ok(watermark)); }, |
||||
error => |
||||
{ |
||||
_snackbar.Add(error.Value, Severity.Error); |
||||
_logger.LogError("Error copying Watermark: {Error}", error.Value); |
||||
MudDialog.Close(DialogResult.Cancel()); |
||||
}); |
||||
} |
||||
|
||||
private void Cancel(MouseEventArgs e) => MudDialog.Cancel(); |
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.ViewModels; |
||||
using FluentValidation; |
||||
|
||||
namespace ErsatzTV.Validators |
||||
{ |
||||
public class WatermarkEditViewModelValidator : AbstractValidator<WatermarkEditViewModel> |
||||
{ |
||||
public WatermarkEditViewModelValidator() |
||||
{ |
||||
RuleFor(x => x.Name).NotEmpty(); |
||||
|
||||
RuleFor(x => x.Image) |
||||
.NotEmpty() |
||||
.WithMessage("Watermark image is required!"); |
||||
|
||||
RuleFor(x => x.Width) |
||||
.GreaterThan(0) |
||||
.LessThanOrEqualTo(100) |
||||
.When( |
||||
vm => vm.Mode != ChannelWatermarkMode.None && |
||||
vm.Size == ChannelWatermarkSize.Scaled); |
||||
|
||||
RuleFor(x => x.HorizontalMargin) |
||||
.GreaterThanOrEqualTo(0) |
||||
.LessThanOrEqualTo(50) |
||||
.When(vm => vm.Mode != ChannelWatermarkMode.None); |
||||
|
||||
RuleFor(x => x.VerticalMargin) |
||||
.GreaterThanOrEqualTo(0) |
||||
.LessThanOrEqualTo(50) |
||||
.When(vm => vm.Mode != ChannelWatermarkMode.None); |
||||
|
||||
RuleFor(x => x.DurationSeconds) |
||||
.GreaterThan(0) |
||||
.LessThan(c => c.FrequencyMinutes * 60) |
||||
.When(vm => vm.Mode != ChannelWatermarkMode.None); |
||||
|
||||
RuleFor(x => x.Opacity) |
||||
.GreaterThan(0) |
||||
.LessThanOrEqualTo(100) |
||||
.When(vm => vm.Mode != ChannelWatermarkMode.None); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
using ErsatzTV.Application.Watermarks; |
||||
using ErsatzTV.Application.Watermarks.Commands; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.ViewModels |
||||
{ |
||||
public class WatermarkEditViewModel |
||||
{ |
||||
public WatermarkEditViewModel() |
||||
{ |
||||
} |
||||
|
||||
public WatermarkEditViewModel(WatermarkViewModel vm) |
||||
{ |
||||
Id = vm.Id; |
||||
Name = vm.Name; |
||||
Image = vm.Image; |
||||
Mode = vm.Mode; |
||||
ImageSource = vm.ImageSource; |
||||
Location = vm.Location; |
||||
Size = vm.Size; |
||||
Width = vm.Width; |
||||
HorizontalMargin = vm.HorizontalMargin; |
||||
VerticalMargin = vm.VerticalMargin; |
||||
FrequencyMinutes = vm.FrequencyMinutes; |
||||
DurationSeconds = vm.DurationSeconds; |
||||
Opacity = vm.Opacity; |
||||
} |
||||
|
||||
public int Id { get; set; } |
||||
public string Name { get; set; } |
||||
public string Image { get; set; } |
||||
public ChannelWatermarkMode Mode { get; set; } |
||||
public ChannelWatermarkImageSource ImageSource { get; set; } |
||||
public ChannelWatermarkLocation Location { get; set; } |
||||
public ChannelWatermarkSize Size { get; set; } |
||||
public int Width { get; set; } |
||||
public int HorizontalMargin { get; set; } |
||||
public int VerticalMargin { get; set; } |
||||
public int FrequencyMinutes { get; set; } |
||||
public int DurationSeconds { get; set; } |
||||
public int Opacity { get; set; } |
||||
|
||||
public CreateWatermark ToCreate() => |
||||
new( |
||||
Name, |
||||
Image, |
||||
Mode, |
||||
ImageSource, |
||||
Location, |
||||
Size, |
||||
Width, |
||||
HorizontalMargin, |
||||
VerticalMargin, |
||||
FrequencyMinutes, |
||||
DurationSeconds, |
||||
Opacity |
||||
); |
||||
|
||||
public UpdateWatermark ToUpdate() => |
||||
new( |
||||
Id, |
||||
Name, |
||||
Image, |
||||
Mode, |
||||
ImageSource, |
||||
Location, |
||||
Size, |
||||
Width, |
||||
HorizontalMargin, |
||||
VerticalMargin, |
||||
FrequencyMinutes, |
||||
DurationSeconds, |
||||
Opacity |
||||
); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue