Skip to content

Commit 3f777ee

Browse files
committed
Add support for Sparkle
1 parent 12c6548 commit 3f777ee

File tree

12 files changed

+205
-102
lines changed

12 files changed

+205
-102
lines changed

Config.debug.xcconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#include "Version.xcconfig"
2+
SLASH = /
23

34
BUNDLE_IDENTIFIER_BASE = dev.com.intii.CopilotForXcode
45
EXTENSION_BUNDLE_NAME = Copilot Dev
6+
SPARKLE_FEED_URL = http:$(SLASH)$(SLASH)127.0.0.1:9433/appcast.xml
7+
SPARKLE_PUBLIC_KEY = WDzm5GHnc6c8kjeJEgX5GuGiPpW6Lc/ovGjLnrrZvPY=

Config.xcconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#include "Version.xcconfig"
2+
SLASH = /
23

34
BUNDLE_IDENTIFIER_BASE = com.intii.CopilotForXcode
45
EXTENSION_BUNDLE_NAME = Copilot
6+
SPARKLE_FEED_URL = https:$(SLASH)$(SLASH)github.com/intitni/CopilotForXcode/blob/main/appcast.xml
7+
SPARKLE_PUBLIC_KEY = WDzm5GHnc6c8kjeJEgX5GuGiPpW6Lc/ovGjLnrrZvPY=

Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Copilot for Xcode/App.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import SwiftUI
22
import XPCShared
3+
import UpdateChecker
34

45
@main
56
struct CopilotForXcodeApp: App {
@@ -11,9 +12,23 @@ struct CopilotForXcodeApp: App {
1112
.onAppear {
1213
UserDefaults.setupDefaultSettings()
1314
}
15+
.environment(\.updateChecker, UpdateChecker(hostBundle: Bundle.main))
1416
}
1517
.windowStyle(.hiddenTitleBar)
1618
}
1719
}
1820

1921
var isPreview: Bool { ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" }
22+
23+
struct UpdateCheckerKey: EnvironmentKey {
24+
static var defaultValue: UpdateChecker = UpdateChecker(hostBundle: nil)
25+
}
26+
27+
extension EnvironmentValues {
28+
var updateChecker: UpdateChecker {
29+
get { self[UpdateCheckerKey.self] }
30+
set { self[UpdateCheckerKey.self] = newValue }
31+
}
32+
}
33+
34+

Copilot for Xcode/AppInfoView.swift

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import SwiftUI
22

33
struct AppInfoView: View {
44
@State var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
5+
@Environment(\.updateChecker) var updateChecker
56

67
var body: some View {
78
Section {
@@ -14,15 +15,35 @@ struct AppInfoView: View {
1415
.foregroundColor(.white.opacity(0.5))
1516

1617
Spacer()
18+
19+
Button(action: {
20+
updateChecker.checkForUpdates()
21+
}) {
22+
HStack(spacing: 2) {
23+
Image(systemName: "arrow.up.right.circle.fill")
24+
Text("Check for Updates")
25+
}
26+
}
27+
.buttonStyle(.copilot)
1728
}
1829

19-
Link(destination: URL(string: "https://github.com/intitni/CopilotForXcode")!) {
20-
HStack(spacing: 2) {
21-
Image(systemName: "link")
22-
Text("GitHub")
30+
HStack(spacing: 16) {
31+
Link(destination: URL(string: "https://github.com/intitni/CopilotForXcode")!) {
32+
HStack(spacing: 2) {
33+
Image(systemName: "link")
34+
Text("GitHub")
35+
}
36+
}
37+
.focusable(false)
38+
39+
Link(destination: URL(string: "https://www.buymeacoffee.com/intitni")!) {
40+
HStack(spacing: 2) {
41+
Image(systemName: "cup.and.saucer.fill")
42+
Text("Buy Me A Coffee")
43+
}
2344
}
45+
.focusable(false)
2446
}
25-
.focusable(false)
2647
}
2748
}
2849
}

Copilot for Xcode/SettingsView.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ final class Settings: ObservableObject {
1111
var realtimeSuggestionDebounce: Double
1212
@AppStorage(\.suggestionPresentationMode)
1313
var suggestionPresentationMode: Preferences.PresentationMode
14-
@AppStorage(\.automaticallyCheckForUpdate)
15-
var automaticallyCheckForUpdate: Bool
1614
@AppStorage(\.suggestionWidgetPositionMode)
1715
var suggestionWidgetPositionMode: SuggestionWidgetPositionMode
1816
@AppStorage(\.widgetColorScheme)
@@ -24,6 +22,7 @@ struct SettingsView: View {
2422
@StateObject var settings = Settings()
2523
@State var editingRealtimeSuggestionDebounce: Double = UserDefaults.shared
2624
.value(for: \.realtimeSuggestionDebounce)
25+
@Environment(\.updateChecker) var updateChecker
2726

2827
var body: some View {
2928
Section {
@@ -33,7 +32,10 @@ struct SettingsView: View {
3332
}
3433
.toggleStyle(.switch)
3534

36-
Toggle(isOn: $settings.automaticallyCheckForUpdate) {
35+
Toggle(isOn: .init(
36+
get: { updateChecker.automaticallyChecksForUpdates },
37+
set: { updateChecker.automaticallyChecksForUpdates = $0 }
38+
)) {
3739
Text("Automatically Check for Update")
3840
}
3941
.toggleStyle(.switch)

Copilot-for-Xcode-Info.plist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,11 @@
66
<string>$(BUNDLE_IDENTIFIER_BASE)</string>
77
<key>EXTENSION_BUNDLE_NAME</key>
88
<string>$(EXTENSION_BUNDLE_NAME)</string>
9+
<key>SUEnableJavaScript</key>
10+
<string>YES</string>
11+
<key>SUFeedURL</key>
12+
<string>$(SPARKLE_FEED_URL)</string>
13+
<key>SUPublicEDKey</key>
14+
<string>$(SPARKLE_PUBLIC_KEY)</string>
915
</dict>
1016
</plist>

Core/Package.resolved

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Core/Package.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ let package = Package(
2727
"Preferences",
2828
"LaunchAgentManager",
2929
"Logger",
30+
"UpdateChecker",
3031
]
3132
),
3233
],
@@ -37,6 +38,7 @@ let package = Package(
3738
.package(url: "https://github.com/JohnSundell/Splash", from: "0.1.0"),
3839
.package(url: "https://github.com/nmdias/FeedKit", from: "9.1.2"),
3940
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"),
41+
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
4042
],
4143
targets: [
4244
.target(name: "CGEventObserver"),
@@ -125,15 +127,19 @@ let package = Package(
125127
),
126128
.target(
127129
name: "UpdateChecker",
128-
dependencies: ["Logger", .product(name: "FeedKit", package: "FeedKit")]
130+
dependencies: [
131+
"Logger",
132+
"Sparkle",
133+
.product(name: "FeedKit", package: "FeedKit"),
134+
]
129135
),
130136
.target(name: "AXExtension"),
131137
.target(name: "Logger"),
132138
.target(
133139
name: "OpenAIService",
134140
dependencies: [
135141
"Logger",
136-
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms")
142+
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
137143
]
138144
),
139145
.testTarget(
Lines changed: 27 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,37 @@
1-
import AppKit
2-
import FeedKit
3-
import Foundation
1+
import Sparkle
42
import Logger
5-
import SwiftUI
63

7-
let skippedUpdateVersionKey = "skippedUpdateVersion"
4+
public final class UpdateChecker {
5+
let updater: SPUUpdater
6+
let hostBundleFound: Bool
87

9-
public struct UpdateChecker {
10-
var skippedUpdateVersion: String? {
11-
UserDefaults.standard.string(forKey: skippedUpdateVersionKey)
12-
}
13-
14-
public init() {}
15-
16-
public func checkForUpdate() {
17-
let url = URL(string: "https://github.com/intitni/CopilotForXcode/releases.atom")!
18-
let parser = FeedParser(URL: url)
19-
parser.parseAsync { result in
20-
switch result {
21-
case let .success(.atom(feed)):
22-
if let entry = feed.entries?.first(where: {
23-
guard let title = $0.title else { return false }
24-
return !title.contains("-")
25-
}) {
26-
self.alertIfUpdateAvailable(entry)
27-
}
28-
case let .failure(error):
29-
Logger.updateChecker.error(error)
30-
default: break
31-
}
8+
public init(hostBundle: Bundle?) {
9+
if hostBundle == nil {
10+
hostBundleFound = false
11+
Logger.updateChecker.error("Host bundle not found")
12+
} else {
13+
hostBundleFound = true
3214
}
33-
}
34-
35-
func alertIfUpdateAvailable(_ entry: AtomFeedEntry) {
36-
guard let version = entry.title,
37-
let currentVersion = Bundle.main
38-
.infoDictionary?["CFBundleShortVersionString"] as? String,
39-
version != skippedUpdateVersion,
40-
version.compare(currentVersion, options: .numeric) == .orderedDescending
41-
else { return }
42-
43-
Task { @MainActor in
44-
let screenFrame = NSScreen.main?.frame ?? .zero
45-
let window = NSWindow(
46-
contentRect: .init(
47-
x: screenFrame.midX,
48-
y: screenFrame.midY + 200,
49-
width: 500,
50-
height: 500
51-
),
52-
styleMask: .borderless,
53-
backing: .buffered,
54-
defer: true
55-
)
56-
window.level = .floating
57-
window.isReleasedWhenClosed = false
58-
window.contentViewController = NSHostingController(
59-
rootView: AlertView(entry: entry, window: window)
60-
)
61-
window.makeKeyAndOrderFront(nil)
15+
updater = SPUUpdater(
16+
hostBundle: hostBundle ?? Bundle.main,
17+
applicationBundle: Bundle.main,
18+
userDriver: SPUStandardUserDriver(hostBundle: hostBundle ?? Bundle.main, delegate: nil),
19+
delegate: nil
20+
)
21+
do {
22+
try updater.start()
23+
} catch {
24+
Logger.updateChecker.error(error.localizedDescription)
6225
}
6326
}
64-
}
65-
66-
struct AlertView: View {
67-
let entry: AtomFeedEntry
68-
let window: NSWindow
69-
70-
var body: some View {
71-
let version = entry.title ?? "0.0.0"
72-
Color.clear.alert(
73-
"Copilot for Xcode \(version) is available!",
74-
isPresented: .constant(true)
75-
) {
76-
Button {
77-
if let url = URL(string: entry.links?.first?.attributes?.href ?? "") {
78-
NSWorkspace.shared.open(url)
79-
}
80-
window.close()
81-
} label: {
82-
Text("Visit Release Page")
83-
}
8427

85-
Button {
86-
UserDefaults.standard.set(version, forKey: skippedUpdateVersionKey)
87-
window.close()
88-
} label: {
89-
Text("Skip This Version")
90-
}
28+
public func checkForUpdates() {
29+
updater.checkForUpdates()
30+
}
9131

92-
Button { window.close() } label: { Text("Cancel") }
93-
} message: {
94-
Text("Would you like to visit the release page?")
95-
}
32+
public var automaticallyChecksForUpdates: Bool {
33+
get { updater.automaticallyChecksForUpdates }
34+
set { updater.automaticallyChecksForUpdates = newValue }
9635
}
9736
}
37+

0 commit comments

Comments
 (0)