From 34847b623497fe589cbcd4227616b448508c107e Mon Sep 17 00:00:00 2001 From: nimbold <11913706+nimbold@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:22:05 +0330 Subject: [PATCH] fix: resolve duplicate resolution shortcuts, update freeze, and process id flashing - Fix keyboard shortcut collision causing main window to intercept Enter/Escape keys when duplicate resolution sheet is open - Fix UI freeze when checking release notes for an update by parsing HTML asynchronously - Improve Sparkle changelog formatting by converting HTML tags to clean Markdown - Change internal Process ID status message to Starting... when queueing downloads - Fix EXC_BREAKPOINT crash on app launch by prioritizing Bundle.main over Bundle.module - Update CHANGELOG.md for 0.6.2 release --- CHANGELOG.md | 9 ++++ Sources/Firelink/AddDownloadsView.swift | 4 +- Sources/Firelink/DownloadController.swift | 2 +- Sources/Firelink/MediaEngineManager.swift | 12 ++++- .../Settings/InlineUpdateUserDriver.swift | 48 ++++++++++++++++--- 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddbb2bc..1af5804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.2] - 2026-06-08 + +### Fixes +- Fix keyboard shortcut collision that caused the main window to intercept Enter/Escape keys when the duplicate resolution sheet was open. +- Fix UI freeze when checking release notes for an update by parsing HTML asynchronously on a background thread. +- Improve Sparkle changelog formatting by converting HTML tags to clean Markdown instead of stripping them into an unreadable block of text. +- Change the internal `Process xxxxx` status message to a cleaner `Starting...` message when queueing a new download. +- Fix `EXC_BREAKPOINT` crash on app launch in production builds by prioritizing `Bundle.main` over `Bundle.module` when accessing resources. + ## [0.6.1] - 2026-06-08 ### New Features diff --git a/Sources/Firelink/AddDownloadsView.swift b/Sources/Firelink/AddDownloadsView.swift index 4e4f8ef..e6d830e 100644 --- a/Sources/Firelink/AddDownloadsView.swift +++ b/Sources/Firelink/AddDownloadsView.swift @@ -312,7 +312,7 @@ struct AddDownloadsView: View { Button("Cancel") { dismiss() } - .keyboardShortcut(.cancelAction) + .keyboardShortcut(showingDuplicates ? nil : .cancelAction) Button("Add to Queue") { addDownloads(start: false) @@ -324,7 +324,7 @@ struct AddDownloadsView: View { } .buttonStyle(.borderedProminent) .disabled(!canAddDownloads) - .keyboardShortcut(.defaultAction) + .keyboardShortcut(showingDuplicates ? nil : .defaultAction) } } diff --git a/Sources/Firelink/DownloadController.swift b/Sources/Firelink/DownloadController.swift index 870ad86..230ede9 100644 --- a/Sources/Firelink/DownloadController.swift +++ b/Sources/Firelink/DownloadController.swift @@ -602,7 +602,7 @@ final class DownloadController: ObservableObject { update(item.id) { $0.rpcPort = handle.rpcPort $0.rpcSecret = handle.rpcSecret - $0.message = "Process \(handle.processIdentifier)" + $0.message = "Starting..." } saveDownloads() applySpeedLimitsToActiveDownloads() diff --git a/Sources/Firelink/MediaEngineManager.swift b/Sources/Firelink/MediaEngineManager.swift index f718143..82b825e 100644 --- a/Sources/Firelink/MediaEngineManager.swift +++ b/Sources/Firelink/MediaEngineManager.swift @@ -31,11 +31,19 @@ final class MediaEngineManager: ObservableObject { } func binaryPath(for addon: AddonType) -> URL? { - for bundle in [Bundle.main, Bundle.module] { - if let bundled = bundle.url(forResource: addon.binaryName, withExtension: nil), + if let bundled = Bundle.main.url(forResource: addon.binaryName, withExtension: nil), + FileManager.default.isExecutableFile(atPath: bundled.path) { + return bundled + } + + // Prevent fatalError crash: avoid accessing Bundle.module if running in a packaged app. + if Bundle.main.bundleURL.pathExtension.lowercased() != "app" { + #if SWIFT_PACKAGE + if let bundled = Bundle.module.url(forResource: addon.binaryName, withExtension: nil), FileManager.default.isExecutableFile(atPath: bundled.path) { return bundled } + #endif } return nil } diff --git a/Sources/Firelink/Settings/InlineUpdateUserDriver.swift b/Sources/Firelink/Settings/InlineUpdateUserDriver.swift index 3f7e31a..d4c3941 100644 --- a/Sources/Firelink/Settings/InlineUpdateUserDriver.swift +++ b/Sources/Firelink/Settings/InlineUpdateUserDriver.swift @@ -32,19 +32,53 @@ class InlineUpdateUserDriver: NSObject, SPUUserDriver { } func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { - DispatchQueue.main.async { + DispatchQueue.global(qos: .userInitiated).async { if let htmlString = String(data: downloadData.data, encoding: .utf8) { - self.updater?.releaseNotes = self.stripHTML(htmlString) + let parsedText = self.fastHTMLToMarkdown(htmlString) + DispatchQueue.main.async { + self.updater?.releaseNotes = parsedText + } } } } - private func stripHTML(_ string: String) -> String { - guard let data = string.data(using: .utf8) else { return string } - if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) { - return attributedString.string.trimmingCharacters(in: .whitespacesAndNewlines) + nonisolated private func fastHTMLToMarkdown(_ html: String) -> String { + var text = html + text = text.replacingOccurrences(of: "
", with: "\n", options: .caseInsensitive) + text = text.replacingOccurrences(of: "
", with: "\n", options: .caseInsensitive) + text = text.replacingOccurrences(of: "
", with: "\n", options: .caseInsensitive) + text = text.replacingOccurrences(of: "

", with: "\n\n", options: .caseInsensitive) + text = text.replacingOccurrences(of: "
  • ", with: "- ", options: .caseInsensitive) + text = text.replacingOccurrences(of: "
  • ", with: "\n", options: .caseInsensitive) + text = text.replacingOccurrences(of: "

    ", with: "# ", options: .caseInsensitive) + text = text.replacingOccurrences(of: "

    ", with: "\n\n", options: .caseInsensitive) + text = text.replacingOccurrences(of: "

    ", with: "## ", options: .caseInsensitive) + text = text.replacingOccurrences(of: "

    ", with: "\n\n", options: .caseInsensitive) + text = text.replacingOccurrences(of: "

    ", with: "### ", options: .caseInsensitive) + text = text.replacingOccurrences(of: "

    ", with: "\n\n", options: .caseInsensitive) + text = text.replacingOccurrences(of: "", with: "**", options: .caseInsensitive) + text = text.replacingOccurrences(of: "", with: "**", options: .caseInsensitive) + text = text.replacingOccurrences(of: "", with: "**", options: .caseInsensitive) + text = text.replacingOccurrences(of: "", with: "**", options: .caseInsensitive) + + if let regex = try? NSRegularExpression(pattern: "<[^>]+>", options: .caseInsensitive) { + let range = NSRange(location: 0, length: text.utf16.count) + text = regex.stringByReplacingMatches(in: text, options: [], range: range, withTemplate: "") } - return string + + text = text.replacingOccurrences(of: " ", with: " ") + text = text.replacingOccurrences(of: "&", with: "&") + text = text.replacingOccurrences(of: "<", with: "<") + text = text.replacingOccurrences(of: ">", with: ">") + text = text.replacingOccurrences(of: """, with: "\"") + text = text.replacingOccurrences(of: "'", with: "'") + + if let regex = try? NSRegularExpression(pattern: "\\n{3,}", options: []) { + let range = NSRange(location: 0, length: text.utf16.count) + text = regex.stringByReplacingMatches(in: text, options: [], range: range, withTemplate: "\n\n") + } + + return text.trimmingCharacters(in: .whitespacesAndNewlines) } func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) {