From 956734ce391dbd74cd2016d44a29efffa4a0a73b Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Mon, 26 May 2025 11:32:25 -0500 Subject: [PATCH] globalization fixes (#2014) * fix crashes caused by decimal separator * improvements to playout reset ui * remove code quality workflow --- .github/workflows/code_quality.yml | 23 -------- CHANGELOG.md | 3 + .../Playouts/Commands/BuildPlayoutHandler.cs | 4 +- .../ExtractEmbeddedSubtitlesHandler.cs | 4 +- .../Interfaces/Locking/IEntityLocker.cs | 5 +- .../PlayoutUpdatedNotification.cs | 5 ++ .../Locking/EntityLocker.cs | 13 +++-- ErsatzTV/Pages/Channels.razor | 4 +- ErsatzTV/Pages/PlayoutEditor.razor | 3 +- ErsatzTV/Pages/Playouts.razor | 56 ++++++++++--------- 10 files changed, 54 insertions(+), 66 deletions(-) delete mode 100644 .github/workflows/code_quality.yml create mode 100644 ErsatzTV.Core/Notifications/PlayoutUpdatedNotification.cs diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml deleted file mode 100644 index a1238895..00000000 --- a/.github/workflows/code_quality.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Qodana -on: - workflow_dispatch: - push: - branches: - - main - -jobs: - qodana: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - checks: write - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit - fetch-depth: 0 # a full history is required for pull request analysis - - name: 'Qodana Scan' - uses: JetBrains/qodana-action@v2024.1 - env: - QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b4c847..30b89c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fix error message about synchronizing Plex collections from a Plex server that has zero collections - Fix navigation after form submission when using `ETV_BASE_URL` environment variable +- Fix UI crashes when channel numbers contain a period `.` in locales that have a different decimal separator (e.g. `,`) +- Fix playout detail table to only reload once when resetting a playout +- Fix date formatting in playout detail table on reload (will now respect browser's `Accept-Language` header) ## [25.1.0] - 2025-01-10 ### Added diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs index d63db386..6d105e13 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -69,7 +69,7 @@ public class BuildPlayoutHandler : IRequestHandler OnPlayoutChanged; bool LockLibrary(int libraryId); bool UnlockLibrary(int libraryId); bool IsLibraryLocked(int libraryId); @@ -31,7 +30,7 @@ public interface IEntityLocker bool LockPlexCollections(); bool UnlockPlexCollections(); bool ArePlexCollectionsLocked(); - bool LockPlayout(int playoutId); - bool UnlockPlayout(int playoutId); + Task LockPlayout(int playoutId); + Task UnlockPlayout(int playoutId); bool IsPlayoutLocked(int playoutId); } diff --git a/ErsatzTV.Core/Notifications/PlayoutUpdatedNotification.cs b/ErsatzTV.Core/Notifications/PlayoutUpdatedNotification.cs new file mode 100644 index 00000000..d51c0703 --- /dev/null +++ b/ErsatzTV.Core/Notifications/PlayoutUpdatedNotification.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace ErsatzTV.Core.Notifications; + +public record PlayoutUpdatedNotification(int PlayoutId, bool IsLocked) : INotification; diff --git a/ErsatzTV.Infrastructure/Locking/EntityLocker.cs b/ErsatzTV.Infrastructure/Locking/EntityLocker.cs index f4820831..942386cf 100644 --- a/ErsatzTV.Infrastructure/Locking/EntityLocker.cs +++ b/ErsatzTV.Infrastructure/Locking/EntityLocker.cs @@ -1,9 +1,11 @@ using System.Collections.Concurrent; using ErsatzTV.Core.Interfaces.Locking; +using ErsatzTV.Core.Notifications; +using MediatR; namespace ErsatzTV.Infrastructure.Locking; -public class EntityLocker : IEntityLocker +public class EntityLocker(IMediator mediator) : IEntityLocker { private readonly ConcurrentDictionary _lockedLibraries = new(); private readonly ConcurrentDictionary _lockedPlayouts = new(); @@ -21,7 +23,6 @@ public class EntityLocker : IEntityLocker public event EventHandler OnEmbyCollectionsChanged; public event EventHandler OnJellyfinCollectionsChanged; public event EventHandler OnPlexCollectionsChanged; - public event EventHandler OnPlayoutChanged; public bool LockLibrary(int libraryId) { @@ -208,22 +209,22 @@ public class EntityLocker : IEntityLocker public bool ArePlexCollectionsLocked() => _plexCollections; - public bool LockPlayout(int playoutId) + public async Task LockPlayout(int playoutId) { if (!_lockedPlayouts.ContainsKey(playoutId) && _lockedPlayouts.TryAdd(playoutId, 0)) { - OnPlayoutChanged?.Invoke(this, playoutId); + await mediator.Publish(new PlayoutUpdatedNotification(playoutId, true)); return true; } return false; } - public bool UnlockPlayout(int playoutId) + public async Task UnlockPlayout(int playoutId) { if (_lockedPlayouts.TryRemove(playoutId, out byte _)) { - OnPlayoutChanged?.Invoke(this, playoutId); + await mediator.Publish(new PlayoutUpdatedNotification(playoutId, false)); return true; } diff --git a/ErsatzTV/Pages/Channels.razor b/ErsatzTV/Pages/Channels.razor index d627e9ec..ac321cb7 100644 --- a/ErsatzTV/Pages/Channels.razor +++ b/ErsatzTV/Pages/Channels.razor @@ -29,7 +29,7 @@ - Number + Number Logo @@ -219,7 +219,7 @@ await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.ChannelsPageSize, state.PageSize.ToString()), _cts.Token); List channels = await Mediator.Send(new GetAllChannels(), _cts.Token); - IOrderedEnumerable sorted = channels.OrderBy(c => decimal.Parse(c.Number)); + IOrderedEnumerable sorted = channels.OrderBy(c => decimal.Parse(c.Number, CultureInfo.InvariantCulture)); CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); var processedChannels = new List(); diff --git a/ErsatzTV/Pages/PlayoutEditor.razor b/ErsatzTV/Pages/PlayoutEditor.razor index fcca8307..60115554 100644 --- a/ErsatzTV/Pages/PlayoutEditor.razor +++ b/ErsatzTV/Pages/PlayoutEditor.razor @@ -1,5 +1,6 @@ @page "/playouts/add" @page "/playouts/add/{kind}" +@using System.Globalization @using ErsatzTV.Application.Channels @using ErsatzTV.Application.ProgramSchedules @implements IDisposable @@ -99,7 +100,7 @@ _model.Kind = Kind; _channels = await Mediator.Send(new GetAllChannels(), _cts.Token) - .Map(list => list.OrderBy(vm => decimal.Parse(vm.Number)).ToList()); + .Map(list => list.OrderBy(vm => decimal.Parse(vm.Number, CultureInfo.InvariantCulture)).ToList()); if (string.IsNullOrWhiteSpace(Kind)) { diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 7c0527d8..6b9bfb35 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -1,12 +1,16 @@ -@page "/playouts" +@page "/playouts" +@using System.Globalization @using ErsatzTV.Application.Configuration @using ErsatzTV.Application.Playouts @using ErsatzTV.Core.Scheduling +@using ErsatzTV.Core.Notifications +@using MediatR.Courier @implements IDisposable @inject IDialogService Dialog @inject IMediator Mediator @inject ChannelWriter WorkerChannel @inject IEntityLocker EntityLocker; +@inject ICourier Courier;
@@ -50,7 +54,7 @@ - + Channel @@ -199,8 +203,8 @@ Duration - @context.Start.ToString("G") - @context.Finish.ToString("G") + @context.Start.ToString("G", _dtf) + @context.Finish.ToString("G", _dtf) @context.Title @context.Duration @@ -214,6 +218,8 @@ @code { private readonly CancellationTokenSource _cts = new(); + private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat; + private MudTable _table; private MudTable _detailTable; private int _rowsPerPage = 10; @@ -237,15 +243,30 @@ } } - public void Dispose() + protected override void OnInitialized() { - EntityLocker.OnPlayoutChanged -= ReloadDetailsIfNeeded; + Courier.Subscribe(HandlePlayoutUpdated); + } + public void Dispose() + { _cts.Cancel(); _cts.Dispose(); } - protected override void OnInitialized() => EntityLocker.OnPlayoutChanged += ReloadDetailsIfNeeded; + public async Task HandlePlayoutUpdated(PlayoutUpdatedNotification notification, CancellationToken cancellationToken) + { + // only refresh detail table on unlock operations (after playout has been modified) + if (notification.IsLocked == false) + { + if (notification.PlayoutId == _selectedPlayoutId && _detailTable is not null) + { + await InvokeAsync(() => _detailTable.ReloadServerData()); + } + } + + await InvokeAsync(StateHasChanged); + } protected override async Task OnParametersSetAsync() { @@ -320,15 +341,6 @@ private async Task ResetPlayout(PlayoutNameViewModel playout) { await WorkerChannel.WriteAsync(new BuildPlayout(playout.PlayoutId, PlayoutBuildMode.Reset), _cts.Token); - if (_table != null) - { - await _table.ReloadServerData(); - } - - if (_selectedPlayoutId == playout.PlayoutId) - { - await PlayoutSelected(playout); - } } private async Task ScheduleReset(PlayoutNameViewModel playout) @@ -356,7 +368,7 @@ await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString()), _cts.Token); List playouts = await Mediator.Send(new GetAllPlayouts(), _cts.Token); - IOrderedEnumerable sorted = playouts.OrderBy(p => decimal.Parse(p.ChannelNumber)); + IOrderedEnumerable sorted = playouts.OrderBy(p => decimal.Parse(p.ChannelNumber, CultureInfo.InvariantCulture)); // TODO: properly page this data return new TableData @@ -366,16 +378,6 @@ }; } - private async void ReloadDetailsIfNeeded(object sender, int playoutId) - { - if (playoutId == _selectedPlayoutId && _detailTable is not null) - { - await InvokeAsync(() => _detailTable.ReloadServerData()); - } - - await InvokeAsync(StateHasChanged); - } - private async Task> DetailServerReload(TableState state, CancellationToken cancellationToken) { await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()), _cts.Token);