diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0c12532..4398a3df8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ErsatzTV.Application/Channels/ChannelSortViewModel.cs b/ErsatzTV.Application/Channels/ChannelSortViewModel.cs new file mode 100644 index 000000000..0f8ee3c98 --- /dev/null +++ b/ErsatzTV.Application/Channels/ChannelSortViewModel.cs @@ -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; +} diff --git a/ErsatzTV.Application/Channels/Commands/UpdateChannelNumbers.cs b/ErsatzTV.Application/Channels/Commands/UpdateChannelNumbers.cs new file mode 100644 index 000000000..805511228 --- /dev/null +++ b/ErsatzTV.Application/Channels/Commands/UpdateChannelNumbers.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Channels; + +public record UpdateChannelNumbers(List Channels) : IRequest>; diff --git a/ErsatzTV.Application/Channels/Commands/UpdateChannelNumbersHandler.cs b/ErsatzTV.Application/Channels/Commands/UpdateChannelNumbersHandler.cs new file mode 100644 index 000000000..57a8b5a55 --- /dev/null +++ b/ErsatzTV.Application/Channels/Commands/UpdateChannelNumbersHandler.cs @@ -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 dbContextFactory, + ChannelWriter workerChannel) + : IRequestHandler> +{ + public async Task> 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 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.None; + } + catch (Exception ex) + { + return BaseError.New("Failed to update channel numbers: " + ex.Message); + } + } +} diff --git a/ErsatzTV.Application/Channels/Queries/GetAllChannelsForSort.cs b/ErsatzTV.Application/Channels/Queries/GetAllChannelsForSort.cs new file mode 100644 index 000000000..2d3ad48da --- /dev/null +++ b/ErsatzTV.Application/Channels/Queries/GetAllChannelsForSort.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Channels; + +public record GetAllChannelsForSort : IRequest>; diff --git a/ErsatzTV.Application/Channels/Queries/GetAllChannelsForSortHandler.cs b/ErsatzTV.Application/Channels/Queries/GetAllChannelsForSortHandler.cs new file mode 100644 index 000000000..fc9afed03 --- /dev/null +++ b/ErsatzTV.Application/Channels/Queries/GetAllChannelsForSortHandler.cs @@ -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 dbContextFactory) + : IRequestHandler> +{ + public async Task> 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 + }; +} diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 1aaff2183..3844b309a 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -28,6 +28,7 @@ + diff --git a/ErsatzTV/Pages/ChannelNumbers.razor b/ErsatzTV/Pages/ChannelNumbers.razor new file mode 100644 index 000000000..21c4bb7a2 --- /dev/null +++ b/ErsatzTV/Pages/ChannelNumbers.razor @@ -0,0 +1,109 @@ +@page "/channels/numbers" +@using ErsatzTV.Application.Channels +@implements IDisposable +@inject IMediator Mediator +@inject NavigationManager NavigationManager +@inject ISnackbar Snackbar +@inject ILogger Logger + + + +
+
+ + Save Channels + + + Reset Channels + +
+
+
+ + + + +
+
+
+
+ + Edit Channel Numbers + + + +
+ @context.Number + + + @context.Name + + + + (@context.OriginalNumber) + +
+
+
+
+
+
+ +@code { + private CancellationTokenSource _cts; + private List _channels = []; + private List _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 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 obj) + { + for (var i = 0; i < _channels.Count; i++) + { + _channels[i].Number = _channelNumbers[i]; + } + + StateHasChanged(); + } +} diff --git a/ErsatzTV/Pages/Channels.razor b/ErsatzTV/Pages/Channels.razor index 7302d302e..f6d972c85 100644 --- a/ErsatzTV/Pages/Channels.razor +++ b/ErsatzTV/Pages/Channels.razor @@ -15,6 +15,9 @@ Add Channel + + Edit Channel Numbers +
diff --git a/ErsatzTV/Pages/_Host.cshtml b/ErsatzTV/Pages/_Host.cshtml index 78cf2197d..743d16d50 100644 --- a/ErsatzTV/Pages/_Host.cshtml +++ b/ErsatzTV/Pages/_Host.cshtml @@ -15,13 +15,15 @@ ErsatzTV - + - + + + @await Html.PartialAsync("../Shared/_Favicons") diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 24eb5e03a..9509c75ad 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -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 services.AddMudServices(); + services.AddSortable(); + var coreAssembly = Assembly.GetAssembly(typeof(LibraryScanProgress)); if (coreAssembly != null) { diff --git a/ErsatzTV/_Imports.razor b/ErsatzTV/_Imports.razor index 5f0912544..8b536efdf 100644 --- a/ErsatzTV/_Imports.razor +++ b/ErsatzTV/_Imports.razor @@ -14,6 +14,7 @@ @using Microsoft.Extensions.Logging @using Microsoft.JSInterop @using Blazored.FluentValidation +@using BlazorSortable @using Heron.MudCalendar @using LanguageExt @using MediatR