From ee24037462f9f97eb51459de273bd1053d734a75 Mon Sep 17 00:00:00 2001 From: niksedk Date: Tue, 16 Jun 2026 09:02:50 +0200 Subject: [PATCH] Fix change frame rate/speed corrupting selected line duration Change frame rate (and change speed) updated each line's start and end time in two separate steps. When times grow (e.g. 25 -> 23.976), setting the start first pushes it past the still-unscaled end, briefly producing start > end / a negative duration that gets published via change notification. For the selected line this transient leaks into the editor's two-way bound time/duration up/down controls: the duration control coerces the negative value to zero and writes it back into the model, moving the end time. The result is the selected line's end time scaled roughly twice, inflating its duration (issue #11650). Non-selected rows have no bound controls, so they scale correctly. Add SubtitleLineViewModel.SetTimes(start, end) which assigns both times with notifications suppressed (the idiom already used by the constructor and UpdateFrom) and recomputes duration once, so no invalid intermediate state is ever exposed. Use it from ChangeFrameRate and ChangeSpeed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/Features/Main/SubtitleLineViewModel.cs | 18 ++++++++++++++++++ .../ChangeFrameRateViewModel.cs | 5 +++-- .../Sync/ChangeSpeed/ChangeSpeedViewModel.cs | 6 ++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/ui/Features/Main/SubtitleLineViewModel.cs b/src/ui/Features/Main/SubtitleLineViewModel.cs index 3c1ebd2d6fd..c337b1da735 100644 --- a/src/ui/Features/Main/SubtitleLineViewModel.cs +++ b/src/ui/Features/Main/SubtitleLineViewModel.cs @@ -595,6 +595,24 @@ internal void SetStartTimeOnly(TimeSpan timeSpan) UpdateDuration(); } + /// + /// Sets start and end time together without ever publishing an invalid + /// intermediate state (e.g. start > end / negative duration). Updating + /// the two times separately can briefly expose such a state to the live + /// editor controls bound to the selected line; the duration up/down clamps + /// the negative value and writes it back, corrupting the end time. Suppress + /// notifications while both values are assigned, then recompute duration once. + /// + internal void SetTimes(TimeSpan startTime, TimeSpan endTime) + { + _skipUpdate = true; + StartTime = startTime; + EndTime = endTime; + _skipUpdate = false; + + UpdateDuration(); + } + internal void Adjust(double factor, double adjustmentInSeconds) { if (StartTime.IsMaxTime()) diff --git a/src/ui/Features/Sync/ChangeFrameRate/ChangeFrameRateViewModel.cs b/src/ui/Features/Sync/ChangeFrameRate/ChangeFrameRateViewModel.cs index 99f08d7c99b..3a7051ab156 100644 --- a/src/ui/Features/Sync/ChangeFrameRate/ChangeFrameRateViewModel.cs +++ b/src/ui/Features/Sync/ChangeFrameRate/ChangeFrameRateViewModel.cs @@ -157,8 +157,9 @@ internal static void ChangeFrameRate(ObservableCollection double ratio = fromFrameRate / toFrameRate; foreach (var line in subtitles) { - line.SetStartTimeOnly(TimeSpan.FromMilliseconds(line.StartTime.TotalMilliseconds * ratio)); - line.EndTime = TimeSpan.FromMilliseconds(line.EndTime.TotalMilliseconds * ratio); + line.SetTimes( + TimeSpan.FromMilliseconds(line.StartTime.TotalMilliseconds * ratio), + TimeSpan.FromMilliseconds(line.EndTime.TotalMilliseconds * ratio)); } } } \ No newline at end of file diff --git a/src/ui/Features/Sync/ChangeSpeed/ChangeSpeedViewModel.cs b/src/ui/Features/Sync/ChangeSpeed/ChangeSpeedViewModel.cs index e85664fc642..a09df7c792e 100644 --- a/src/ui/Features/Sync/ChangeSpeed/ChangeSpeedViewModel.cs +++ b/src/ui/Features/Sync/ChangeSpeed/ChangeSpeedViewModel.cs @@ -86,8 +86,10 @@ internal static void ChangeSpeed(ObservableCollection sub { foreach (var subtitle in subtitles) { - subtitle.SetStartTimeOnly(TimeSpan.FromMilliseconds(subtitle.StartTime.TotalMilliseconds * (100.0 / speedPercent))); - subtitle.EndTime = TimeSpan.FromMilliseconds(subtitle.EndTime.TotalMilliseconds * (100.0 / speedPercent)); + var factor = 100.0 / speedPercent; + subtitle.SetTimes( + TimeSpan.FromMilliseconds(subtitle.StartTime.TotalMilliseconds * factor), + TimeSpan.FromMilliseconds(subtitle.EndTime.TotalMilliseconds * factor)); } } } \ No newline at end of file