From c7dcc0e88fa736ca52dbe3a233171f70dbd5f543 Mon Sep 17 00:00:00 2001 From: pdjdev Date: Wed, 7 Oct 2026 11:30:39 +0900 Subject: [PATCH 1/4] Keep Linux launcher progress connection alive --- .../Platform/Progress/PlatformProgress.cs | 5 +- .../Progress/PlatformProgressGroup.cs | 10 +++- .../Logic/Platform/PlatformProgressTests.cs | 59 +++++++++++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/ui/Logic/Platform/Progress/PlatformProgress.cs b/src/ui/Logic/Platform/Progress/PlatformProgress.cs index ee9fac78285..25bbdffac4a 100644 --- a/src/ui/Logic/Platform/Progress/PlatformProgress.cs +++ b/src/ui/Logic/Platform/Progress/PlatformProgress.cs @@ -13,8 +13,9 @@ public static class PlatformProgress AvaloniaProperty.RegisterAttached("IsActive", typeof(PlatformProgress)); private static readonly ConditionalWeakTable WindowGroups = new(); - private static readonly PlatformProgressGroup ApplicationGroup = new(() => - OperatingSystem.IsMacOS() ? new MacDockProgress() : new LinuxLauncherProgress()); + private static readonly PlatformProgressGroup ApplicationGroup = new( + () => OperatingSystem.IsMacOS() ? new MacDockProgress() : new LinuxLauncherProgress(), + keepBackendWhenIdle: OperatingSystem.IsLinux()); public static ProgressBar WithPlatformProgress(this ProgressBar progressBar, Window window, string activeProperty, IValueConverter? converter = null) diff --git a/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs b/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs index 04cedacfd43..5b8c2539b57 100644 --- a/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs +++ b/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs @@ -4,7 +4,7 @@ namespace Nikse.SubtitleEdit.Logic.Platform.Progress; -internal sealed class PlatformProgressGroup(Func createProgress) +internal sealed class PlatformProgressGroup(Func createProgress, bool keepBackendWhenIdle = false) { private readonly List<(object Source, double? Percentage, bool Indeterminate)> _sources = []; private IPlatformProgress? _progress; @@ -30,8 +30,12 @@ public void Update(object source, double? percentage, bool indeterminate) if (_sources.Count == 0) { _progress?.Update(null, false); - _progress?.Dispose(); - _progress = null; + // Keep the Linux connection alive so asynchronous clear requests can be sent. + if (!keepBackendWhenIdle) + { + _progress?.Dispose(); + _progress = null; + } return; } diff --git a/tests/UI/Logic/Platform/PlatformProgressTests.cs b/tests/UI/Logic/Platform/PlatformProgressTests.cs index af8dd2f54ac..247ae5fb220 100644 --- a/tests/UI/Logic/Platform/PlatformProgressTests.cs +++ b/tests/UI/Logic/Platform/PlatformProgressTests.cs @@ -135,6 +135,65 @@ public void NewOperationCreatesNewBackendAfterCleanup() group.Update(source, null, false); } + [Fact] + public void RetainedBackendRestoresOtherActiveSource() + { + var progress = new FakeProgress(); + var group = new PlatformProgressGroup(() => progress, keepBackendWhenIdle: true); + var first = new object(); + var second = new object(); + + group.Update(first, 70, false); + group.Update(second, 20, false); + group.Update(first, 80, false); + group.Update(second, null, false); + + Assert.Equal(((double?)80, false), progress.Updates[^1]); + Assert.False(progress.IsDisposed); + + group.Update(first, null, false); + Assert.Equal(((double?)null, false), progress.Updates[^1]); + Assert.False(progress.IsDisposed); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RetainedBackendClearsAndIsReusedForNextOperation(bool indeterminate) + { + var instances = new List(); + var group = new PlatformProgressGroup(() => + { + var progress = new FakeProgress(); + instances.Add(progress); + return progress; + }, keepBackendWhenIdle: true); + var first = new object(); + var second = new object(); + double? percentage = indeterminate ? null : 10; + + group.Update(first, null, false); + Assert.Empty(instances); + + group.Update(first, percentage, indeterminate); + var backend = Assert.Single(instances); + group.Update(first, null, false); + + Assert.Equal(2, backend.Updates.Count); + Assert.Equal((percentage, indeterminate), backend.Updates[0]); + Assert.Equal(((double?)null, false), backend.Updates[1]); + Assert.False(backend.IsDisposed); + + group.Update(second, 20, false); + + Assert.Same(backend, Assert.Single(instances)); + Assert.Equal(((double?)20, false), backend.Updates[^1]); + group.Update(second, null, false); + Assert.Equal(4, backend.Updates.Count); + Assert.Equal(((double?)null, false), backend.Updates[^1]); + Assert.False(backend.IsDisposed); + } + [Fact] public void UnavailableBackendDoesNotInterruptOperation() { From e1cb75f20fec1ff609540ccdf935829f96c3909d Mon Sep 17 00:00:00 2001 From: pdjdev Date: Wed, 7 Oct 2026 11:42:21 +0900 Subject: [PATCH 2/4] Throttle platform progress updates --- .../Progress/PlatformProgressGroup.cs | 12 ++- .../Logic/Platform/PlatformProgressTests.cs | 93 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs b/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs index 5b8c2539b57..eed132ca591 100644 --- a/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs +++ b/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs @@ -8,6 +8,7 @@ internal sealed class PlatformProgressGroup(Func createProgre { private readonly List<(object Source, double? Percentage, bool Indeterminate)> _sources = []; private IPlatformProgress? _progress; + private (object Source, int? Percentage, bool Indeterminate)? _lastUpdate; public void Update(object source, double? percentage, bool indeterminate) { @@ -30,6 +31,7 @@ public void Update(object source, double? percentage, bool indeterminate) if (_sources.Count == 0) { _progress?.Update(null, false); + _lastUpdate = null; // Keep the Linux connection alive so asynchronous clear requests can be sent. if (!keepBackendWhenIdle) { @@ -41,7 +43,15 @@ public void Update(object source, double? percentage, bool indeterminate) _progress ??= createProgress(); var current = _sources[^1]; - _progress.Update(current.Percentage, current.Indeterminate); + var effectivePercentage = !current.Indeterminate && current.Percentage is { } value + ? (int?)value : null; + // Source changes must be published even when their visible state is the same. + if (_lastUpdate is { } last && ReferenceEquals(last.Source, current.Source) && + last.Percentage == effectivePercentage && last.Indeterminate == current.Indeterminate) + return; + + _progress.Update(effectivePercentage, current.Indeterminate); + _lastUpdate = (current.Source, effectivePercentage, current.Indeterminate); } catch (Exception exception) { diff --git a/tests/UI/Logic/Platform/PlatformProgressTests.cs b/tests/UI/Logic/Platform/PlatformProgressTests.cs index 247ae5fb220..0f33a3f65e4 100644 --- a/tests/UI/Logic/Platform/PlatformProgressTests.cs +++ b/tests/UI/Logic/Platform/PlatformProgressTests.cs @@ -73,6 +73,7 @@ public void CompletingBackgroundSourceKeepsCurrentSource() group.Update(second, 20, false); group.Update(first, null, false); + Assert.Equal(2, progress.Updates.Count); Assert.Equal(((double?)20, false), progress.Updates[^1]); Assert.False(progress.IsDisposed); group.Update(second, null, false); @@ -194,6 +195,98 @@ public void RetainedBackendClearsAndIsReusedForNextOperation(bool indeterminate) Assert.False(backend.IsDisposed); } + [Fact] + public void UpdatesWithinSameWholePercentageAreSkipped() + { + var progress = new FakeProgress(); + var group = new PlatformProgressGroup(() => progress); + var source = new object(); + + group.Update(source, 10.1, false); + group.Update(source, 10.99, false); + group.Update(source, 11, false); + group.Update(source, 11.9, false); + group.Update(source, 10.9, false); + + Assert.Equal(new[] { ((double?)10, false), ((double?)11, false), ((double?)10, false) }, progress.Updates); + group.Update(source, null, false); + Assert.Equal(((double?)null, false), progress.Updates[^1]); + } + + [Fact] + public void IndeterminateTransitionsAreSentButNumericChangesAreSkipped() + { + var progress = new FakeProgress(); + var group = new PlatformProgressGroup(() => progress); + var source = new object(); + + group.Update(source, 35.2, false); + group.Update(source, 35.2, true); + group.Update(source, 80.9, true); + group.Update(source, null, true); + group.Update(source, 35.9, false); + + Assert.Equal(new[] { ((double?)35, false), ((double?)null, true), ((double?)35, false) }, progress.Updates); + group.Update(source, null, false); + Assert.Equal(((double?)null, false), progress.Updates[^1]); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NewSourceAndRestorationAreSentEvenWithSameEffectiveState(bool keepBackendWhenIdle, bool indeterminate) + { + var progress = new FakeProgress(); + var group = new PlatformProgressGroup(() => progress, keepBackendWhenIdle); + var first = new object(); + var second = new object(); + + group.Update(first, 20.2, indeterminate); + group.Update(second, 20.9, indeterminate); + group.Update(first, 20.7, indeterminate); + Assert.Equal(2, progress.Updates.Count); + + group.Update(second, null, false); + + double? effectivePercentage = indeterminate ? null : 20; + Assert.Equal(new[] { (effectivePercentage, indeterminate), (effectivePercentage, indeterminate), + (effectivePercentage, indeterminate) }, progress.Updates); + group.Update(first, null, false); + Assert.Equal(((double?)null, false), progress.Updates[^1]); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ClearAndRestartOfSameSourceAreSentEvenWithSameWholePercentage(bool keepBackendWhenIdle) + { + var instances = new List(); + var group = new PlatformProgressGroup(() => + { + var progress = new FakeProgress(); + instances.Add(progress); + return progress; + }, keepBackendWhenIdle); + var source = new object(); + + group.Update(source, 20.2, false); + group.Update(source, 20.9, false); + group.Update(source, null, false); + + Assert.Equal(new[] { ((double?)20, false), ((double?)null, false) }, instances[0].Updates); + Assert.Equal(!keepBackendWhenIdle, instances[0].IsDisposed); + + group.Update(source, 20.7, false); + + Assert.Equal(keepBackendWhenIdle ? 1 : 2, instances.Count); + Assert.Equal(keepBackendWhenIdle ? 3 : 1, instances[^1].Updates.Count); + Assert.Equal(((double?)20, false), instances[^1].Updates[^1]); + group.Update(source, null, false); + Assert.Equal(((double?)null, false), instances[^1].Updates[^1]); + } + [Fact] public void UnavailableBackendDoesNotInterruptOperation() { From efac2c29134f830d09452e71adb4db258b13be7f Mon Sep 17 00:00:00 2001 From: pdjdev Date: Wed, 7 Oct 2026 11:47:22 +0900 Subject: [PATCH 3/4] Fix platform progress using order --- src/ui/Features/Tools/AiReview/AiReviewWindow.cs | 2 +- src/ui/Features/Video/BurnIn/BurnInWindow.cs | 2 +- src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs | 2 +- src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/Features/Tools/AiReview/AiReviewWindow.cs b/src/ui/Features/Tools/AiReview/AiReviewWindow.cs index 0e2dc01c651..4a5c3cd7fd7 100644 --- a/src/ui/Features/Tools/AiReview/AiReviewWindow.cs +++ b/src/ui/Features/Tools/AiReview/AiReviewWindow.cs @@ -9,8 +9,8 @@ using Nikse.SubtitleEdit.Features.Files.Compare; using Nikse.SubtitleEdit.Features.Main; using Nikse.SubtitleEdit.Logic; -using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.Config; +using Nikse.SubtitleEdit.Logic.Platform.Progress; namespace Nikse.SubtitleEdit.Features.Tools.AiReview; diff --git a/src/ui/Features/Video/BurnIn/BurnInWindow.cs b/src/ui/Features/Video/BurnIn/BurnInWindow.cs index 17176b3af15..4c2e62de562 100644 --- a/src/ui/Features/Video/BurnIn/BurnInWindow.cs +++ b/src/ui/Features/Video/BurnIn/BurnInWindow.cs @@ -9,8 +9,8 @@ using Nikse.SubtitleEdit.Controls; using Nikse.SubtitleEdit.Features.Main.Layout; using Nikse.SubtitleEdit.Logic; -using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.Config; +using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.ValueConverters; using Nikse.SubtitleEdit.UiLogic.Export; using System; diff --git a/src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs b/src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs index f79b18e8ed5..a32677e5235 100644 --- a/src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs +++ b/src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs @@ -6,8 +6,8 @@ using Avalonia.Layout; using Avalonia.Styling; using Nikse.SubtitleEdit.Logic; -using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.Config; +using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.ValueConverters; namespace Nikse.SubtitleEdit.Features.Video.ShotChanges; diff --git a/src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs b/src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs index 2eb5a96cb67..9ffa08e317e 100644 --- a/src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs +++ b/src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs @@ -10,9 +10,9 @@ using System.Linq; using Nikse.SubtitleEdit.Features.Video.SpeechToText.Engines; using Nikse.SubtitleEdit.Logic; -using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.Config; using Nikse.SubtitleEdit.Logic.Download; +using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.ValueConverters; namespace Nikse.SubtitleEdit.Features.Video.SpeechToText; From 527cbad22880e089d45a19e266ddd55ad3d1f4be Mon Sep 17 00:00:00 2001 From: pdjdev Date: Wed, 7 Oct 2026 12:33:19 +0900 Subject: [PATCH 4/4] Consolidate platform progress tests --- .../Logic/Platform/PlatformProgressTests.cs | 58 +++---------------- 1 file changed, 9 insertions(+), 49 deletions(-) diff --git a/tests/UI/Logic/Platform/PlatformProgressTests.cs b/tests/UI/Logic/Platform/PlatformProgressTests.cs index 0f33a3f65e4..37014cddcc7 100644 --- a/tests/UI/Logic/Platform/PlatformProgressTests.cs +++ b/tests/UI/Logic/Platform/PlatformProgressTests.cs @@ -39,11 +39,13 @@ private sealed class FakeProgress : IPlatformProgress public void Dispose() => IsDisposed = true; } - [Fact] - public void CompletingCurrentSourceRestoresOtherActiveSource() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CompletingCurrentSourceRestoresOtherActiveSource(bool keepBackendWhenIdle) { var progress = new FakeProgress(); - var group = new PlatformProgressGroup(() => progress); + var group = new PlatformProgressGroup(() => progress, keepBackendWhenIdle); var first = new object(); var second = new object(); @@ -58,7 +60,7 @@ public void CompletingCurrentSourceRestoresOtherActiveSource() group.Update(first, null, false); Assert.Equal(((double?)null, false), progress.Updates[^1]); - Assert.True(progress.IsDisposed); + Assert.Equal(!keepBackendWhenIdle, progress.IsDisposed); } [Fact] @@ -112,51 +114,6 @@ public void IndeterminateSourceRemainsActive() group.Update(source, null, false); } - [Fact] - public void NewOperationCreatesNewBackendAfterCleanup() - { - var instances = new List(); - var group = new PlatformProgressGroup(() => - { - var progress = new FakeProgress(); - instances.Add(progress); - return progress; - }); - var source = new object(); - - group.Update(source, null, false); - Assert.Empty(instances); - group.Update(source, 10, false); - group.Update(source, null, false); - group.Update(source, 20, false); - - Assert.Equal(2, instances.Count); - Assert.True(instances[0].IsDisposed); - Assert.Equal(((double?)20, false), instances[1].Updates[^1]); - group.Update(source, null, false); - } - - [Fact] - public void RetainedBackendRestoresOtherActiveSource() - { - var progress = new FakeProgress(); - var group = new PlatformProgressGroup(() => progress, keepBackendWhenIdle: true); - var first = new object(); - var second = new object(); - - group.Update(first, 70, false); - group.Update(second, 20, false); - group.Update(first, 80, false); - group.Update(second, null, false); - - Assert.Equal(((double?)80, false), progress.Updates[^1]); - Assert.False(progress.IsDisposed); - - group.Update(first, null, false); - Assert.Equal(((double?)null, false), progress.Updates[^1]); - Assert.False(progress.IsDisposed); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -271,6 +228,9 @@ public void ClearAndRestartOfSameSourceAreSentEvenWithSameWholePercentage(bool k }, keepBackendWhenIdle); var source = new object(); + group.Update(source, null, false); + Assert.Empty(instances); + group.Update(source, 20.2, false); group.Update(source, 20.9, false); group.Update(source, null, false);