Compare commits

..

5 Commits

Author SHA1 Message Date
nimbold b71a58ae06 fix: resolve duplicate resolution window not closing
- Dispatch executeAddDownloads asynchronously after closing the sheet to avoid swallowing dismiss()
2026-06-08 16:42:11 +03:30
github-actions[bot] 6dcb0e33e8 chore(release): update appcast for 0.6.2 2026-06-08 12:54:49 +00:00
nimbold 34847b6234 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
2026-06-08 16:22:05 +03:30
github-actions[bot] ac7963e353 chore(release): update appcast for 0.6.1 2026-06-08 12:05:49 +00:00
nimbold a4936fa141 ci: automate Sparkle appcast updates 2026-06-08 15:33:43 +03:30
7 changed files with 155 additions and 27 deletions
+80 -6
View File
@@ -31,11 +31,14 @@ jobs:
run: |
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
VERSION="${GITHUB_REF_NAME#v}"
TAG_NAME="${GITHUB_REF_NAME}"
else
VERSION="${{ inputs.version }}"
TAG_NAME="v${VERSION}"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION"
- name: Show build environment
@@ -102,17 +105,88 @@ jobs:
if-no-files-found: error
- name: Publish GitHub release
if: github.ref_type == 'tag'
if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
VERSION="${{ steps.version.outputs.version }}"
TAG_NAME="${{ steps.version.outputs.tag_name }}"
awk '/^## \['"$VERSION"'\]/{flag=1; next} /^## \[/{if(flag) exit} flag' CHANGELOG.md > release_notes.md
test -s release_notes.md
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" dist/*.dmg --clobber
gh release edit "$GITHUB_REF_NAME" --notes-file release_notes.md
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
gh release upload "$TAG_NAME" dist/*.dmg --clobber
gh release edit "$TAG_NAME" --notes-file release_notes.md
else
gh release create "$GITHUB_REF_NAME" dist/*.dmg --title "$GITHUB_REF_NAME" --notes-file release_notes.md --verify-tag
gh release create "$TAG_NAME" dist/*.dmg --title "$TAG_NAME" --notes-file release_notes.md --verify-tag
fi
- name: Update Sparkle appcast
if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
run: |
if [[ -z "${SPARKLE_ED_PRIVATE_KEY}" ]]; then
echo "Missing SPARKLE_ED_PRIVATE_KEY repository secret; cannot update appcast.xml" >&2
exit 1
fi
VERSION="${{ steps.version.outputs.version }}"
TAG_NAME="${{ steps.version.outputs.tag_name }}"
BUILD_NUMBER="${{ github.run_number }}"
DMG_PATH="dist/Firelink-${VERSION}-mac-arm64.dmg"
DMG_URL="https://github.com/nimbold/Firelink/releases/download/${TAG_NAME}/Firelink-${VERSION}-mac-arm64.dmg"
RELEASE_NOTES_URL="https://github.com/nimbold/Firelink/releases/tag/${TAG_NAME}"
PUB_DATE="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
SIGN_OUTPUT="$(printf '%s' "$SPARKLE_ED_PRIVATE_KEY" | .build/artifacts/sparkle/Sparkle/bin/sign_update --ed-key-file - "$DMG_PATH")"
ED_SIGNATURE="$(printf '%s\n' "$SIGN_OUTPUT" | sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p' | head -1)"
LENGTH="$(stat -f%z "$DMG_PATH" 2>/dev/null || stat -c%s "$DMG_PATH")"
if [[ -z "$ED_SIGNATURE" || -z "$LENGTH" ]]; then
echo "Failed to extract Sparkle signature or DMG length" >&2
printf '%s\n' "$SIGN_OUTPUT" >&2
exit 1
fi
git fetch origin main
git checkout main
git pull --ff-only origin main
cat > appcast.xml <<XML
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Firelink Updates</title>
<link>https://github.com/nimbold/Firelink</link>
<description>Most recent updates for Firelink</description>
<language>en</language>
<item>
<title>Version ${VERSION}</title>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<sparkle:hardwareRequirements>arm64</sparkle:hardwareRequirements>
<sparkle:releaseNotesLink>${RELEASE_NOTES_URL}</sparkle:releaseNotesLink>
<pubDate>${PUB_DATE}</pubDate>
<enclosure url="${DMG_URL}"
sparkle:version="${BUILD_NUMBER}"
sparkle:shortVersionString="${VERSION}"
length="${LENGTH}"
type="application/octet-stream"
sparkle:edSignature="${ED_SIGNATURE}" />
</item>
</channel>
</rss>
XML
xmllint --noout appcast.xml
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add appcast.xml
if git diff --cached --quiet; then
echo "appcast.xml already up to date"
else
git commit -m "chore(release): update appcast for ${VERSION}"
git push origin main
fi
+10
View File
@@ -5,6 +5,16 @@ 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 a bug where confirming a duplicate resolution failed to close the Add Downloads window, misleading users into thinking the download didn't start.
- 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
+5 -3
View File
@@ -56,7 +56,9 @@ struct AddDownloadsView: View {
conflicts: $conflictingDownloads,
onConfirm: {
showingDuplicates = false
executeAddDownloads(start: pendingStartFlag, conflicts: conflictingDownloads)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
executeAddDownloads(start: pendingStartFlag, conflicts: conflictingDownloads)
}
},
onCancel: {
showingDuplicates = false
@@ -312,7 +314,7 @@ struct AddDownloadsView: View {
Button("Cancel") {
dismiss()
}
.keyboardShortcut(.cancelAction)
.keyboardShortcut(showingDuplicates ? nil : .cancelAction)
Button("Add to Queue") {
addDownloads(start: false)
@@ -324,7 +326,7 @@ struct AddDownloadsView: View {
}
.buttonStyle(.borderedProminent)
.disabled(!canAddDownloads)
.keyboardShortcut(.defaultAction)
.keyboardShortcut(showingDuplicates ? nil : .defaultAction)
}
}
+1 -1
View File
@@ -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()
+10 -2
View File
@@ -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
}
@@ -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: "<br>", with: "\n", options: .caseInsensitive)
text = text.replacingOccurrences(of: "<br/>", with: "\n", options: .caseInsensitive)
text = text.replacingOccurrences(of: "<br />", with: "\n", options: .caseInsensitive)
text = text.replacingOccurrences(of: "</p>", with: "\n\n", options: .caseInsensitive)
text = text.replacingOccurrences(of: "<li>", with: "- ", options: .caseInsensitive)
text = text.replacingOccurrences(of: "</li>", with: "\n", options: .caseInsensitive)
text = text.replacingOccurrences(of: "<h1>", with: "# ", options: .caseInsensitive)
text = text.replacingOccurrences(of: "</h1>", with: "\n\n", options: .caseInsensitive)
text = text.replacingOccurrences(of: "<h2>", with: "## ", options: .caseInsensitive)
text = text.replacingOccurrences(of: "</h2>", with: "\n\n", options: .caseInsensitive)
text = text.replacingOccurrences(of: "<h3>", with: "### ", options: .caseInsensitive)
text = text.replacingOccurrences(of: "</h3>", with: "\n\n", options: .caseInsensitive)
text = text.replacingOccurrences(of: "<b>", with: "**", options: .caseInsensitive)
text = text.replacingOccurrences(of: "</b>", with: "**", options: .caseInsensitive)
text = text.replacingOccurrences(of: "<strong>", with: "**", options: .caseInsensitive)
text = text.replacingOccurrences(of: "</strong>", 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: "&nbsp;", with: " ")
text = text.replacingOccurrences(of: "&amp;", with: "&")
text = text.replacingOccurrences(of: "&lt;", with: "<")
text = text.replacingOccurrences(of: "&gt;", with: ">")
text = text.replacingOccurrences(of: "&quot;", with: "\"")
text = text.replacingOccurrences(of: "&#39;", 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) {
+8 -8
View File
@@ -6,17 +6,17 @@
<description>Most recent updates for Firelink</description>
<language>en</language>
<item>
<title>Version 0.5.7</title>
<title>Version 0.6.2</title>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<sparkle:hardwareRequirements>arm64</sparkle:hardwareRequirements>
<sparkle:releaseNotesLink>https://github.com/nimbold/Firelink/releases/tag/v0.5.7</sparkle:releaseNotesLink>
<pubDate>Sat, 06 Jun 2026 11:36:06 +0000</pubDate>
<enclosure url="https://github.com/nimbold/Firelink/releases/download/v0.5.7/Firelink-0.5.7-mac-arm64.dmg"
sparkle:version="22"
sparkle:shortVersionString="0.5.7"
length="8871979"
<sparkle:releaseNotesLink>https://github.com/nimbold/Firelink/releases/tag/v0.6.2</sparkle:releaseNotesLink>
<pubDate>Mon, 08 Jun 2026 12:54:48 +0000</pubDate>
<enclosure url="https://github.com/nimbold/Firelink/releases/download/v0.6.2/Firelink-0.6.2-mac-arm64.dmg"
sparkle:version="26"
sparkle:shortVersionString="0.6.2"
length="75882342"
type="application/octet-stream"
sparkle:edSignature="Y4QEJqGw6IeXL5JmaRqt5U22Bmc8JD02+oLRIFoKgOJO4v6p6+AmBP9VoM3yp/MAzj+gJMWiI/Gf4aPx4x3dAg==" />
sparkle:edSignature="vvylIjvPhUZMvs/XCvhPDISBdRxolstgLLDDmU2i2Nb6Hf5mnSf+orMRYV0VewBSREB49DN64l0tkHaZrkXVBQ==" />
</item>
</channel>
</rss>