Skip to content

Commit 723add8

Browse files
author
CSL
committed
Add Sparkle auto-update with SwiftUI settings UI
1 parent 65437a0 commit 723add8

9 files changed

Lines changed: 236 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ iOSInjectionProject/
9494
# macOS
9595
.DS_Store
9696

97+
# CocoaPods
98+
Pods/
99+
97100
# Build artifacts
98101
*.dmg
99102
*.dmg.sha256

Podfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
platform :osx, '13.0'
2+
use_frameworks!
3+
4+
target 'Seahorse' do
5+
pod 'Sparkle', '~> 2.6'
6+
end
7+
8+
post_install do |installer|
9+
installer.pods_project.targets.each do |target|
10+
target.build_configurations.each do |config|
11+
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
12+
end
13+
end
14+
end
15+

Podfile.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
PODS:
2+
- Sparkle (2.8.1)
3+
4+
DEPENDENCIES:
5+
- Sparkle (~> 2.6)
6+
7+
SPEC REPOS:
8+
trunk:
9+
- Sparkle
10+
11+
SPEC CHECKSUMS:
12+
Sparkle: a346a4341537c625955751ed3ae4b340b68551fa
13+
14+
PODFILE CHECKSUM: 2a2178557cf1f5fa6482cb8440320258aac23cf6
15+
16+
COCOAPODS: 1.16.2

Seahorse.xcodeproj/project.pbxproj

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
A79823F56A914FFEFD22770F /* Pods_Seahorse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18E7A1921ECE390AA6BEAE44 /* Pods_Seahorse.framework */; };
1011
C404BAF32EC7301C007D7FD4 /* OpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = C404BAF22EC7301C007D7FD4 /* OpenAI */; };
1112
C4B4298C2EE00D5600EF6DB2 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = C4B4298B2EE00D5600EF6DB2 /* Kingfisher */; };
1213
/* End PBXBuildFile section */
1314

1415
/* Begin PBXFileReference section */
16+
18E7A1921ECE390AA6BEAE44 /* Pods_Seahorse.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Seahorse.framework; sourceTree = BUILT_PRODUCTS_DIR; };
17+
3DCFB21E74EAE507F2254B97 /* Pods-Seahorse.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Seahorse.debug.xcconfig"; path = "Target Support Files/Pods-Seahorse/Pods-Seahorse.debug.xcconfig"; sourceTree = "<group>"; };
1518
C404BA202EC6C12F007D7FD4 /* Seahorse.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Seahorse.app; sourceTree = BUILT_PRODUCTS_DIR; };
19+
CBC401249550BFF57145509A /* Pods-Seahorse.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Seahorse.release.xcconfig"; path = "Target Support Files/Pods-Seahorse/Pods-Seahorse.release.xcconfig"; sourceTree = "<group>"; };
1620
/* End PBXFileReference section */
1721

1822
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -43,17 +47,29 @@
4347
files = (
4448
C4B4298C2EE00D5600EF6DB2 /* Kingfisher in Frameworks */,
4549
C404BAF32EC7301C007D7FD4 /* OpenAI in Frameworks */,
50+
A79823F56A914FFEFD22770F /* Pods_Seahorse.framework in Frameworks */,
4651
);
4752
runOnlyForDeploymentPostprocessing = 0;
4853
};
4954
/* End PBXFrameworksBuildPhase section */
5055

5156
/* Begin PBXGroup section */
57+
6C1DAC8F9CA2A6DFACB56001 /* Pods */ = {
58+
isa = PBXGroup;
59+
children = (
60+
3DCFB21E74EAE507F2254B97 /* Pods-Seahorse.debug.xcconfig */,
61+
CBC401249550BFF57145509A /* Pods-Seahorse.release.xcconfig */,
62+
);
63+
path = Pods;
64+
sourceTree = "<group>";
65+
};
5266
C404BA172EC6C12F007D7FD4 = {
5367
isa = PBXGroup;
5468
children = (
5569
C404BA222EC6C12F007D7FD4 /* Seahorse */,
5670
C404BA212EC6C12F007D7FD4 /* Products */,
71+
6C1DAC8F9CA2A6DFACB56001 /* Pods */,
72+
DE0D97D3608C4B6D77C70138 /* Frameworks */,
5773
);
5874
sourceTree = "<group>";
5975
};
@@ -65,16 +81,26 @@
6581
name = Products;
6682
sourceTree = "<group>";
6783
};
84+
DE0D97D3608C4B6D77C70138 /* Frameworks */ = {
85+
isa = PBXGroup;
86+
children = (
87+
18E7A1921ECE390AA6BEAE44 /* Pods_Seahorse.framework */,
88+
);
89+
name = Frameworks;
90+
sourceTree = "<group>";
91+
};
6892
/* End PBXGroup section */
6993

7094
/* Begin PBXNativeTarget section */
7195
C404BA1F2EC6C12F007D7FD4 /* Seahorse */ = {
7296
isa = PBXNativeTarget;
7397
buildConfigurationList = C404BA2F2EC6C130007D7FD4 /* Build configuration list for PBXNativeTarget "Seahorse" */;
7498
buildPhases = (
99+
D308CD5C64C0451E7291B63D /* [CP] Check Pods Manifest.lock */,
75100
C404BA1C2EC6C12F007D7FD4 /* Sources */,
76101
C404BA1D2EC6C12F007D7FD4 /* Frameworks */,
77102
C404BA1E2EC6C12F007D7FD4 /* Resources */,
103+
57B1FE8A0A8486C1AAA2AEBC /* [CP] Embed Pods Frameworks */,
78104
);
79105
buildRules = (
80106
);
@@ -140,6 +166,48 @@
140166
};
141167
/* End PBXResourcesBuildPhase section */
142168

169+
/* Begin PBXShellScriptBuildPhase section */
170+
57B1FE8A0A8486C1AAA2AEBC /* [CP] Embed Pods Frameworks */ = {
171+
isa = PBXShellScriptBuildPhase;
172+
buildActionMask = 2147483647;
173+
files = (
174+
);
175+
inputFileListPaths = (
176+
"${PODS_ROOT}/Target Support Files/Pods-Seahorse/Pods-Seahorse-frameworks-${CONFIGURATION}-input-files.xcfilelist",
177+
);
178+
name = "[CP] Embed Pods Frameworks";
179+
outputFileListPaths = (
180+
"${PODS_ROOT}/Target Support Files/Pods-Seahorse/Pods-Seahorse-frameworks-${CONFIGURATION}-output-files.xcfilelist",
181+
);
182+
runOnlyForDeploymentPostprocessing = 0;
183+
shellPath = /bin/sh;
184+
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Seahorse/Pods-Seahorse-frameworks.sh\"\n";
185+
showEnvVarsInLog = 0;
186+
};
187+
D308CD5C64C0451E7291B63D /* [CP] Check Pods Manifest.lock */ = {
188+
isa = PBXShellScriptBuildPhase;
189+
buildActionMask = 2147483647;
190+
files = (
191+
);
192+
inputFileListPaths = (
193+
);
194+
inputPaths = (
195+
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
196+
"${PODS_ROOT}/Manifest.lock",
197+
);
198+
name = "[CP] Check Pods Manifest.lock";
199+
outputFileListPaths = (
200+
);
201+
outputPaths = (
202+
"$(DERIVED_FILE_DIR)/Pods-Seahorse-checkManifestLockResult.txt",
203+
);
204+
runOnlyForDeploymentPostprocessing = 0;
205+
shellPath = /bin/sh;
206+
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
207+
showEnvVarsInLog = 0;
208+
};
209+
/* End PBXShellScriptBuildPhase section */
210+
143211
/* Begin PBXSourcesBuildPhase section */
144212
C404BA1C2EC6C12F007D7FD4 /* Sources */ = {
145213
isa = PBXSourcesBuildPhase;
@@ -271,6 +339,7 @@
271339
};
272340
C404BA302EC6C130007D7FD4 /* Debug */ = {
273341
isa = XCBuildConfiguration;
342+
baseConfigurationReference = 3DCFB21E74EAE507F2254B97 /* Pods-Seahorse.debug.xcconfig */;
274343
buildSettings = {
275344
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
276345
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@@ -301,6 +370,7 @@
301370
};
302371
C404BA312EC6C130007D7FD4 /* Release */ = {
303372
isa = XCBuildConfiguration;
373+
baseConfigurationReference = CBC401249550BFF57145509A /* Pods-Seahorse.release.xcconfig */;
304374
buildSettings = {
305375
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
306376
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;

Seahorse.xcworkspace/contents.xcworkspacedata

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

Seahorse/Info.plist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,9 @@
99
</dict>
1010
<key>NSAppleEventsUsageDescription</key>
1111
<string>需要监听复制操作以自动保存内容到收藏夹</string>
12+
<key>SUEnableAutomaticChecks</key>
13+
<true/>
14+
<key>SUFeedURL</key>
15+
<string>https://example.com/appcast.xml</string>
1216
</dict>
1317
</plist>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// SparkleUpdater.swift
3+
// Seahorse
4+
//
5+
// Created by caishilin on 2025/12/08.
6+
//
7+
8+
import Foundation
9+
import Sparkle
10+
11+
@MainActor
12+
final class SparkleUpdater: ObservableObject {
13+
static let shared = SparkleUpdater()
14+
15+
private let controller: SPUStandardUpdaterController
16+
17+
@Published var automaticallyChecksForUpdates: Bool
18+
@Published var automaticallyDownloadsUpdates: Bool
19+
20+
private init() {
21+
controller = SPUStandardUpdaterController(
22+
startingUpdater: true,
23+
updaterDelegate: nil,
24+
userDriverDelegate: nil
25+
)
26+
27+
let updater = controller.updater
28+
automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
29+
automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates
30+
}
31+
32+
func checkForUpdates() {
33+
controller.updater.checkForUpdates()
34+
}
35+
36+
func setAutomaticallyChecks(_ value: Bool) {
37+
controller.updater.automaticallyChecksForUpdates = value
38+
automaticallyChecksForUpdates = value
39+
}
40+
41+
func setAutomaticallyDownloads(_ value: Bool) {
42+
controller.updater.automaticallyDownloadsUpdates = value
43+
automaticallyDownloadsUpdates = value
44+
}
45+
46+
var currentVersionDescription: String {
47+
let bundle = Bundle.main
48+
let version = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
49+
let build = bundle.infoDictionary?["CFBundleVersion"] as? String ?? "?"
50+
return "v\(version) (\(build))"
51+
}
52+
}
53+

Seahorse/Views/Settings/BasicSettingsView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ struct BasicSettingsView: View {
2525
var body: some View {
2626
ScrollView {
2727
VStack(alignment: .leading, spacing: 24) {
28+
// Updates
29+
UpdateSettingsView()
30+
31+
Divider()
32+
2833
// App Language Setting
2934
VStack(alignment: .leading, spacing: 10) {
3035
Text(L10n.appLanguage)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// UpdateSettingsView.swift
3+
// Seahorse
4+
//
5+
// Created by caishilin on 2025/12/08.
6+
//
7+
8+
import SwiftUI
9+
import Sparkle
10+
11+
struct UpdateSettingsView: View {
12+
@StateObject private var updater = SparkleUpdater.shared
13+
14+
var body: some View {
15+
VStack(alignment: .leading, spacing: 10) {
16+
Text("App Updates")
17+
.font(.system(size: 16, weight: .semibold))
18+
19+
Text("Keep Seahorse up to date with the latest improvements.")
20+
.font(.system(size: 11))
21+
.foregroundStyle(.secondary)
22+
23+
HStack(alignment: .top, spacing: 12) {
24+
VStack(alignment: .leading, spacing: 8) {
25+
Toggle("Automatically check for updates", isOn: Binding(
26+
get: { updater.automaticallyChecksForUpdates },
27+
set: { updater.setAutomaticallyChecks($0) }
28+
))
29+
30+
Toggle("Automatically download updates", isOn: Binding(
31+
get: { updater.automaticallyDownloadsUpdates },
32+
set: { updater.setAutomaticallyDownloads($0) }
33+
))
34+
35+
HStack(spacing: 8) {
36+
Button {
37+
updater.checkForUpdates()
38+
} label: {
39+
HStack(spacing: 6) {
40+
Image(systemName: "arrow.clockwise")
41+
Text("Check for Updates")
42+
}
43+
}
44+
45+
Text("Current version: \(updater.currentVersionDescription)")
46+
.font(.system(size: 11))
47+
.foregroundStyle(.secondary)
48+
}
49+
}
50+
Spacer()
51+
}
52+
}
53+
.padding(.vertical, 8)
54+
}
55+
}
56+
57+
#Preview {
58+
UpdateSettingsView()
59+
}
60+

0 commit comments

Comments
 (0)