From 91c1e6b6b627aee99a07bcf799d604ee7b616373 Mon Sep 17 00:00:00 2001 From: Muaz Date: Mon, 13 Jul 2026 05:14:57 +0300 Subject: [PATCH 1/3] Register Linux test build workflow --- .github/workflows/release-linux-test.yml | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .github/workflows/release-linux-test.yml diff --git a/.github/workflows/release-linux-test.yml b/.github/workflows/release-linux-test.yml new file mode 100644 index 00000000000..57d9b1064f5 --- /dev/null +++ b/.github/workflows/release-linux-test.yml @@ -0,0 +1,83 @@ +name: Release Linux test build + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag (blank = auto timestamp, e.g. linux-test-YYYYMMDD-HHMMSS)' + required: false + default: '' + type: string + +jobs: + build-linux: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Extract version from Se.cs + shell: pwsh + run: | + $seContent = Get-Content "src/ui/Logic/Config/Se.cs" -Raw + $m = [regex]::Match($seContent, 'public\s+static\s+string\s+Version\s*\{[^}]+\}\s*=\s*"v([^"]+)"') + if (-not $m.Success) { throw "Could not find Version in Se.cs" } + $versionString = $m.Groups[1].Value + $parts = $versionString -split '-', 2 + $numericFields = $parts[0].Split('.') + $major = [int]$numericFields[0] + $minor = if ($numericFields.Length -gt 1) { [int]$numericFields[1] } else { 0 } + $build = if ($numericFields.Length -gt 2) { [int]$numericFields[2] } else { 0 } + $revision = 0 + if ($parts.Length -gt 1) { + $rm = [regex]::Match($parts[1], '\d+$') + if ($rm.Success) { $revision = [int]$rm.Value } + } + "APP_VER_DISPLAY=$versionString" >> $env:GITHUB_ENV + "APP_VER_FULL=$major.$minor.$build.$revision" >> $env:GITHUB_ENV + + - name: Restore dependencies + run: dotnet restore + + - name: Publish Linux x64 app + run: | + dotnet publish src/ui/UI.csproj -c Release -r linux-x64 --self-contained true \ + -p:PublishSingleFile=true \ + -p:DebugSymbols=false \ + -p:DebugType=none \ + -p:Version=$APP_VER_FULL \ + -p:FileVersion=$APP_VER_FULL \ + -p:InformationalVersion=$APP_VER_DISPLAY \ + -o ./publish/linux-x64 + rm -f ./publish/linux-x64/*.pdb + chmod +x ./publish/linux-x64/SubtitleEdit || true + + - name: Tar Linux x64 build + run: tar -C ./publish/linux-x64 -czf ./SubtitleEdit-Linux-x64.tar.gz . + + - name: Generate release tag + id: tag + shell: bash + run: | + if [ -n "${{ inputs.release_tag }}" ]; then + echo "tag=$(echo '${{ inputs.release_tag }}' | xargs)" >> $GITHUB_OUTPUT + else + echo "tag=linux-test-$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT + fi + + - name: Create pre-release with Linux tarball + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: "Linux test build - ${{ steps.tag.outputs.tag }}" + prerelease: true + files: ./SubtitleEdit-Linux-x64.tar.gz From bda09a8384f720d5995e056aef8b27a2a257949c Mon Sep 17 00:00:00 2001 From: Muaz Date: Thu, 16 Jul 2026 23:00:35 +0300 Subject: [PATCH 2/3] Keep the macOS and Windows test build workflows on main They only existed on the test-build/fixes-plus-features branch, which is 362 commits behind upstream, so they were one branch deletion away from being lost and drifted from mainstream. main already carried the Linux one, so all three now live together and build against current upstream. Also drop three em dashes from the macOS release notes. --- .github/workflows/release-macos-test.yml | 295 +++++++++++++++++++++ .github/workflows/release-windows-test.yml | 89 +++++++ 2 files changed, 384 insertions(+) create mode 100644 .github/workflows/release-macos-test.yml create mode 100644 .github/workflows/release-windows-test.yml diff --git a/.github/workflows/release-macos-test.yml b/.github/workflows/release-macos-test.yml new file mode 100644 index 00000000000..b74f2185ca2 --- /dev/null +++ b/.github/workflows/release-macos-test.yml @@ -0,0 +1,295 @@ +name: Release macOS test build + +# Builds and publishes UNSIGNED macOS test DMGs (Apple Silicon + Intel) from the +# current branch and attaches them to a GitHub pre-release on this fork, so they +# can be handed to Mac testers (e.g. to try the MLX Whisper Mac GPU engine). +# +# This is a macOS-only, fork-friendly slimmed copy of build-ui.yml's macOS path: +# the release is gated ONLY on the Mac build (not on Windows/Linux/Flatpak), and +# code signing/notarization are skipped (no certificate on a fork). + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag (blank = auto timestamp, e.g. macos-test-YYYYMMDD-HHMMSS)' + required: false + default: '' + type: string + +env: + IINA_VERSION: v1.4.2 + IINA_SHA256: 2e0fd89fbba1c92a6c115171e5b51904883bb497fbe513a6961d080fbab08ff6 + +jobs: + build-macos: + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + arch_label: ARM64 + runtime: osx-arm64 + ffmpeg_url: https://www.osxexperts.net/ffmpeg81arm.zip + ffmpeg_sha256: ebb82529562b71170807bbc6b0e7eb4f0b13af8cbb0e085bb9e8f6fe709598ad + dmg_format: UDBZ + expected_arch: arm64 + - arch: x64 + arch_label: x64 + runtime: osx-x64 + ffmpeg_url: https://www.osxexperts.net/ffmpeg80intel.zip + ffmpeg_sha256: 2d24d22db78c87f394a5822867acd5c5dc5e762cd261a44bd26923f3a5af3e07 + dmg_format: UDZO + expected_arch: x86_64 + steps: + - uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Extract version from Se.cs + run: | + set -e + line=$(grep -E 'public[[:space:]]+static[[:space:]]+string[[:space:]]+Version[[:space:]]*\{' src/ui/Logic/Config/Se.cs) + version_string=$(printf '%s' "$line" | sed -E 's/.*"v([^"]+)".*/\1/') + numeric_part="${version_string%%-*}" + suffix="" + case "$version_string" in + *-*) suffix="${version_string#*-}" ;; + esac + dot_count=$(printf '%s' "$numeric_part" | tr -cd '.' | wc -c | tr -d ' ') + case "$dot_count" in + 0) numeric_part="${numeric_part}.0.0" ;; + 1) numeric_part="${numeric_part}.0" ;; + esac + revision=$(printf '%s' "$suffix" | grep -oE '[0-9]+$' || true) + revision=${revision:-0} + app_ver_full="${numeric_part}.${revision}" + echo "APP_VER_DISPLAY=$version_string" >> "$GITHUB_ENV" + echo "APP_VER_FULL=$app_ver_full" >> "$GITHUB_ENV" + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Download + verify static ffmpeg (${{ matrix.arch }}) + run: | + set -e + curl -fL -o ffmpeg.zip "${{ matrix.ffmpeg_url }}" + echo "${{ matrix.ffmpeg_sha256 }} ffmpeg.zip" | shasum -a 256 -c - + mkdir -p ffmpeg-temp + unzip -o ffmpeg.zip -d ffmpeg-temp + rm ffmpeg.zip + if [ ! -f ffmpeg-temp/ffmpeg ]; then + found=$(find ffmpeg-temp -maxdepth 3 -type f -name ffmpeg | head -1) + [ -n "$found" ] && mv "$found" ffmpeg-temp/ffmpeg + fi + chmod +x ffmpeg-temp/ffmpeg + archs=$(lipo -archs ffmpeg-temp/ffmpeg) + if [ "$archs" != "${{ matrix.expected_arch }}" ]; then + echo "ERROR: ffmpeg binary archs are '$archs', expected exactly '${{ matrix.expected_arch }}'" + exit 1 + fi + + - name: Download + verify + extract libmpv from IINA + run: | + set -e + curl -fL -o iina.dmg "https://github.com/iina/iina/releases/download/${IINA_VERSION}/IINA.${IINA_VERSION}.dmg" + echo "${IINA_SHA256} iina.dmg" | shasum -a 256 -c - + hdiutil attach iina.dmg -nobrowse -readonly -mountpoint ./iina-mnt + mkdir -p libmpv-temp + for dylib in ./iina-mnt/IINA.app/Contents/Frameworks/*.dylib; do + name=$(basename "$dylib") + case "$name" in + libswift_*) continue ;; + esac + cp -L "$dylib" "./libmpv-temp/$name" + done + hdiutil detach ./iina-mnt + rm -f iina.dmg + [ -f libmpv-temp/libmpv.2.dylib ] || { + echo "ERROR: libmpv.2.dylib not extracted from IINA - did the bundle layout change?"; exit 1; + } + + - name: Publish macOS app (${{ matrix.arch }}) + run: | + set -e + dotnet publish src/ui/UI.csproj -c Release -r ${{ matrix.runtime }} --self-contained true \ + -p:PublishSingleFile=true \ + -p:DebugSymbols=false \ + -p:DebugType=none \ + -p:Version="$APP_VER_FULL" \ + -p:FileVersion="$APP_VER_FULL" \ + -p:InformationalVersion="$APP_VER_DISPLAY" \ + -o ./publish/macos-${{ matrix.arch }} + find ./publish/macos-${{ matrix.arch }} -name "*.pdb" -delete + archs=$(lipo -archs ./publish/macos-${{ matrix.arch }}/SubtitleEdit) + if [ "$archs" != "${{ matrix.expected_arch }}" ]; then + echo "ERROR: SubtitleEdit binary archs are '$archs', expected exactly '${{ matrix.expected_arch }}'" + exit 1 + fi + + - name: Update Info.plist with version from Se.cs + run: | + chmod +x ./installer/macBundle/update-plist-version.sh + ./installer/macBundle/update-plist-version.sh + + - name: Assemble and package unsigned DMG (${{ matrix.arch }}) + run: | + set -e + app="./SubtitleEdit-${{ matrix.arch_label }}.app" + cp -R "./installer/macBundle/SubtitleEdit.app" "$app" + + cp "./publish/macos-${{ matrix.arch }}/SubtitleEdit" "$app/Contents/MacOS/" + cp "./publish/macos-${{ matrix.arch }}/"*.dylib "$app/Contents/MacOS/" 2>/dev/null || true + chmod +x "$app/Contents/MacOS/SubtitleEdit" + rm -rf "./publish/macos-${{ matrix.arch }}" + + cp "./ffmpeg-temp/ffmpeg" "$app/Contents/MacOS/" + chmod +x "$app/Contents/MacOS/ffmpeg" + rm -rf "./ffmpeg-temp" + + mkdir -p "$app/Contents/Frameworks" + cp "./libmpv-temp/"*.dylib "$app/Contents/Frameworks/" + chmod -R 755 "$app/Contents/Frameworks/" + rm -rf "./libmpv-temp" + + executable="$app/Contents/MacOS/SubtitleEdit" + if ! otool -l "$executable" | grep -q "@executable_path/../Frameworks"; then + install_name_tool -add_rpath "@executable_path/../Frameworks" "$executable" || true + fi + + # No Apple Developer ID here, so ad-hoc sign (sign -) instead of removing the + # signature. On Apple Silicon an app with NO signature is killed by the kernel and + # won't run at all; an ad-hoc signature makes it launchable once the user clears the + # download quarantine (Gatekeeper). Sign inside-out: nested dylibs + bundled ffmpeg + # first, then the app bundle. No --options runtime, so .NET's JIT works without + # hardened-runtime entitlements. This does NOT bypass Gatekeeper - the user still + # allows it once via the bundled fix-unsigned-app.sh / `xattr -cr` / Open Anyway. + for f in "$app/Contents/Frameworks/"*.dylib "$app/Contents/MacOS/"*.dylib "$app/Contents/MacOS/ffmpeg"; do + [ -f "$f" ] || continue + codesign --force --sign - "$f" 2>/dev/null || true + done + codesign --force --sign - "$app/Contents/MacOS/SubtitleEdit" 2>/dev/null || true + codesign --force --sign - "$app" 2>/dev/null || true + + dmg_dir="./dmg-temp-${{ matrix.arch }}" + dmg_out="./SubtitleEdit-macOS-${{ matrix.arch_label }}.dmg" + mkdir -p "$dmg_dir" + cp -R "$app" "$dmg_dir/Subtitle Edit.app" + rm -rf "$app" + + cp "./LICENSE" "$dmg_dir/LICENSE" + cp "./installer/macBundle/THIRD_PARTY_LICENSES.txt" "$dmg_dir/THIRD_PARTY_LICENSES.txt" + cp "./installer/macBundle/README-macOS.txt" "$dmg_dir/README-macOS.txt" + cp "./installer/macBundle/fix-unsigned-app.sh" "$dmg_dir/fix-unsigned-app.sh" + chmod +x "$dmg_dir/fix-unsigned-app.sh" + ln -s /Applications "$dmg_dir/Applications" + + sync + attempts=0 + until hdiutil create -volname "SubtitleEdit ${{ matrix.arch_label }}" -srcfolder "$dmg_dir" -ov -format ${{ matrix.dmg_format }} "$dmg_out"; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 3 ]; then + echo "::error::hdiutil create failed after $attempts attempts" + exit 1 + fi + echo "hdiutil create failed (attempt $attempts), retrying in 5s..." + sleep 5 + done + rm -rf "$dmg_dir" + + - name: Upload macOS DMG artifact + uses: actions/upload-artifact@v5 + with: + name: macos-dmg-${{ matrix.arch }} + path: ./SubtitleEdit-macOS-${{ matrix.arch_label }}.dmg + overwrite: true + + create-release: + needs: [build-macos] + # Publish whatever arch builds succeeded. The Intel/x64 leg depends on an external static + # ffmpeg whose architecture occasionally changes upstream and breaks that leg; don't let that + # block the (primary) Apple Silicon DMG from being released. + if: ${{ !cancelled() }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + + - name: Generate release tag + id: tag + run: | + if [ -n "${{ inputs.release_tag }}" ]; then + echo "tag=$(echo '${{ inputs.release_tag }}' | xargs)" >> $GITHUB_OUTPUT + else + echo "tag=macos-test-$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT + fi + + - name: Download macOS artifacts + uses: actions/download-artifact@v5 + with: + path: ./artifacts + continue-on-error: true + + - name: Stage release assets + run: | + mkdir -p ./release-assets + # Copy whichever DMGs were produced; a failed arch leg simply has no artifact. + cp ./artifacts/macos-dmg-arm64/*.dmg ./release-assets/ 2>/dev/null || true + cp ./artifacts/macos-dmg-x64/*.dmg ./release-assets/ 2>/dev/null || true + ls -la ./release-assets/ + if [ -z "$(ls -A ./release-assets/ 2>/dev/null)" ]; then + echo "::error::No DMG artifacts were produced by any arch build." + exit 1 + fi + + - name: Create pre-release with macOS DMGs + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: "macOS test build - ${{ steps.tag.outputs.tag }}" + prerelease: true + draft: false + body: | + **Unsigned macOS test build** with the **MLX Whisper Mac** (Apple GPU) speech-to-text engine. + + ### Downloads + - **Apple Silicon (M1/M2/M3/M4/M5):** `SubtitleEdit-macOS-ARM64.dmg` + - **Intel:** `SubtitleEdit-macOS-x64.dmg` + + ### Install (the app is unsigned, required Terminal steps) + This build has no paid Apple Developer signature, so macOS will otherwise show + *"Subtitle Edit is damaged"* or kill it on launch (especially on Apple Silicon). This is + expected, and it is not a defect in the build. After dragging **Subtitle Edit** to your + Applications folder, run these three commands in **Terminal**: + + ```bash + codesign --force --deep --sign - "/Applications/Subtitle Edit.app" + xattr -cr "/Applications/Subtitle Edit.app" + open "/Applications/Subtitle Edit.app" + ``` + + - The first line re-signs the app with a local ad-hoc signature so Apple Silicon will run it. + - The second clears the download quarantine. + - The third launches it. After this it opens normally every time. + + If `codesign` reports a permission error, prefix it with `sudo`. See `README-macOS.txt` in the DMG for more. + + ### Trying the MLX Whisper Mac engine (Apple GPU) + 1. Install the backend once: `pip3 install mlx-whisper` + 2. In Subtitle Edit: **Video → Audio to text (Whisper)**, pick **MLX Whisper Mac**, choose model **large-v3-turbo** and transcribe. + 3. The model downloads automatically on first use. Inference runs on the Apple GPU / Neural Engine via MLX, which is much faster than CPU on M-series Macs. + + This is a community **test build** from a fork, not an official Subtitle Edit release. + files: | + ./release-assets/SubtitleEdit-macOS-ARM64.dmg + ./release-assets/SubtitleEdit-macOS-x64.dmg diff --git a/.github/workflows/release-windows-test.yml b/.github/workflows/release-windows-test.yml new file mode 100644 index 00000000000..25142038cf5 --- /dev/null +++ b/.github/workflows/release-windows-test.yml @@ -0,0 +1,89 @@ +name: Release Windows test build + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag (blank = auto timestamp, e.g. win-test-YYYYMMDD-HHMMSS)' + required: false + default: '' + type: string + +jobs: + build-windows: + runs-on: windows-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Extract version from Se.cs + shell: pwsh + run: | + $seContent = Get-Content "src/ui/Logic/Config/Se.cs" -Raw + $m = [regex]::Match($seContent, 'public\s+static\s+string\s+Version\s*\{[^}]+\}\s*=\s*"v([^"]+)"') + if (-not $m.Success) { throw "Could not find Version in Se.cs" } + $versionString = $m.Groups[1].Value + $parts = $versionString -split '-', 2 + $numericFields = $parts[0].Split('.') + $major = [int]$numericFields[0] + $minor = if ($numericFields.Length -gt 1) { [int]$numericFields[1] } else { 0 } + $build = if ($numericFields.Length -gt 2) { [int]$numericFields[2] } else { 0 } + $revision = 0 + if ($parts.Length -gt 1) { + $rm = [regex]::Match($parts[1], '\d+$') + if ($rm.Success) { $revision = [int]$rm.Value } + } + "APP_VER_DISPLAY=$versionString" >> $env:GITHUB_ENV + "APP_VER_FULL=$major.$minor.$build.$revision" >> $env:GITHUB_ENV + + - name: Restore dependencies + run: dotnet restore + + - name: Download libmpv for Windows + run: | + Invoke-WebRequest -Uri "https://github.com/SubtitleEdit/support-files/releases/download/libmpv-2026-04-21/libmpv2-win64.zip" -OutFile "libmpv2-64.zip" + Expand-Archive -Path "libmpv2-64.zip" -DestinationPath "libmpv-temp" + + - name: Publish Windows x64 app + run: | + dotnet publish src/ui/UI.csproj -c Release -r win-x64 --self-contained true ` + -p:PublishSingleFile=true ` + -p:DebugSymbols=false ` + -p:DebugType=none ` + -p:Version=$env:APP_VER_FULL ` + -p:FileVersion=$env:APP_VER_FULL ` + -p:InformationalVersion=$env:APP_VER_DISPLAY ` + -o ./publish/windows-x64 + Copy-Item "libmpv-temp/libmpv-2.dll" "./publish/windows-x64/" + Get-ChildItem -Path "./publish/windows-x64" -Filter "*.pdb" -Recurse -File | Remove-Item -Force + + - name: Zip Windows x64 build + shell: pwsh + run: Compress-Archive -Path ./publish/windows-x64/* -DestinationPath ./SubtitleEdit-Windows-x64.zip + + - name: Generate release tag + id: tag + shell: bash + run: | + if [ -n "${{ inputs.release_tag }}" ]; then + echo "tag=$(echo '${{ inputs.release_tag }}' | xargs)" >> $GITHUB_OUTPUT + else + echo "tag=win-test-$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT + fi + + - name: Create pre-release with Windows zip + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: "Windows test build - ${{ steps.tag.outputs.tag }}" + prerelease: true + files: ./SubtitleEdit-Windows-x64.zip From 348ea1bc161c6dc1550b56b23389467de990cdf4 Mon Sep 17 00:00:00 2001 From: Muaz Date: Tue, 21 Jul 2026 18:18:12 +0300 Subject: [PATCH 3/3] Clear edit box text selection when a subtitle row is tapped After a search selects a match, the found line is already the selected row, so re-tapping it raises no SelectionChanged and the matched text stays selected. Line level actions such as italic then apply only to the selected fragment. Collapse the edit box selection on any grid tap so selecting the line acts on the whole line. Fixes #12688. --- src/ui/Features/Main/MainViewModel.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ui/Features/Main/MainViewModel.cs b/src/ui/Features/Main/MainViewModel.cs index 6df9867fee7..d4229bf0da3 100644 --- a/src/ui/Features/Main/MainViewModel.cs +++ b/src/ui/Features/Main/MainViewModel.cs @@ -22435,6 +22435,13 @@ private void CenterAudioVisualizerOnCurrentVideoPosition() internal async void OnSubtitleGridSingleTapped(object? sender, TappedEventArgs e) { + // Tapping a row means "select this line", so collapse any leftover text + // selection in the edit box (for example the fragment a search selected) + // and let line level actions such as italic apply to the whole line. + // Re-tapping the already selected row raises no SelectionChanged, so this + // cannot rely on SubtitleGridSelectionChanged. See issue #12688. + EditTextBox.ClearSelection(); + _singleTapCancellationTokenSource?.Cancel(); var cts = _singleTapCancellationTokenSource = new CancellationTokenSource();