diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj
index c7d5b59..923971f 100644
--- a/Squirrel.xcodeproj/project.pbxproj
+++ b/Squirrel.xcodeproj/project.pbxproj
@@ -666,7 +666,7 @@
CODE_SIGN_ENTITLEMENTS = resources/Squirrel.entitlements;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 1.1.1;
+ CURRENT_PROJECT_VERSION = 1.1.2;
DEAD_CODE_STRIPPING = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -722,7 +722,7 @@
CLANG_ENABLE_OBJC_ARC = YES;
CODE_SIGN_ENTITLEMENTS = resources/Squirrel.entitlements;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 1.1.1;
+ CURRENT_PROJECT_VERSION = 1.1.2;
DEAD_CODE_STRIPPING = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
diff --git a/appcast.xml b/appcast.xml
new file mode 100644
index 0000000..e8a6e59
--- /dev/null
+++ b/appcast.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ TriFecta 更新
+ https://github.com/thesadbee/TriFecta/releases
+ TriFecta(基于 Rime/Squirrel 的 macOS 中文输入法)更新源
+ -
+ TriFecta v1.1.1
+ 1.1.1
+ 1.1.1
+ https://github.com/thesadbee/TriFecta/releases/tag/v1.1.1
+ Fri, 28 Aug 2026 03:13:18 +0000
+
+
+
+
+
diff --git a/resources/Info.plist b/resources/Info.plist
index df13876..e08090d 100644
--- a/resources/Info.plist
+++ b/resources/Info.plist
@@ -102,9 +102,9 @@
NSPrincipalClass
NSApplication
SUEnableAutomaticChecks
-
+
SUFeedURL
- https://rime.github.io/release/squirrel/appcast.xml
+ https://raw.githubusercontent.com/thesadbee/TriFecta/main/appcast.xml
SUPublicEDKey
ukvWq2dKOWn3B9AsdsQIwOptiDdDKdUjAVNgFxSvB2o=
TICapsLockLanguageSwitchCapable
diff --git a/settings/Sources/TriFectaSettings/Pages/AboutPage.swift b/settings/Sources/TriFectaSettings/Pages/AboutPage.swift
index 617306c..61be4c4 100644
--- a/settings/Sources/TriFectaSettings/Pages/AboutPage.swift
+++ b/settings/Sources/TriFectaSettings/Pages/AboutPage.swift
@@ -1,5 +1,8 @@
//
-// 关于:版本信息、GitHub 链接、检查更新。
+// 关于:版本信息、GitHub 链接、更新检查。
+// 更新分为两块,避免混淆:
+// - Squirrel 更新:来自上游 rime/Squirrel 仓库(引擎本体),与 TriFecta 版本无关;
+// - TriFecta 更新:来自 thesadbee/TriFecta 仓库(项目自身的发布版本)。
//
import SwiftUI
import AppKit
@@ -7,22 +10,28 @@ import AppKit
struct AboutPage: View {
@EnvironmentObject private var state: AppState
@Environment(\.appTheme) private var theme
+ @StateObject private var updater = AboutUpdateModel()
private enum Links {
static let repo = "https://github.com/thesadbee/TriFecta"
static let releases = "https://github.com/thesadbee/TriFecta/releases"
+ static let squirrelReleases = "https://github.com/rime/squirrel/releases"
+ static let squirrelFeed = "https://rime.github.io/release/squirrel/appcast.xml"
}
private var imeVersion: String {
let plist = NSDictionary(contentsOf: state.repo.paths.imeInfoPlist) as? [String: Any]
- let short = plist?["CFBundleShortVersionString"] as? String ?? "—"
+ let short = plist?["CFBundleShortVersionString"] as? String
let build = plist?["CFBundleVersion"] as? String ?? "—"
- return "\(short) (\(build))"
+ if let s = short, !s.isEmpty { return "\(s) (\(build))" }
+ return build
}
+ /// TriFecta 当前版本(对应发布 tag 的 v 前缀形式)
+ private var trifectaCurrent: String { "v" + imeVersion }
+
private var settingsVersion: String {
- let short = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev"
- return short
+ Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev"
}
private func open(_ url: String) {
@@ -31,6 +40,21 @@ struct AboutPage: View {
}
}
+ // 上游 Squirrel 版本行:当前 → 最新
+ private var squirrelLine: String {
+ let latest = updater.squirrelLatest ?? "获取中…"
+ return "当前 \(imeVersion) → 上游最新 \(latest)"
+ }
+
+ // TriFecta 版本行:当前 → 最新(含“有新版本”提示)
+ private var trifectaLine: String {
+ if let lt = updater.trifectaLatest {
+ let hasNew = AboutUpdateModel.isNewer(current: trifectaCurrent, latest: lt)
+ return "当前 \(trifectaCurrent) → 最新 \(lt)\(hasNew ? "(有新版本)" : "")"
+ }
+ return "当前 \(trifectaCurrent) → 最新 获取中…"
+ }
+
var body: some View {
PageScroll(title: "关于", footer: "TriFecta:基于 Rime/Squirrel 的 macOS 中文输入法(GPL-3.0)") {
SettingCard {
@@ -47,17 +71,110 @@ struct AboutPage: View {
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
- Divider()
+ }
+
+ SettingCard {
+ // —— 上游 Squirrel 引擎更新(与 TriFecta 版本无关)——
+ SettingRow("Squirrel 更新(上游)",
+ subtitle: "上游 rime / 鼠鬚管 引擎版本,非 TriFecta 版本更新",
+ icon: "arrow.triangle.2.circlepath",
+ divider: false) {
+ VStack(alignment: .trailing, spacing: 4) {
+ Text(squirrelLine)
+ .font(.system(size: 11))
+ .foregroundColor(.secondary)
+ Button("打开上游 Releases") { open(Links.squirrelReleases) }
+ }
+ }
+ }
+
+ SettingCard {
+ // —— TriFecta 项目自身更新 ——
+ SettingRow("TriFecta 更新",
+ subtitle: "来自 thesadbee/TriFecta 仓库的项目版本",
+ icon: "arrow.triangle.2.circlepath",
+ divider: false) {
+ VStack(alignment: .trailing, spacing: 4) {
+ Text(trifectaLine)
+ .font(.system(size: 11))
+ .foregroundColor(.secondary)
+ Button(updater.loading ? "检查中…" : "检查 TriFecta 更新") { updater.refresh() }
+ }
+ }
+ }
+
+ SettingCard {
SettingRow("GitHub 仓库", icon: "arrow.up.right.square") {
Button("打开") { open(Links.repo) }
}
- SettingRow("Rime Wiki(上游文档)", icon: "book") {
+ SettingRow("Rime Wiki(上游文档)", icon: "book", divider: false) {
Button("打开") { open("https://github.com/rime/home/wiki") }
}
- SettingRow("检查更新", icon: "arrow.triangle.2.circlepath", divider: false) {
- Button("查看 Releases") { open(Links.releases) }
- }
}
}
+ .task { updater.refresh() }
+ }
+}
+
+/// 拉取两个仓库的最新 release 版本号(tag_name)。
+@MainActor
+final class AboutUpdateModel: ObservableObject {
+ @Published var squirrelLatest: String?
+ @Published var trifectaLatest: String?
+ @Published var loading = false
+ @Published var error: String?
+
+ func refresh() {
+ loading = true
+ let group = DispatchGroup()
+
+ var s: String?, t: String?, e: String?
+
+ group.enter()
+ fetchLatest("https://api.github.com/repos/rime/squirrel/releases/latest") { tag, err in
+ s = tag; if err != nil { e = e ?? err }; group.leave()
+ }
+
+ group.enter()
+ fetchLatest("https://api.github.com/repos/thesadbee/TriFecta/releases/latest") { tag, err in
+ t = tag; if err != nil { e = e ?? err }; group.leave()
+ }
+
+ group.notify(queue: .main) {
+ self.squirrelLatest = s
+ self.trifectaLatest = t
+ self.error = e
+ self.loading = false
+ }
+ }
+
+ private func fetchLatest(_ urlString: String, _ completion: @escaping (String?, String?) -> Void) {
+ guard let url = URL(string: urlString) else { completion(nil, "invalid url"); return }
+ var req = URLRequest(url: url)
+ req.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
+ URLSession.shared.dataTask(with: req) { data, resp, err in
+ if let err = err { completion(nil, err.localizedDescription); return }
+ guard let data = data,
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ let tag = obj["tag_name"] as? String else {
+ completion(nil, "no tag_name"); return
+ }
+ completion(tag, nil)
+ }.resume()
+ }
+
+ /// 轻量版本比较("v1.1.1" 式):latest 是否比 current 更新
+ static func isNewer(current: String, latest: String) -> Bool {
+ let c = current.lowercased().replacingOccurrences(of: "v", with: "")
+ let l = latest.lowercased().replacingOccurrences(of: "v", with: "")
+ let cp = c.split(separator: ".").compactMap { Int($0) }
+ let lp = l.split(separator: ".").compactMap { Int($0) }
+ let n = max(cp.count, lp.count)
+ for i in 0.. a }
+ }
+ return false
}
}