Browse Source

add page to edit channel numbers (#2454)

pull/2444/head
Jason Dove 3 months ago committed by GitHub
parent
commit
9ec220c122
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 10
      ErsatzTV.Application/Channels/ChannelSortViewModel.cs
  3. 5
      ErsatzTV.Application/Channels/Commands/UpdateChannelNumbers.cs
  4. 63
      ErsatzTV.Application/Channels/Commands/UpdateChannelNumbersHandler.cs
  5. 3
      ErsatzTV.Application/Channels/Queries/GetAllChannelsForSort.cs
  6. 31
      ErsatzTV.Application/Channels/Queries/GetAllChannelsForSortHandler.cs
  7. 1
      ErsatzTV/ErsatzTV.csproj
  8. 109
      ErsatzTV/Pages/ChannelNumbers.razor
  9. 3
      ErsatzTV/Pages/Channels.razor
  10. 6
      ErsatzTV/Pages/_Host.cshtml
  11. 3
      ErsatzTV/Startup.cs
  12. 1
      ErsatzTV/_Imports.razor

2
CHANGELOG.md

@ -50,6 +50,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -50,6 +50,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Future work will add other placement options
- Break content is currently limited to playlists (which do *not* pad - they simply play through the playlist one time)
- Future work will add other collection options which will pad to the full block duration
- Add page to reorder channels (edit channel numbers) using drag and drop
- New page is at **Channels** > **Edit Channel Numbers**
### Fixed
- Fix green output when libplacebo tonemapping is used with NVIDIA acceleration and 10-bit output in FFmpeg Profile

10
ErsatzTV.Application/Channels/ChannelSortViewModel.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Application.Channels;
public class ChannelSortViewModel
{
public int Id { get; set; }
public string Number { get; set; }
public string Name { get; set; }
public string OriginalNumber { get; set; }
public bool HasChanged => OriginalNumber != Number;
}

5
ErsatzTV.Application/Channels/Commands/UpdateChannelNumbers.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Channels;
public record UpdateChannelNumbers(List<ChannelSortViewModel> Channels) : IRequest<Option<BaseError>>;

63
ErsatzTV.Application/Channels/Commands/UpdateChannelNumbersHandler.cs

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Channels;
public class UpdateChannelNumbersHandler(
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
: IRequestHandler<UpdateChannelNumbers, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(UpdateChannelNumbers request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
try
{
var numberUpdates = request.Channels.ToDictionary(c => c.Id, c => c.Number);
var channelIds = numberUpdates.Keys;
List<Channel> channelsToUpdate = await dbContext.Channels
.Where(c => channelIds.Contains(c.Id))
.ToListAsync(cancellationToken);
// give every channel a non-conflicting number
foreach (var channel in channelsToUpdate)
{
channel.Number = $"-{channel.Id}";
}
// save those changes
await dbContext.SaveChangesAsync(cancellationToken);
// give every channel the proper new number
foreach (var channel in channelsToUpdate)
{
channel.Number = numberUpdates[channel.Id];
}
// save those changes
await dbContext.SaveChangesAsync(cancellationToken);
// commit the transaction
await transaction.CommitAsync(cancellationToken);
// update channel list and xmltv
await workerChannel.WriteAsync(new RefreshChannelList(), cancellationToken);
foreach (var channel in channelsToUpdate)
{
await workerChannel.WriteAsync(new RefreshChannelData(channel.Number), cancellationToken);
}
return Option<BaseError>.None;
}
catch (Exception ex)
{
return BaseError.New("Failed to update channel numbers: " + ex.Message);
}
}
}

3
ErsatzTV.Application/Channels/Queries/GetAllChannelsForSort.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetAllChannelsForSort : IRequest<List<ChannelSortViewModel>>;

31
ErsatzTV.Application/Channels/Queries/GetAllChannelsForSortHandler.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
using System.Globalization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Channels;
public class GetAllChannelsForSortHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllChannelsForSort, List<ChannelSortViewModel>>
{
public async Task<List<ChannelSortViewModel>> Handle(
GetAllChannelsForSort request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Channels
.AsNoTracking()
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToSortViewModel)
.OrderBy(c => decimal.Parse(c.Number, CultureInfo.InvariantCulture)).ToList());
}
private static ChannelSortViewModel ProjectToSortViewModel(Channel channel)
=> new()
{
Id = channel.Id,
Number = channel.Number,
Name = channel.Name,
OriginalNumber = channel.Number
};
}

1
ErsatzTV/ErsatzTV.csproj

@ -28,6 +28,7 @@ @@ -28,6 +28,7 @@
<ItemGroup>
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
<PackageReference Include="BlazorSortable" Version="4.0.0" />
<PackageReference Include="Bugsnag.AspNet.Core" Version="4.1.0" />
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />

109
ErsatzTV/Pages/ChannelNumbers.razor

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
@page "/channels/numbers"
@using ErsatzTV.Application.Channels
@implements IDisposable
@inject IMediator Mediator
@inject NavigationManager NavigationManager
@inject ISnackbar Snackbar
@inject ILogger<ChannelNumbers> Logger
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%; align-items: center" class="ml-6 mr-6">
<div class="d-none d-md-flex">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Save" OnClick="@SaveChannels">
Save Channels
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" Class="ml-3" StartIcon="@Icons.Material.Filled.Undo" OnClick="@ReloadAllChannels">
Reset Channels
</MudButton>
</div>
<div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none">
<div class="flex-grow-1"></div>
<MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Save" Label="Save Channels" OnClick="@SaveChannels"/>
<MudMenuItem Icon="@Icons.Material.Filled.Undo" Label="Reset Channels" OnClick="@ReloadAllChannels"/>
</MudMenu>
</div>
</div>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Edit Channel Numbers</MudText>
<MudDivider Class="mb-6"/>
<Sortable TItem="ChannelSortViewModel" Items="_channels" OnUpdate="OnUpdate">
<MudPaper Class="@($"pa-2 ma-2 ")"
Style="@($"max-width: 500px; {(context.HasChanged ? "background-color: var(--mud-palette-dark-lighten)" : "")}")">
<div class="d-flex align-center">
<MudText Style="min-width: 6ch">@context.Number</MudText>
<MudText Class="flex-grow-1">
@context.Name
</MudText>
<MudText Style="@(context.HasChanged ? "" : "visibility: hidden;")">
(@context.OriginalNumber)
</MudText>
</div>
</MudPaper>
</Sortable>
</MudContainer>
</div>
</MudForm>
@code {
private CancellationTokenSource _cts;
private List<ChannelSortViewModel> _channels = [];
private List<string> _channelNumbers = [];
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadAllChannels();
}
private async Task ReloadAllChannels()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_channels = await Mediator.Send(new GetAllChannelsForSort(), token);
_channelNumbers = _channels.Map(c => c.Number).ToList();
}
catch (OperationCanceledException)
{
// do nothing
}
}
private async Task SaveChannels()
{
var changedChannels = _channels.Where(c => c.HasChanged).ToList();
Option<BaseError> maybeError = await Mediator.Send(new UpdateChannelNumbers(changedChannels), _cts.Token);
maybeError.Match(
error =>
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving channel numbers: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("channels"));
}
private void OnUpdate(SortableEventArgs<ChannelSortViewModel> obj)
{
for (var i = 0; i < _channels.Count; i++)
{
_channels[i].Number = _channelNumbers[i];
}
StateHasChanged();
}
}

3
ErsatzTV/Pages/Channels.razor

@ -15,6 +15,9 @@ @@ -15,6 +15,9 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" StartIcon="@Icons.Material.Filled.Add" Href="channels/add">
Add Channel
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="ml-3" StartIcon="@Icons.Material.Filled.Edit" Href="channels/numbers">
Edit Channel Numbers
</MudButton>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">

6
ErsatzTV/Pages/_Host.cshtml

@ -15,13 +15,15 @@ @@ -15,13 +15,15 @@
<title>ErsatzTV</title>
<base href="~/"/>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" rel="stylesheet"/>
<link href="_content/MudBlazor/MudBlazor.min.css?v=@(Assembly.GetAssembly(typeof(AbstractLocalizationInterceptor))?.GetName().Version?.ToString())" rel="stylesheet"/>
<link href="css/site.css" asp-append-version="true" rel="stylesheet"/>
<link href="ErsatzTV.styles.css" asp-append-version="true" rel="stylesheet"/>
<link href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet">
<link href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet"/>
<link href="_content/BlazorSortable/css/blazor-sortable.css" rel="stylesheet"/>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome@1/+esm"></script>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
@await Html.PartialAsync("../Shared/_Favicons")

3
ErsatzTV/Startup.cs

@ -4,6 +4,7 @@ using System.Reflection; @@ -4,6 +4,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Channels;
using BlazorSortable;
using Bugsnag.AspNet.Core;
using Dapper;
using ErsatzTV.Application;
@ -338,6 +339,8 @@ public class Startup @@ -338,6 +339,8 @@ public class Startup
services.AddMudServices();
services.AddSortable();
var coreAssembly = Assembly.GetAssembly(typeof(LibraryScanProgress));
if (coreAssembly != null)
{

1
ErsatzTV/_Imports.razor

@ -14,6 +14,7 @@ @@ -14,6 +14,7 @@
@using Microsoft.Extensions.Logging
@using Microsoft.JSInterop
@using Blazored.FluentValidation
@using BlazorSortable
@using Heron.MudCalendar
@using LanguageExt
@using MediatR

Loading…
Cancel
Save