Files
2026-07-13 12:22:33 +08:00

182 lines
6.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AppKit
import SwiftUI
@MainActor
struct AboutPane: View {
let updater: UpdaterProviding
@State private var iconHover = false
@AppStorage("autoUpdateEnabled") private var autoUpdateEnabled: Bool = true
@AppStorage(UpdateChannel.userDefaultsKey)
private var updateChannelRaw: String = UpdateChannel.defaultChannel.rawValue
@State private var didLoadUpdaterState = false
private var versionString: String {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
return build.map { "\(version) (\($0))" } ?? version
}
private var buildTimestamp: String? {
guard let raw = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String else { return nil }
let parser = ISO8601DateFormatter()
parser.formatOptions = [.withInternetDateTime]
guard let date = parser.date(from: raw) else { return raw }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = .current
return formatter.string(from: date)
}
var body: some View {
Form {
Section {
self.hero
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)
}
if self.updater.isAvailable {
Section {
Toggle(L("check_updates_auto"), isOn: self.$autoUpdateEnabled)
Picker(selection: self.updateChannelBinding) {
ForEach(UpdateChannel.allCases) { channel in
Text(channel.displayName).tag(channel)
}
} label: {
SettingsRowLabel(L("update_channel"), subtitle: self.updateChannel.description)
}
LabeledContent(String(format: L("version_format"), self.versionString)) {
Button(L("check_for_updates")) { self.updater.checkForUpdates(nil) }
}
} header: {
Text(L("section_updates"))
}
} else {
Section {
Text(self.updater.unavailableReason ?? L("updates_unavailable"))
.foregroundStyle(.secondary)
}
}
Section {
AboutLinkRow(
icon: "chevron.left.slash.chevron.right",
title: L("link_github"),
url: "https://github.com/steipete/CodexBar")
AboutLinkRow(icon: "globe", title: L("link_website"), url: "https://steipete.me")
AboutLinkRow(icon: "bird", title: L("link_twitter"), url: "https://twitter.com/steipete")
AboutLinkRow(icon: "envelope", title: L("link_email"), url: "mailto:peter@steipete.me")
} header: {
Text(L("section_links"))
} footer: {
Text(L("copyright"))
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
}
}
.formStyle(.grouped)
.toggleStyle(.switch)
.scrollContentBackground(.hidden)
.onAppear {
guard !self.didLoadUpdaterState else { return }
// Align Sparkle's flag with the persisted preference on first load.
self.updater.automaticallyChecksForUpdates = self.autoUpdateEnabled
self.updater.automaticallyDownloadsUpdates = self.autoUpdateEnabled
self.didLoadUpdaterState = true
}
.onChange(of: self.autoUpdateEnabled) { _, newValue in
self.updater.automaticallyChecksForUpdates = newValue
self.updater.automaticallyDownloadsUpdates = newValue
}
}
private var hero: some View {
VStack(spacing: 10) {
if let image = NSApplication.shared.applicationIconImage {
Button(action: self.openProjectHome) {
Image(nsImage: image)
.resizable()
.frame(width: 92, height: 92)
.cornerRadius(16)
.scaleEffect(self.iconHover ? 1.05 : 1.0)
.shadow(color: self.iconHover ? .accentColor.opacity(0.25) : .clear, radius: 6)
}
.buttonStyle(.plain)
.focusEffectDisabled()
.onHover { hovering in
withAnimation(.spring(response: 0.32, dampingFraction: 0.78)) {
self.iconHover = hovering
}
}
}
VStack(spacing: 2) {
Text("CodexBar")
.font(.title3).bold()
Text(String(format: L("version_format"), self.versionString))
.foregroundStyle(.secondary)
if let buildTimestamp {
Text(String(format: L("built_format"), buildTimestamp))
.font(.footnote)
.foregroundStyle(.secondary)
}
Text(L("about_tagline"))
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 6)
}
private var updateChannel: UpdateChannel {
UpdateChannel(rawValue: self.updateChannelRaw) ?? .stable
}
private var updateChannelBinding: Binding<UpdateChannel> {
Binding(
get: { self.updateChannel },
set: { newValue in
self.updateChannelRaw = newValue.rawValue
self.updater.checkForUpdates(nil)
})
}
private func openProjectHome() {
guard let url = URL(string: "https://github.com/steipete/CodexBar") else { return }
NSWorkspace.shared.open(url)
}
}
@MainActor
struct AboutLinkRow: View {
let icon: String
let title: String
let url: String
@State private var hovering = false
var body: some View {
Button {
if let url = URL(string: self.url) { NSWorkspace.shared.open(url) }
} label: {
HStack(spacing: 8) {
Image(systemName: self.icon)
.frame(width: 18)
.foregroundStyle(.secondary)
Text(self.title)
.foregroundStyle(.primary)
Spacer()
Image(systemName: "arrow.up.right")
.font(.caption)
.foregroundStyle(self.hovering ? Color.accentColor : Color.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.onHover { self.hovering = $0 }
}
}