aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS/AppUpdateView.swift
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-10-24 14:16:30 -0700
committerDustin Mierau <dustin@mierau.me>2025-10-24 14:16:30 -0700
commit8bab12046b2c91cff7e1194e576acecf57551e08 (patch)
treeefd06a26ee7a12e43b6d77c147347459f8092427 /Hotline/macOS/AppUpdateView.swift
parent2df678753e2e6fe309de6b546925540f89e3d680 (diff)
Further along work towards auto updating. Show release notes for the builds that are more recent than yours.
Diffstat (limited to 'Hotline/macOS/AppUpdateView.swift')
-rw-r--r--Hotline/macOS/AppUpdateView.swift187
1 files changed, 187 insertions, 0 deletions
diff --git a/Hotline/macOS/AppUpdateView.swift b/Hotline/macOS/AppUpdateView.swift
new file mode 100644
index 0000000..89312fc
--- /dev/null
+++ b/Hotline/macOS/AppUpdateView.swift
@@ -0,0 +1,187 @@
+import SwiftUI
+import MarkdownUI
+import AppKit
+import Observation
+
+struct AppUpdateView: View {
+ @Environment(\.dismiss) private var dismiss
+ @Bindable private var update = AppUpdate.shared
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ if let message = update.message {
+ messageOnlyView(message)
+ }
+ else if update.release != nil {
+ headerSection
+ releaseNotesSection
+ actionRow
+ }
+ else {
+ defaultPlaceholder
+ }
+ }
+ .padding(.horizontal, 24)
+ .padding(.bottom, 24)
+ .padding(.top, 8)
+ .frame(width: update.message != nil ? 380 : 520)
+ .frame(idealHeight: 360)
+ .onChange(of: update.showWindow) { _, show in
+ if !show {
+ dismiss()
+ }
+ }
+ .onDisappear {
+ update.handleWindowDismissed()
+ }
+ }
+
+ private var headerSection: some View {
+ HStack(alignment: .center, spacing: 8) {
+ Image(nsImage: NSApplication.shared.applicationIconImage)
+ .resizable()
+ .scaledToFit()
+ .frame(width: 56, height: 56)
+ .shadow(color: Color.black.mix(with: .red, by: 0.4).opacity(0.15), radius: 3, y: 1.5)
+
+ VStack(alignment: .leading, spacing: 2) {
+ if let release = update.release {
+ Text("Hotline \(release.displayVersion)")
+ .font(.title2)
+ .fontWeight(.semibold)
+ }
+ Text("A new version of Hotline is available. 🎉")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+
+ private var releaseNotesSection: some View {
+ ScrollView(.vertical) {
+ Markdown(releaseNotesMarkdown())
+ .textSelection(.enabled)
+ .markdownTheme(.gitHub.text(text: {
+ FontSize(.em(0.85))
+ }))
+ .font(.system(size: 14))
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 12)
+ .padding(.horizontal, 12)
+ }
+ .frame(minHeight: 220, maxHeight: 260)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(Color(nsColor: .separatorColor), lineWidth: 1)
+ )
+ }
+
+ private var actionRow: some View {
+ HStack {
+ Button("Not Now") {
+ update.remindLater()
+ }
+ .buttonBorderShape(.capsule)
+ .keyboardShortcut(.escape, modifiers: [])
+ .controlSize(.large)
+ .disabled(update.isDownloading)
+
+ Spacer()
+
+ if update.isDownloading {
+ ProgressView()
+ .controlSize(.small)
+ .padding(.trailing, 12)
+ }
+
+ Button("Download") {
+ update.startDownload()
+ }
+ .buttonBorderShape(.capsule)
+ .keyboardShortcut(.defaultAction)
+ .controlSize(.large)
+ .disabled(update.isDownloading)
+ }
+ }
+
+ @ViewBuilder
+ private func messageOnlyView(_ message: AppUpdateMessage) -> some View {
+ let iconName = {
+ switch message.kind {
+ case .info:
+ return "info.circle"
+ case .success:
+ return "checkmark.circle.fill"
+ case .error:
+ return "exclamationmark.triangle.fill"
+ }
+ }()
+
+ HStack(alignment: .center, spacing: 12) {
+
+ if message.kind == .success {
+ Text("👍")
+ .font(.system(size: 42))
+ .shadow(color: .yellow.mix(with: .black, by: 0.3).opacity(0.2), radius: 4, y: 1.5)
+ }
+ else {
+ Image(systemName: iconName)
+ .resizable()
+ .scaledToFit()
+ .symbolRenderingMode(.multicolor)
+ .frame(width: 48, height: 48)
+ }
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(message.title)
+ .font(.title2)
+ .fontWeight(.semibold)
+ Text(message.detail)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ }
+
+ private var defaultPlaceholder: some View {
+ VStack(alignment: .center, spacing: 12) {
+ Text("No update information available.")
+ .font(.headline)
+ Button("Close") {
+ update.acknowledgeMessage()
+ }
+ .keyboardShortcut(.defaultAction)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+
+ private func releaseNotesMarkdown() -> String {
+ if let combined = update.releaseNotesCombined?
+ .trimmingCharacters(in: .whitespacesAndNewlines),
+ combined.isEmpty == false {
+ return combined
+ }
+
+ let fallback = update.release?.notes.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ return fallback.isEmpty ? "_No release notes provided._" : fallback
+ }
+}
+
+#Preview {
+ AppUpdate.shared.release = UpdateReleaseInfo(
+ tagName: "1.0beta1",
+ displayVersion: "1.0b1",
+ versionNumber: 1.0,
+ buildNumber: 1,
+ notes: """
+ - Added support for release notes in Markdown.
+ - Improved the update workflow for macOS users.
+ """,
+ downloadURL: URL(string: "https://example.com")!,
+ assetName: "Hotline.zip"
+ )
+ AppUpdate.shared.releases = [AppUpdate.shared.release!]
+ AppUpdate.shared.releaseNotesCombined = nil
+ AppUpdate.shared.showWindow = true
+ return AppUpdateView()
+}