From 9752f2f419a7eb442c84c6a82d19cf38ba006243 Mon Sep 17 00:00:00 2001 From: thesadbee Date: Fri, 28 Aug 2026 13:25:56 +0800 Subject: [PATCH 1/4] about: separate Squirrel (upstream) update from TriFecta update + live version fetch --- .../TriFectaSettings/Pages/AboutPage.swift | 137 ++++++++++++++++-- 1 file changed, 127 insertions(+), 10 deletions(-) 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 } } From d17d0356af18e7396383b953b8207bf6ac751cb8 Mon Sep 17 00:00:00 2001 From: thesadbee Date: Fri, 28 Aug 2026 13:31:55 +0800 Subject: [PATCH 2/4] sparkle: point feed to TriFecta, disable auto-check (avoid upstream Squirrel overwrite) --- appcast.xml | 31 +++++++++++++++++++++++++++++++ resources/Info.plist | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 appcast.xml 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 From 2c5c0fb3baa49a496e396222f0ccd2d3cabfaafe Mon Sep 17 00:00:00 2001 From: thesadbee Date: Fri, 28 Aug 2026 15:02:50 +0800 Subject: [PATCH 3/4] Bump version to 1.1.2 --- Squirrel.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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)", From 854c7278a1a6b76041ff2b3a3cbaf9f99244c32e Mon Sep 17 00:00:00 2001 From: thesadbee Date: Fri, 28 Aug 2026 16:55:22 +0800 Subject: [PATCH 4/4] feat(appearance): continuous liquid-glass slider (style/glass_opacity, 0=full glass, 1=opaque) --- .../Pages/AppearancePage.swift | 19 ++++++++++++++++ .../TriFectaSettingsCore/EffectiveModel.swift | 11 ++++++++-- .../SettingsRepository.swift | 4 ++++ sources/SquirrelTheme.swift | 22 +++++++++++++++++++ sources/SquirrelView.swift | 4 ++-- 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/settings/Sources/TriFectaSettings/Pages/AppearancePage.swift b/settings/Sources/TriFectaSettings/Pages/AppearancePage.swift index 6d40d73..8f720ae 100644 --- a/settings/Sources/TriFectaSettings/Pages/AppearancePage.swift +++ b/settings/Sources/TriFectaSettings/Pages/AppearancePage.swift @@ -89,6 +89,25 @@ struct AppearancePage: View { } } + SettingCard { + SettingRow("候选框液态玻璃", + subtitle: "左=完全液态玻璃,右=完全不透明(微微发白保证对比)", + icon: "circle.lefthalf.filled", + divider: false) { + HStack(spacing: 8) { + Image(systemName: "circle.lefthalf.filled") + .font(.system(size: 12)) + .foregroundColor(.secondary) + Slider(value: $state.style.glassOpacity, in: 0...1) + .frame(width: 150) + Image(systemName: "circle.fill") + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + .frame(width: 200) + } + } + SettingCard { SettingRow("三色分组配色", subtitle: "候选多时按 ~ 键分组选字(红/黄/绿)") { diff --git a/settings/Sources/TriFectaSettingsCore/EffectiveModel.swift b/settings/Sources/TriFectaSettingsCore/EffectiveModel.swift index 259bddb..820ef82 100644 --- a/settings/Sources/TriFectaSettingsCore/EffectiveModel.swift +++ b/settings/Sources/TriFectaSettingsCore/EffectiveModel.swift @@ -16,15 +16,20 @@ public struct StyleValues: Equatable { public var fontFace: String public var fontPoint: Double public var candidateFormat: String + /// 候选框液态玻璃程度:0=完全液态玻璃(透明底、露玻璃模糊),1=完全不透明(微微发白)。 + /// 连续无极取值,写入 style/glass_opacity。 + public var glassOpacity: Double public init(colorScheme: String, candidateListLayout: String, textOrientation: String, - fontFace: String, fontPoint: Double, candidateFormat: String) { + fontFace: String, fontPoint: Double, candidateFormat: String, + glassOpacity: Double = 0) { self.colorScheme = colorScheme self.candidateListLayout = candidateListLayout self.textOrientation = textOrientation self.fontFace = fontFace self.fontPoint = fontPoint self.candidateFormat = candidateFormat + self.glassOpacity = glassOpacity } /// 与 SharedSupport 基线一致的缺省 UI 值(readStyle 回退与 AppState 初始快照共用) @@ -174,9 +179,11 @@ public enum RimeModel { let fontFace = style?["font_face"]?.string ?? d.fontFace let fontPoint = style?["font_point"]?.float.map { $0 } ?? style?["font_point"]?.int.map { Double($0) } ?? d.fontPoint let format = style?["candidate_format"]?.string ?? d.candidateFormat + let glass = style?["glass_opacity"]?.float.map { Double($0) } ?? d.glassOpacity return StyleValues(colorScheme: schemeID, candidateListLayout: layout, textOrientation: orientation, fontFace: fontFace, - fontPoint: fontPoint, candidateFormat: format) + fontPoint: fontPoint, candidateFormat: format, + glassOpacity: max(0, min(1, glass))) } public static func readColorSchemes(_ node: Node) -> [(id: String, name: String)] { diff --git a/settings/Sources/TriFectaSettingsCore/SettingsRepository.swift b/settings/Sources/TriFectaSettingsCore/SettingsRepository.swift index 26b4eab..e1a5280 100644 --- a/settings/Sources/TriFectaSettingsCore/SettingsRepository.swift +++ b/settings/Sources/TriFectaSettingsCore/SettingsRepository.swift @@ -74,6 +74,9 @@ public final class SettingsRepository { if cur.candidateFormat != style.candidateFormat { try editor.setScalar(section: "style", keyText: "candidate_format", value: .string(style.candidateFormat)) } + if cur.glassOpacity != style.glassOpacity { + try editor.setScalar(section: "style", keyText: "glass_opacity", value: .number(formatNumber(style.glassOpacity))) + } } if let gc = changes.groupColors { let cur = RimeModel.readGroupColors(effective) @@ -152,6 +155,7 @@ public final class SettingsRepository { try assertWritten("style/font_face", before: before.fontFace, expected: style.fontFace, after: after.fontFace) try assertWritten("style/font_point", before: before.fontPoint, expected: style.fontPoint, after: after.fontPoint) try assertWritten("style/candidate_format", before: before.candidateFormat, expected: style.candidateFormat, after: after.candidateFormat) + try assertWritten("style/glass_opacity", before: before.glassOpacity, expected: style.glassOpacity, after: after.glassOpacity) } if let gc = changes.groupColors { let before = RimeModel.readGroupColors(effective) diff --git a/sources/SquirrelTheme.swift b/sources/SquirrelTheme.swift index d959c9c..2bdf46d 100644 --- a/sources/SquirrelTheme.swift +++ b/sources/SquirrelTheme.swift @@ -63,6 +63,9 @@ final class SquirrelTheme { private(set) var alpha: CGFloat = 1 private(set) var translucency = false + /// 候选框液态玻璃程度(0=完全液态玻璃(透明底、露出玻璃模糊),1=完全不透明)。 + /// 由 style/glass_opacity 覆盖;未设置时按 translucency 推导(true→0,false→1)。 + private(set) var glassOpacity: CGFloat = 0 private(set) var mutualExclusive = false private(set) var linear = false private(set) var vertical = false @@ -206,6 +209,18 @@ final class SquirrelTheme { } } + /// 候选框面板的底色(不透明端使用):把方案背景朝白色拉近一点, + /// 让「玻璃感变弱」时背景微微发白、保持对比度,而不是纯黑/纯透明。 + var panelBackgroundColor: NSColor { + let base = backgroundColor + guard let c = base.usingColorSpace(.sRGB) else { return base } + let t: CGFloat = 0.15 // 朝白色拉近 15% + return NSColor(srgbRed: c.redComponent * (1 - t) + t, + green: c.greenComponent * (1 - t) + t, + blue: c.blueComponent * (1 - t) + t, + alpha: c.alphaComponent) + } + func load(config: SquirrelConfig, dark: Bool) { linear ?= config.getString("style/candidate_list_layout").map { $0 == "linear" } vertical ?= config.getString("style/text_orientation").map { $0 == "vertical" } @@ -304,6 +319,13 @@ final class SquirrelTheme { self.commentFontSize = commentFontSize preeditFonts = decodeFonts(from: preeditFontName ?? fontName) self.preeditFontSize = preeditFontSize + + // 液态玻璃程度:默认按 translucency 推导(true→0 全玻璃,false→1 不透明), + // style/glass_opacity 提供连续无极覆盖。 + glassOpacity = translucency ? 0 : 1 + if let v = config.getDouble("style/glass_opacity") { + glassOpacity = max(0, min(1, CGFloat(v))) + } } } diff --git a/sources/SquirrelView.swift b/sources/SquirrelView.swift index c5521f8..0bf5e9f 100644 --- a/sources/SquirrelView.swift +++ b/sources/SquirrelView.swift @@ -291,8 +291,8 @@ final class SquirrelView: NSView { } } let panelLayer = shapeFromPath(path: backPath) - // 开启 translucency 时背景涂透明,露出底层 NSGlassEffectView 的液态玻璃效果 - panelLayer.fillColor = theme.translucency ? NSColor.clear.cgColor : theme.backgroundColor.cgColor + // 连续无极控制:glassOpacity=0 → 全透明(露出液态玻璃),=1 → 不透明(微微发白的底色) + panelLayer.fillColor = theme.panelBackgroundColor.withAlphaComponent(theme.glassOpacity).cgColor let panelLayerMask = shapeFromPath(path: backgroundPath) panelLayer.mask = panelLayerMask self.layer?.addSublayer(panelLayer)