diff --git a/.github/workflows/split-dialogs.yml b/.github/workflows/split-dialogs.yml
index aeb86ce2..39e27d98 100644
--- a/.github/workflows/split-dialogs.yml
+++ b/.github/workflows/split-dialogs.yml
@@ -5,11 +5,13 @@ on:
branches: [main]
paths:
- 'se5/SplitDialogs/**'
+ - 'se5/SplitDialogs.Tests/**'
- 'se5/Plugin-Shared/**'
- '.github/workflows/split-dialogs.yml'
pull_request:
paths:
- 'se5/SplitDialogs/**'
+ - 'se5/SplitDialogs.Tests/**'
- 'se5/Plugin-Shared/**'
- '.github/workflows/split-dialogs.yml'
workflow_dispatch:
@@ -90,8 +92,22 @@ jobs:
echo "ref=$ref" >> "$GITHUB_OUTPUT"
echo "release=$release_flag" >> "$GITHUB_OUTPUT"
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Test
+ run: dotnet test se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj -c Release
+
build:
- needs: prepare
+ needs: [prepare, test]
runs-on: ubuntu-latest
strategy:
fail-fast: false
diff --git a/se5/SplitDialogs.Tests/BatchModeTests.cs b/se5/SplitDialogs.Tests/BatchModeTests.cs
new file mode 100644
index 00000000..03c41d77
--- /dev/null
+++ b/se5/SplitDialogs.Tests/BatchModeTests.cs
@@ -0,0 +1,108 @@
+using SubtitleEdit.Plugins.Shared;
+using SubtitleEdit.Plugins.SplitDialogs;
+using System.Text.Json;
+
+namespace SplitDialogs.Tests;
+
+/// Subtitle Edit Batch convert: "batch" splits every dialog headless, "configure" returns the options only.
+public class BatchModeTests
+{
+ private const string DialogSrt =
+ "1\n00:00:01,000 --> 00:00:04,000\n- Andro!\n- Ben, I've been oozed.\n\n" +
+ "2\n00:00:05,000 --> 00:00:07,000\nPlain line.\n\n" +
+ "3\n00:00:08,000 --> 00:00:10,000\n- How are you\n- Fine.\n";
+
+ private static PluginRequest Request(string requestType, string srt, string? settingsJson = null) => new()
+ {
+ RequestType = requestType,
+ Subtitle = new PluginSubtitle { Format = "SubRip", SubRip = srt, Native = srt },
+ Settings = settingsJson == null ? null : JsonDocument.Parse(settingsJson).RootElement.Clone(),
+ SettingsVersion = settingsJson == null ? null : 1,
+ };
+
+ private static List Texts(PluginResponse response) =>
+ new SrtCueDocument(response.Subtitle!.Native).Cues.Select(c => string.Join("\n", c.Lines)).ToList();
+
+ [Fact]
+ public void Batch_SplitsAllDialogs_WithDefaultSettings()
+ {
+ var response = BatchMode.BuildBatchResponse(Request(PluginRequestType.Batch, DialogSrt));
+
+ Assert.Equal(PluginStatus.Ok, response.Status);
+ Assert.Equal("SubRip", response.Subtitle!.Format);
+ // Line 3 is not split by default: "How are you" does not end a sentence.
+ Assert.Equal(new[] { "Andro!", "Ben, I've been oozed.", "Plain line.", "- How are you\n- Fine." }, Texts(response));
+ Assert.Null(response.Settings);
+ Assert.Null(response.SettingsVersion);
+ }
+
+ [Fact]
+ public void Batch_UsesSavedSettings()
+ {
+ var response = BatchMode.BuildBatchResponse(
+ Request(PluginRequestType.Batch, DialogSrt, "{\"requireSentenceEnding\":false,\"gapMs\":100}"));
+
+ var cues = new SrtCueDocument(response.Subtitle!.Native).Cues;
+ Assert.Equal(new[] { "Andro!", "Ben, I've been oozed.", "Plain line.", "How are you", "Fine." },
+ cues.Select(c => string.Join("\n", c.Lines)));
+ Assert.Equal(100, cues[4].StartMs - cues[3].EndMs);
+ Assert.Equal(1000, cues[0].StartMs);
+ Assert.Equal(10000, cues[4].EndMs);
+ }
+
+ [Fact]
+ public void Batch_NoDialogs_ReturnsOkWithoutSubtitle()
+ {
+ var response = BatchMode.BuildBatchResponse(
+ Request(PluginRequestType.Batch, "1\n00:00:01,000 --> 00:00:02,000\nHello.\n"));
+
+ Assert.Equal(PluginStatus.Ok, response.Status);
+ Assert.Null(response.Subtitle);
+ }
+
+ [Fact]
+ public void Batch_Assa_KeepsNativeFormat()
+ {
+ const string assa =
+ "[Script Info]\nScriptType: v4.00+\n\n[Events]\n" +
+ "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n" +
+ "Dialogue: 0,0:00:01.00,0:00:04.00,Top,Bob,0,0,0,,- Hi!\\N- Hello.\n";
+ var request = new PluginRequest
+ {
+ RequestType = PluginRequestType.Batch,
+ Subtitle = new PluginSubtitle { Format = "Advanced Sub Station Alpha", Native = assa },
+ };
+
+ var response = BatchMode.BuildBatchResponse(request);
+
+ Assert.Equal("Advanced Sub Station Alpha", response.Subtitle!.Format);
+ var dialogues = response.Subtitle.Native.Split('\n').Where(l => l.StartsWith("Dialogue:")).ToList();
+ Assert.Equal(2, dialogues.Count);
+ Assert.All(dialogues, d => Assert.Contains(",Top,Bob,", d));
+ Assert.EndsWith(",Hi!", dialogues[0]);
+ Assert.EndsWith(",Hello.", dialogues[1]);
+ }
+
+ [Fact]
+ public void Configure_ReturnsSettingsWithoutSubtitle()
+ {
+ var response = BatchMode.BuildConfigureResponse(new SplitSettings(false, 80));
+
+ Assert.Equal(PluginStatus.Ok, response.Status);
+ Assert.Null(response.Subtitle);
+ Assert.Equal(1, response.SettingsVersion);
+ Assert.False(response.Settings!.Value.GetProperty("requireSentenceEnding").GetBoolean());
+ Assert.Equal(80, response.Settings.Value.GetProperty("gapMs").GetInt32());
+ }
+
+ [Fact]
+ public void Settings_RoundTrip_AndDefaults()
+ {
+ var saved = new SplitSettings(false, 42);
+ var request = new PluginRequest { Settings = saved.ToJson(), SettingsVersion = SplitSettings.Version };
+
+ Assert.Equal(saved, SplitSettings.Load(request));
+ Assert.Equal(SplitSettings.Default, SplitSettings.Load(new PluginRequest()));
+ Assert.Equal(SplitSettings.Default, SplitSettings.Load(new PluginRequest { Settings = saved.ToJson(), SettingsVersion = 99 }));
+ }
+}
diff --git a/se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj b/se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj
new file mode 100644
index 00000000..ff6f6284
--- /dev/null
+++ b/se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/se5/SplitDialogs/BatchMode.cs b/se5/SplitDialogs/BatchMode.cs
new file mode 100644
index 00000000..9facaa1a
--- /dev/null
+++ b/se5/SplitDialogs/BatchMode.cs
@@ -0,0 +1,54 @@
+using SubtitleEdit.Plugins.Shared;
+
+namespace SubtitleEdit.Plugins.SplitDialogs;
+
+/// Subtitle Edit's Batch convert: "batch" splits headless, "configure" returns the settings only.
+public static class BatchMode
+{
+ ///
+ /// A "batch" request: split every dialog line in the file with the saved options, without
+ /// UI - what the window would do with its default "all rows checked". No dialogs is "ok"
+ /// without a subtitle.
+ ///
+ public static PluginResponse BuildBatchResponse(PluginRequest request)
+ {
+ var settings = SplitSettings.Load(request);
+ var document = CueDocument.Create(request.Subtitle);
+ var splits = new Dictionary>();
+ foreach (var cue in document.Cues)
+ {
+ if (!cue.CanSplit)
+ {
+ continue;
+ }
+
+ var texts = DialogSplitter.Split(cue.Lines, settings.RequireSentenceEnding);
+ if (texts != null)
+ {
+ splits[cue.Index] = SplitTiming.Distribute(cue.StartMs, cue.EndMs, texts, settings.GapMs);
+ }
+ }
+
+ if (splits.Count == 0)
+ {
+ return new PluginResponse { Status = PluginStatus.Ok, Message = "Split dialogs: no dialog lines found." };
+ }
+
+ var lineCount = splits.Values.Sum(p => p.Count);
+ return new PluginResponse
+ {
+ Status = PluginStatus.Ok,
+ Message = $"Split {splits.Count} dialog line(s) into {lineCount} lines.",
+ UndoDescription = "Split dialogs",
+ Subtitle = new PluginSubtitle
+ {
+ Format = document.FormatName,
+ Native = document.Build(splits),
+ },
+ };
+ }
+
+ /// A "configure" request: hand back the settings only, for the next Batch convert run.
+ public static PluginResponse BuildConfigureResponse(SplitSettings settings) =>
+ new() { Status = PluginStatus.Ok, Settings = settings.ToJson(), SettingsVersion = SplitSettings.Version };
+}
diff --git a/se5/SplitDialogs/MainWindow.axaml b/se5/SplitDialogs/MainWindow.axaml
index e4ca4edb..7618d0a3 100644
--- a/se5/SplitDialogs/MainWindow.axaml
+++ b/se5/SplitDialogs/MainWindow.axaml
@@ -64,7 +64,8 @@
-
@@ -72,6 +73,24 @@
+
+
+
+
+
+
_rows = new();
private readonly HashSet _excludedCues = new();
@@ -30,12 +26,15 @@ public partial class MainWindow : Window
private Border _listBorder = null!;
private ListBox _dialogsList = null!;
private Button _applyButton = null!;
+ private StackPanel _selectButtonsPanel = null!;
+ private StackPanel _configurePanel = null!;
public MainWindow() : this(new PluginRequest()) { }
public MainWindow(PluginRequest request)
{
_request = request;
+ _isConfigure = request.RequestType == PluginRequestType.Configure;
InitializeComponent();
_document = CueDocument.Create(request.Subtitle);
@@ -44,9 +43,24 @@ public MainWindow(PluginRequest request)
: "all lines";
_subtitleLabel.Text = $"Dialog lines found in {_scope}. Each checked line becomes one subtitle per speaker, without the dash, and its time is shared out by text length.";
- var (requireSentenceEnding, gapMs) = LoadSettings(request);
- _requireSentenceEndingCheckBox.IsChecked = requireSentenceEnding;
- _gapInput.Value = gapMs;
+ var settings = SplitSettings.Load(request);
+ _requireSentenceEndingCheckBox.IsChecked = settings.RequireSentenceEnding;
+ _gapInput.Value = settings.GapMs;
+
+ if (_isConfigure)
+ {
+ // Opened from Batch convert's Configure button: no subtitle, only the two options.
+ _subtitleLabel.Text = "Settings for Batch convert.";
+ _selectButtonsPanel.IsVisible = false;
+ _headerBorder.IsVisible = false;
+ _listBorder.IsVisible = false;
+ _configurePanel.IsVisible = true;
+ _summaryLabel.Text = "Used for every file in Batch convert";
+ _applyButton.Content = "OK";
+ _applyButton.IsEnabled = true;
+ return;
+ }
+
_requireSentenceEndingCheckBox.IsCheckedChanged += (_, _) => BuildRows();
_gapInput.ValueChanged += (_, _) => UpdateParts();
@@ -66,6 +80,8 @@ private void InitializeComponent()
_listBorder = this.FindControl("ListBorder")!;
_dialogsList = this.FindControl("DialogsList")!;
_applyButton = this.FindControl