Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions se5/PersianErrors.Tests/BatchModeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using SubtitleEdit.Plugins.PersianErrors;
using SubtitleEdit.Plugins.Shared;
using System.Text.Json;

namespace PersianErrors.Tests;

/// <summary>Subtitle Edit Batch convert: "batch" fixes every line headless, "configure" returns the groups only.</summary>
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<int> { 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());
}
}
63 changes: 59 additions & 4 deletions se5/PersianErrors/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]> _undo = new();
private List<FixItem> _fixes = new();
private CancellationTokenSource? _analysisCancellation;
private bool _suspendAnalysis;
private bool _savedSelectedLinesOnly;

[ObservableProperty] private bool _isBusy;
[ObservableProperty] private double _progress;
Expand All @@ -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
Expand All @@ -55,6 +60,7 @@ public MainViewModel(PluginRequest request)
if (e.PropertyName == nameof(GroupOption.IsEnabled))
{
OnPropertyChanged(nameof(EnabledGroupCount));
OnPropertyChanged(nameof(StatusText));
Analyze();
}
};
Expand All @@ -77,20 +83,29 @@ 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";
}
}

/// <summary>Opened from Batch convert's Configure button: only the groups matter, there is no subtitle.</summary>
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;
public string SelectedLinesText => SelectedIndices.Count == 1 ? "Selected line (1)" : $"Selected lines ({SelectedIndices.Count:N0})";
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;
Expand All @@ -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...";
Expand Down Expand Up @@ -152,7 +172,7 @@ private void RaiseState()

private async void Analyze()
{
if (_suspendAnalysis)
if (_suspendAnalysis || _isConfigure || _isBatch)
{
return;
}
Expand Down Expand Up @@ -339,9 +359,42 @@ private int ApplySelected()
private void UpdateChangedLineCount() =>
ChangedLineCount = _texts.Where((t, i) => !string.Equals(t, _originalTexts[i], StringComparison.Ordinal)).Count();

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>A "configure" request: hand back the settings only, for the next Batch convert run.</summary>
public PluginResponse BuildConfigureResponse() =>
new() { Status = PluginStatus.Ok, Settings = BuildSettings(), SettingsVersion = SettingsVersion };

/// <summary>The response for OK: applies what is still selected, then hands the lines back.</summary>
public PluginResponse BuildResponse()
{
if (_isConfigure)
{
return BuildConfigureResponse();
}

if (!IsBusy)
{
ApplySelected();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
}

Expand Down
22 changes: 17 additions & 5 deletions se5/PersianErrors/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
</StackPanel>
<TextBlock Text="{Binding SubtitleInfo}" Classes="muted" />
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center" IsVisible="{Binding IsNotConfigure}">
<Border Classes="chip" Background="{StaticResource CardBorder}" IsVisible="{Binding IsBusy}">
<StackPanel Orientation="Horizontal" Spacing="8">
<ProgressBar Value="{Binding Progress}" Width="90" MinWidth="90" VerticalAlignment="Center" />
Expand Down Expand Up @@ -181,13 +181,14 @@
<TextBlock Text="{Binding StatusText}" Classes="muted" VerticalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Button Command="{Binding UndoCommand}" IsEnabled="{Binding CanUndo}" ToolTip.Tip="Undo the last applied pass">
<Button Command="{Binding UndoCommand}" IsEnabled="{Binding CanUndo}" ToolTip.Tip="Undo the last applied pass"
IsVisible="{Binding IsNotConfigure}">
<StackPanel Orientation="Horizontal" Spacing="6">
<i:Icon Value="mdi-undo" FontSize="16" />
<TextBlock Text="Undo" />
</StackPanel>
</Button>
<Button Command="{Binding ApplyAndRecheckCommand}" IsEnabled="{Binding CanApply}"
<Button Command="{Binding ApplyAndRecheckCommand}" IsEnabled="{Binding CanApply}" IsVisible="{Binding IsNotConfigure}"
ToolTip.Tip="Apply the selected fixes and check again - one fix can open the way for another">
<StackPanel Orientation="Horizontal" Spacing="6">
<i:Icon Value="mdi-auto-fix" FontSize="16" />
Expand Down Expand Up @@ -281,7 +282,7 @@
</ItemsControl>
</StackPanel>

<StackPanel Spacing="8" Margin="4,0,0,0">
<StackPanel Spacing="8" Margin="4,0,0,0" IsVisible="{Binding IsNotConfigure}">
<StackPanel Orientation="Horizontal" Spacing="8">
<i:Icon Value="mdi-target" FontSize="16" Opacity="0.7" />
<TextBlock Text="CHECK" Classes="section" />
Expand All @@ -291,7 +292,7 @@
IsEnabled="{Binding HasSelectedLines}" />
</StackPanel>

<StackPanel Spacing="8" Margin="4,0,0,0">
<StackPanel Spacing="8" Margin="4,0,0,0" IsVisible="{Binding IsNotConfigure}">
<StackPanel Orientation="Horizontal" Spacing="8">
<i:Icon Value="mdi-eye-outline" FontSize="16" Opacity="0.7" />
<TextBlock Text="DISPLAY" Classes="section" />
Expand Down Expand Up @@ -389,6 +390,17 @@
</ItemsControl.ItemTemplate>
</ItemsControl>

<!-- Configure (Batch convert) -->
<StackPanel DockPanel.Dock="Top" IsVisible="{Binding IsConfigure}" HorizontalAlignment="Center"
Margin="0,120,0,0" Spacing="10" MaxWidth="520">
<Border Width="88" Height="88" CornerRadius="44" Background="{StaticResource FixTint}" HorizontalAlignment="Center">
<i:Icon Value="mdi-cog-outline" FontSize="48" Foreground="{StaticResource FixBrush}" />
</Border>
<TextBlock Text="Batch convert settings" FontSize="18" FontWeight="SemiBold" HorizontalAlignment="Center" />
<TextBlock Classes="muted" TextWrapping="Wrap" TextAlignment="Center"
Text="Turn the fix groups on or off in the sidebar and click OK. Batch convert then applies every fix of the enabled groups to all lines of each file without showing this window." />
</StackPanel>

<!-- Empty states -->
<StackPanel DockPanel.Dock="Top" IsVisible="{Binding IsClean}" HorizontalAlignment="Center"
Margin="0,120,0,0" Spacing="10">
Expand Down
2 changes: 1 addition & 1 deletion se5/PersianErrors/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ public static class Program
public static int Main(string[] args)
{
IconProvider.Current.Register<MaterialDesignIconProvider>();
return PluginBootstrap.Run<App>(args);
return PluginBootstrap.Run<App>(args, request => new MainViewModel(request).BuildBatchResponse());
}
}
11 changes: 11 additions & 0 deletions se5/PersianErrors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ The groups run in this order, so a later group sees the output of an earlier one

The enabled groups, the display option and the scope are remembered between runs.

## Batch convert

Persian Subtitle Fixes supports Subtitle Edit's **Batch convert** (Tools → Batch convert → Run
plugin; needs a Subtitle Edit version with plugin batch support).

- **Configure...** opens this window without a subtitle. Turn the fix groups on or off and click
OK. The groups are the same setting a normal run remembers.
- During the batch each file is checked against the enabled groups and **every fix is applied**
to all lines, like OK with the default selection (one pass, no re-check). A file with nothing
to fix is left unchanged.

## Differences from the Subtitle Edit 3/4 plugin

- Every subtitle format is kept. The plugin changes only the text of each line (`subtitle.paragraphs`),
Expand Down
1 change: 1 addition & 0 deletions se5/PersianErrors/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"url": "https://github.com/SubtitleEdit/plugins/tree/main/se5/PersianErrors",
"menu": "Tools",
"minSeVersion": "5.0.0",
"batch": true,
"runtime": "dotnet",
"entry": "PersianErrors.dll"
}
Loading