diff --git a/se5/PersianErrors.Tests/BatchModeTests.cs b/se5/PersianErrors.Tests/BatchModeTests.cs
new file mode 100644
index 00000000..1af6965d
--- /dev/null
+++ b/se5/PersianErrors.Tests/BatchModeTests.cs
@@ -0,0 +1,83 @@
+using SubtitleEdit.Plugins.PersianErrors;
+using SubtitleEdit.Plugins.Shared;
+using System.Text.Json;
+
+namespace PersianErrors.Tests;
+
+/// Subtitle Edit Batch convert: "batch" fixes every line headless, "configure" returns the groups only.
+public class BatchModeTests
+{
+ private static PluginRequest Request(string requestType, string? settingsJson, params string[] texts) => new()
+ {
+ RequestType = requestType,
+ Subtitle = new PluginSubtitle
+ {
+ Format = "SubRip",
+ Paragraphs = texts.Select((t, i) => new PluginParagraph { StartMs = i * 1000, EndMs = i * 1000 + 900, Text = t }).ToList(),
+ },
+ Settings = settingsJson == null ? null : JsonDocument.Parse(settingsJson).RootElement.Clone(),
+ SettingsVersion = settingsJson == null ? null : 1,
+ };
+
+ [Fact]
+ public void Batch_FixesAllLines()
+ {
+ var response = new MainViewModel(Request(PluginRequestType.Batch, null, "شدني", "درست", "املاك")).BuildBatchResponse();
+
+ Assert.Equal(PluginStatus.Ok, response.Status);
+ Assert.Equal(new[] { "شدنی", "درست", "املاک" }, response.Subtitle!.Paragraphs!.Select(p => p.Text));
+ Assert.Null(response.Settings);
+ Assert.Null(response.SettingsVersion);
+ }
+
+ [Fact]
+ public void Batch_NothingToFix_ReturnsOkWithoutSubtitle()
+ {
+ var response = new MainViewModel(Request(PluginRequestType.Batch, null, "درست")).BuildBatchResponse();
+
+ Assert.Equal(PluginStatus.Ok, response.Status);
+ Assert.Null(response.Subtitle);
+ }
+
+ [Fact]
+ public void Batch_SkipsDisabledGroups()
+ {
+ var settings = "{\"disabledGroups\":[\"Change Arabic Chars to Persian\"]}";
+
+ var response = new MainViewModel(Request(PluginRequestType.Batch, settings, "شدني")).BuildBatchResponse();
+
+ Assert.Equal(PluginStatus.Ok, response.Status);
+ Assert.Null(response.Subtitle);
+ }
+
+ [Fact]
+ public void Batch_IgnoresSelectedLinesOnly()
+ {
+ var request = Request(PluginRequestType.Batch, "{\"selectedLinesOnly\":true}", "شدني", "املاك");
+ request.SelectedIndices = new List { 0 };
+
+ var response = new MainViewModel(request).BuildBatchResponse();
+
+ Assert.Equal(new[] { "شدنی", "املاک" }, response.Subtitle!.Paragraphs!.Select(p => p.Text));
+ }
+
+ [Fact]
+ public void Configure_ReturnsSettingsWithoutSubtitle()
+ {
+ var vm = new MainViewModel(Request(PluginRequestType.Configure, "{\"selectedLinesOnly\":true,\"showHidden\":false}"));
+ Assert.True(vm.IsConfigure);
+ Assert.False(vm.IsClean);
+ Assert.False(vm.IsBusy);
+ vm.Groups.Single(g => g.Name == "OCR").IsEnabled = false;
+
+ var response = vm.BuildResponse();
+
+ Assert.Equal(PluginStatus.Ok, response.Status);
+ Assert.Null(response.Subtitle);
+ Assert.Equal(1, response.SettingsVersion);
+ var settings = response.Settings!.Value;
+ Assert.Equal(new[] { "OCR" }, settings.GetProperty("disabledGroups").EnumerateArray().Select(e => e.GetString()));
+ Assert.True(settings.GetProperty("selectedLinesOnly").GetBoolean());
+ Assert.False(settings.GetProperty("showHidden").GetBoolean());
+ }
+}
diff --git a/se5/PersianErrors/MainViewModel.cs b/se5/PersianErrors/MainViewModel.cs
index 249bd0ba..d3f5ab7d 100644
--- a/se5/PersianErrors/MainViewModel.cs
+++ b/se5/PersianErrors/MainViewModel.cs
@@ -17,10 +17,13 @@ public sealed partial class MainViewModel : ObservableObject
private readonly string[] _originalTexts;
private readonly string[] _texts;
private readonly long _videoOffsetMs;
+ private readonly bool _isConfigure;
+ private readonly bool _isBatch;
private readonly Stack _undo = new();
private List _fixes = new();
private CancellationTokenSource? _analysisCancellation;
private bool _suspendAnalysis;
+ private bool _savedSelectedLinesOnly;
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private double _progress;
@@ -36,6 +39,8 @@ public sealed partial class MainViewModel : ObservableObject
public MainViewModel(PluginRequest request)
{
_request = request;
+ _isConfigure = request.RequestType == PluginRequestType.Configure;
+ _isBatch = request.RequestType == PluginRequestType.Batch;
_videoOffsetMs = request.Rules?.VideoOffsetMs ?? 0;
_legacyRequest = request.Subtitle.Paragraphs == null;
_paragraphs = _legacyRequest
@@ -55,6 +60,7 @@ public MainViewModel(PluginRequest request)
if (e.PropertyName == nameof(GroupOption.IsEnabled))
{
OnPropertyChanged(nameof(EnabledGroupCount));
+ OnPropertyChanged(nameof(StatusText));
Analyze();
}
};
@@ -77,12 +83,21 @@ public string SubtitleInfo
{
get
{
+ if (_isConfigure)
+ {
+ return "Settings for Batch convert";
+ }
+
var name = string.IsNullOrEmpty(_request.Subtitle.FileName) ? "Untitled" : Path.GetFileName(_request.Subtitle.FileName);
var format = string.IsNullOrEmpty(_request.Subtitle.Format) ? string.Empty : $" · {_request.Subtitle.Format}";
return $"{name}{format} · {_paragraphs.Count:N0} lines";
}
}
+ /// Opened from Batch convert's Configure button: only the groups matter, there is no subtitle.
+ public bool IsConfigure => _isConfigure;
+ public bool IsNotConfigure => !_isConfigure;
+
public int EnabledGroupCount => Groups.Count(g => g.IsEnabled);
public int TotalRuleCount => Groups.Sum(g => g.Group.Rules.Count);
public bool HasSelectedLines => SelectedIndices.Count > 0;
@@ -90,7 +105,7 @@ public string SubtitleInfo
public bool AllLines { get => !SelectedLinesOnly; set => SelectedLinesOnly = !value; }
public bool HasFixes => FixCount > 0;
public bool HasVisibleFixes => VisibleFixes.Count > 0;
- public bool IsClean => !IsBusy && FixCount == 0;
+ public bool IsClean => !IsBusy && FixCount == 0 && !_isConfigure;
public bool IsFilteredEmpty => !IsBusy && FixCount > 0 && VisibleFixes.Count == 0;
public bool HasChanges => ChangedLineCount > 0;
public bool CanApply => !IsBusy && SelectedCount > 0;
@@ -103,6 +118,11 @@ public string StatusText
{
get
{
+ if (_isConfigure)
+ {
+ return EnabledGroupCount == 1 ? "1 group used for every file in Batch convert" : $"{EnabledGroupCount:N0} groups used for every file in Batch convert";
+ }
+
if (IsBusy)
{
return $"Checking {LinesInScope:N0} lines against {Groups.Where(g => g.IsEnabled).Sum(g => g.Group.Rules.Count):N0} rules...";
@@ -152,7 +172,7 @@ private void RaiseState()
private async void Analyze()
{
- if (_suspendAnalysis)
+ if (_suspendAnalysis || _isConfigure || _isBatch)
{
return;
}
@@ -339,9 +359,42 @@ private int ApplySelected()
private void UpdateChangedLineCount() =>
ChangedLineCount = _texts.Where((t, i) => !string.Equals(t, _originalTexts[i], StringComparison.Ordinal)).Count();
+ ///
+ /// A "batch" request (Batch convert): run the saved enabled groups over every line, without UI,
+ /// and keep every fix - what OK does with the default selection. No fixes is "ok" without a subtitle.
+ ///
+ public PluginResponse BuildBatchResponse()
+ {
+ var groups = Groups.Where(g => g.IsEnabled).Select(g => g.Group).ToList();
+ foreach (var fix in PersianFixer.FindFixes(_texts, Enumerable.Range(0, _texts.Length), groups))
+ {
+ _texts[fix.Index] = fix.After;
+ }
+
+ UpdateChangedLineCount();
+ if (ChangedLineCount == 0)
+ {
+ return new PluginResponse { Status = PluginStatus.Ok, Message = "Persian Subtitle Fixes: nothing to fix." };
+ }
+
+ var response = BuildResponse();
+ response.Settings = null;
+ response.SettingsVersion = null;
+ return response;
+ }
+
+ /// A "configure" request: hand back the settings only, for the next Batch convert run.
+ public PluginResponse BuildConfigureResponse() =>
+ new() { Status = PluginStatus.Ok, Settings = BuildSettings(), SettingsVersion = SettingsVersion };
+
/// The response for OK: applies what is still selected, then hands the lines back.
public PluginResponse BuildResponse()
{
+ if (_isConfigure)
+ {
+ return BuildConfigureResponse();
+ }
+
if (!IsBusy)
{
ApplySelected();
@@ -400,7 +453,8 @@ private JsonElement BuildSettings()
{
["disabledGroups"] = Groups.Where(g => !g.IsEnabled).Select(g => g.Name).ToArray(),
["showHidden"] = ShowHidden,
- ["selectedLinesOnly"] = SelectedLinesOnly,
+ // Configure has no selection: keep the scope the window remembered.
+ ["selectedLinesOnly"] = _isConfigure ? _savedSelectedLinesOnly : SelectedLinesOnly,
};
return JsonSerializer.SerializeToElement(payload);
}
@@ -428,7 +482,8 @@ private void LoadSettings()
if (settings.TryGetProperty("selectedLinesOnly", out var selectedOnly) && selectedOnly.ValueKind is JsonValueKind.True or JsonValueKind.False)
{
- SelectedLinesOnly = selectedOnly.GetBoolean() && HasSelectedLines;
+ _savedSelectedLinesOnly = selectedOnly.GetBoolean();
+ SelectedLinesOnly = _savedSelectedLinesOnly && HasSelectedLines;
}
}
diff --git a/se5/PersianErrors/MainWindow.axaml b/se5/PersianErrors/MainWindow.axaml
index 4fb40e00..f2436048 100644
--- a/se5/PersianErrors/MainWindow.axaml
+++ b/se5/PersianErrors/MainWindow.axaml
@@ -146,7 +146,7 @@
-
+
@@ -181,13 +181,14 @@
-