Skip to content
Open
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let package = Package(
.executableTarget(
name: "ForelApp",
dependencies: ["ForelCore"],
resources: [.copy("Resources")]
resources: [.copy("Resources"), .copy("../../CHANGELOG.md")]
),
.testTarget(
name: "ForelCoreTests",
Expand Down
4 changes: 4 additions & 0 deletions Sources/ForelApp/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
setUpStatusBar()
}
showMainWindowOnFirstLaunch()
if CommandLine.arguments.contains("-forelAfterUpdate") {
updater.loadReleaseNotesFromChangelog()
openMainWindow()
}
}

/// A brand-new install otherwise only shows up as a menu bar icon
Expand Down
65 changes: 64 additions & 1 deletion Sources/ForelApp/Update/UpdaterManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ final class UpdaterManager: ObservableObject {

let tagName: String
let htmlUrl: URL
let body: String
let assets: [Asset]
let draft: Bool
let prerelease: Bool

enum CodingKeys: String, CodingKey {
case tagName = "tag_name"
case htmlUrl = "html_url"
case body
case assets
case draft
case prerelease
Expand All @@ -58,6 +60,8 @@ final class UpdaterManager: ObservableObject {
@Published private(set) var isChecking = false
@Published private(set) var isInstalling = false
@Published private(set) var installError: String?
@Published var showReleaseNotes = true
@Published var releaseNotes: (version: String, body: String, url: URL)? = ("1.0.0", "- Correction bug X\n- Nouveau filtre Y", URL(string: "https://github.com/lab421/forel/releases/tag/v1.0.0")!)

private let db: Database
private var timer: Timer?
Expand Down Expand Up @@ -189,6 +193,65 @@ final class UpdaterManager: ObservableObject {
release.tagName.hasPrefix("v") ? String(release.tagName.dropFirst()) : release.tagName
}

/// Loads the section of CHANGELOG.md for the currently installed version
/// and populates `releaseNotes` so the UI can show a sheet.
func loadReleaseNotesFromChangelog() {
let current = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
loadReleaseNotesFromChangelog(version: current)
}

/// Loads the section of CHANGELOG.md matching `version` (or the first
/// versioned section if `version` is nil).
func loadReleaseNotesFromChangelog(version: String?) {
guard let url = Bundle.module.url(forResource: "CHANGELOG", withExtension: "md"),
let content = try? String(contentsOf: url, encoding: .utf8) else { return }

let lines = content.components(separatedBy: .newlines)
let header = version.map { "## [\($0)]" }
var sectionLines: [String] = []
var inSection = false
var foundFirst = false

for line in lines {
if line.hasPrefix("## [") {
if inSection { break }
if let header {
if line.hasPrefix(header) {
inSection = true
}
} else if !foundFirst {
inSection = true
foundFirst = true
}
continue
}
if inSection {
sectionLines.append(line)
}
}

let body = sectionLines.drop { $0.trimmingCharacters(in: .whitespaces).isEmpty }.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
guard !body.isEmpty else { return }
let v = version ?? Self.findFirstVersion(in: lines)
if let url = URL(string: "https://github.com/\(Self.repo)/releases/tag/v\(v)") {
releaseNotes = (v, body, url)
}
showReleaseNotes = true
}

private static func findFirstVersion(in lines: [String]) -> String {
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("## [") {
let start = trimmed.index(trimmed.startIndex, offsetBy: 4)
if let end = trimmed.firstIndex(of: "]") {
return String(trimmed[start..<end])
}
}
}
return "0.0.0"
}

static func isNewer(_ candidate: String, than current: String) -> Bool {
compareVersions(candidate, current) == .orderedDescending
}
Expand Down Expand Up @@ -305,7 +368,7 @@ final class UpdaterManager: ObservableObject {
/usr/bin/hdiutil detach "$MNT" -quiet 2>/dev/null || /usr/bin/hdiutil detach "$MNT" -force -quiet 2>/dev/null || true
/bin/rmdir "$MNT" 2>/dev/null
/bin/rm -f "$DMG" "$SCRIPT"
/usr/bin/open "$LAUNCH"
/usr/bin/open "$LAUNCH" --args -forelAfterUpdate
"""
let scriptURL = FileManager.default.temporaryDirectory
.appendingPathComponent("forel-update-\(pid)-\(UUID().uuidString).sh")
Expand Down
72 changes: 70 additions & 2 deletions Sources/ForelApp/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ForelCore

struct ContentView: View {
@EnvironmentObject var model: AppModel
@EnvironmentObject var updater: UpdaterManager

var body: some View {
NavigationSplitView {
Expand All @@ -27,9 +28,10 @@ struct ContentView: View {
}
.preferredColorScheme(model.appTheme.colorScheme)
.tint(ForelTheme.accent)
// ForelTheme.accent is a plain static var, not observable; bumping the
// identity here forces every descendant to rebuild and re-read it.
.id(model.accentVersion)
.sheet(isPresented: $updater.showReleaseNotes) {
ReleaseNotesView(version: updater.releaseNotes?.version ?? "", markdown: updater.releaseNotes?.body ?? "", url: updater.releaseNotes?.url)
}
}

private var errorBinding: Binding<Bool> {
Expand All @@ -44,3 +46,69 @@ struct ContentView: View {
)
}
}

private struct ReleaseNotesView: View {
let version: String
let markdown: String
let url: URL?
@Environment(\.dismiss) private var dismiss

var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 10) {
ZStack {
RoundedRectangle(cornerRadius: 9, style: .continuous)
.fill(ForelTheme.accent.opacity(0.18))
Image(systemName: "sparkles.rectangle.stack")
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(ForelTheme.accent)
}
.frame(width: 34, height: 34)
VStack(alignment: .leading, spacing: 1) {
Text("What's New in \(version)")
.font(.system(size: 15, weight: .bold))
.foregroundStyle(ForelTheme.primaryText)
Text("Forel has been updated")
.font(.system(size: 11))
.foregroundStyle(ForelTheme.secondaryText)
}
Spacer()
Button {
dismiss()
} label: {
Image(systemName: "xmark")
.font(.system(size: 11, weight: .bold))
}
.buttonStyle(.plain)
.foregroundStyle(ForelTheme.secondaryText)
}

Divider().overlay(ForelTheme.divider)

ScrollView {
Text(markdown)
.font(.system(size: 12))
.foregroundStyle(ForelTheme.primaryText)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
.scrollIndicators(.never)

HStack {
if let url {
Button("View on GitHub") {
NSWorkspace.shared.open(url)
}
.buttonStyle(SecondaryButtonStyle())
}
Spacer()
Button("Continue") { dismiss() }
.buttonStyle(PrimaryButtonStyle())
.keyboardShortcut(.defaultAction)
}
}
.padding(22)
.frame(width: 520, height: 420)
.background(ForelTheme.background)
}
}
Loading