1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
import SwiftUI
enum VersionCheckState {
case needToCheck
case checking
case upToDate
case updateAvailable(version: String)
}
struct AboutView: View {
@Environment(\.openURL) private var openURL
@State private var versionCheck: VersionCheckState = .needToCheck
@State private var downloadURL: String = "https://github.com/mierau/hotline/releases/latest"
var body: some View {
VStack(alignment: .center, spacing: 0) {
Spacer()
Image("About Hotline")
.padding(.top, 32)
Text("Hotline")
.font(.title)
.fontWeight(.semibold)
.padding(.top, 16)
.foregroundColor(.white)
let appDetails = getAppVersionAndBuild()
Text("\(String(format: "%.1f", appDetails.version))b\(appDetails.build)")
.fontWeight(.semibold)
.foregroundColor(.white)
.opacity(0.6)
HStack(alignment: .center) {
switch versionCheck {
case .needToCheck:
Button("Check for Updates") {
Task {
await checkForUpdate()
}
}
.controlSize(.regular)
case .checking:
HStack(spacing: 8) {
ProgressView()
.controlSize(.small)
.tint(.white)
Text("Checking for updates...")
.fontWeight(.semibold)
.foregroundStyle(.white)
}
case .upToDate:
Label("Hotline is up to date.", systemImage: "checkmark.circle.fill")
.foregroundStyle(.white)
.fontWeight(.semibold)
.tint(.white)
.onTapGesture {
versionCheck = .needToCheck
}
case .updateAvailable(let version):
Button("Download Latest \(version)") {
if let url = URL(string: downloadURL) {
openURL(url)
}
}
.controlSize(.regular)
}
}
.frame(height: 40)
.padding(.top, 8)
Spacer()
}
.frame(width: 300, height: 400)
.ignoresSafeArea()
}
func checkForUpdate() async {
let appDetails = getAppVersionAndBuild()
self.versionCheck = .checking
do {
let url = URL(string: "https://api.github.com/repos/mierau/hotline/releases/latest")!
let (data, _) = try await URLSession.shared.data(from: url)
let versionExpression = /^([0-9\.]+)beta([0-9]+)/
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let tagName = json["tag_name"] as? String,
let assets = json["assets"] as? [[String: Any]],
let firstAsset = assets.first,
let assetDownloadURL = firstAsset["browser_download_url"] as? String,
let versionMatches = try? versionExpression.wholeMatch(in: tagName) {
if let versionNumber = Double(versionMatches.1),
let buildNumber = Int(versionMatches.2),
versionNumber > appDetails.version || buildNumber > appDetails.build {
let versionString = "\(versionMatches.1)b\(versionMatches.2)"
self.versionCheck = .updateAvailable(version: versionString)
downloadURL = assetDownloadURL
}
else {
self.versionCheck = .upToDate
}
}
else {
self.versionCheck = .needToCheck
}
}
catch {
self.versionCheck = .needToCheck
}
}
func getAppVersionAndBuild() -> (version: Double, build: Int) {
let infoDictionary = Bundle.main.infoDictionary!
let version = Double(infoDictionary["CFBundleShortVersionString"]! as! String)!
let build = Int(infoDictionary["CFBundleVersion"]! as! String)!
return (version, build)
}
}
#Preview {
AboutView()
}
|