Files
steipete--codexbar/Sources/CodexBar/PreferencesCodexAccountsSection.swift
T
2026-07-13 12:22:33 +08:00

334 lines
12 KiB
Swift

import Foundation
import SwiftUI
protocol CodexAmbientLoginRunning: Sendable {
func run(timeout: TimeInterval) async -> CodexLoginRunner.Result
}
struct DefaultCodexAmbientLoginRunner: CodexAmbientLoginRunning {
func run(timeout: TimeInterval) async -> CodexLoginRunner.Result {
await CodexLoginRunner.run(timeout: timeout)
}
}
struct CodexAccountsSectionNotice: Equatable {
enum Tone: Equatable {
case secondary
case warning
}
let text: String
let tone: Tone
}
struct CodexAccountsSectionState: Equatable {
let visibleAccounts: [CodexVisibleAccount]
let activeVisibleAccountID: String?
let liveVisibleAccountID: String?
let hasUnreadableManagedAccountStore: Bool
let isAuthenticatingManagedAccount: Bool
let authenticatingManagedAccountID: UUID?
let isRemovingManagedAccount: Bool
let isAuthenticatingLiveAccount: Bool
let isPromotingSystemAccount: Bool
let notice: CodexAccountsSectionNotice?
var showsActivePicker: Bool {
self.visibleAccounts.count > 1
}
var singleVisibleAccount: CodexVisibleAccount? {
self.visibleAccounts.count == 1 ? self.visibleAccounts.first : nil
}
var systemVisibleAccount: CodexVisibleAccount? {
guard let liveVisibleAccountID else { return nil }
return self.visibleAccounts.first { $0.id == liveVisibleAccountID }
}
var showsSystemPicker: Bool {
self.visibleAccounts.count > 1 || (self.liveVisibleAccountID == nil && !self.visibleAccounts.isEmpty)
}
var systemDisplayName: String {
self.systemVisibleAccount?.displayName ?? L("No system account")
}
var canAddAccount: Bool {
!self.hasUnreadableManagedAccountStore &&
!self.isAuthenticatingManagedAccount &&
!self.isRemovingManagedAccount &&
!self.isAuthenticatingLiveAccount &&
!self.isPromotingSystemAccount
}
var addAccountTitle: String {
if self.isAuthenticatingManagedAccount, self.authenticatingManagedAccountID == nil {
return L("Adding Account…")
}
return L("Add Account")
}
func showsLiveBadge(for account: CodexVisibleAccount) -> Bool {
account.isLive
}
var isSystemSelectionDisabled: Bool {
self.hasUnreadableManagedAccountStore ||
self.isAuthenticatingManagedAccount ||
self.isRemovingManagedAccount ||
self.isAuthenticatingLiveAccount ||
self.isPromotingSystemAccount
}
func canPromoteToSystem(_ account: CodexVisibleAccount) -> Bool {
guard self.isSystemSelectionDisabled == false else { return false }
guard account.id != self.liveVisibleAccountID else { return false }
return account.storedAccountID != nil
}
func canReauthenticate(_ account: CodexVisibleAccount) -> Bool {
guard account.canReauthenticate else { return false }
guard self.isAuthenticatingManagedAccount == false else { return false }
guard self.isRemovingManagedAccount == false else { return false }
guard self.isAuthenticatingLiveAccount == false else { return false }
guard self.isPromotingSystemAccount == false else { return false }
if account.storedAccountID != nil {
return self.hasUnreadableManagedAccountStore == false
}
return true
}
func canRemove(_ account: CodexVisibleAccount) -> Bool {
guard account.canRemove else { return false }
guard self.isAuthenticatingManagedAccount == false else { return false }
guard self.isRemovingManagedAccount == false else { return false }
guard self.isAuthenticatingLiveAccount == false else { return false }
guard self.isPromotingSystemAccount == false else { return false }
return self.hasUnreadableManagedAccountStore == false
}
func reauthenticateTitle(for account: CodexVisibleAccount) -> String {
if let accountID = account.storedAccountID,
self.isAuthenticatingManagedAccount,
self.authenticatingManagedAccountID == accountID
{
return L("Re-authenticating…")
}
if account.storedAccountID == nil, self.isAuthenticatingLiveAccount {
return L("Re-authenticating…")
}
return L("Re-auth")
}
}
@MainActor
struct CodexAccountsSectionView: View {
let state: CodexAccountsSectionState
let setActiveVisibleAccount: (String) -> Void
let reauthenticateAccount: (CodexVisibleAccount) -> Void
let removeAccount: (CodexVisibleAccount) -> Void
let requestSystemVisibleAccount: (String) -> Void
let addAccount: () -> Void
var body: some View {
Section {
if let selection = self.activeSelectionBinding {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(L("Active"))
.font(.subheadline.weight(.semibold))
.frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading)
Picker("", selection: selection) {
ForEach(self.state.visibleAccounts) { account in
Text(account.displayName).tag(account.id)
}
}
.labelsHidden()
.pickerStyle(.menu)
.controlSize(.small)
Spacer(minLength: 0)
}
Text(L("Choose which Codex account CodexBar should follow."))
.font(.footnote)
.foregroundStyle(.secondary)
self.systemRow(selection: self.systemSelectionBinding)
}
.disabled(
self.state.isAuthenticatingManagedAccount ||
self.state.isRemovingManagedAccount ||
self.state.isAuthenticatingLiveAccount ||
self.state.isPromotingSystemAccount)
} else if let account = self.state.singleVisibleAccount {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(L("Account"))
.font(.subheadline.weight(.semibold))
.frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading)
Text(account.displayName)
.font(.subheadline)
Spacer(minLength: 0)
}
self.systemRow(selection: nil)
}
}
if self.state.visibleAccounts.isEmpty {
Text(L("No Codex accounts detected yet."))
.font(.footnote)
.foregroundStyle(.secondary)
} else {
VStack(alignment: .leading, spacing: 10) {
ForEach(self.state.visibleAccounts) { account in
CodexAccountsSectionRowView(
account: account,
showsSystemBadge: self.state.showsLiveBadge(for: account),
reauthenticateTitle: self.state.reauthenticateTitle(for: account),
canReauthenticate: self.state.canReauthenticate(account),
canRemove: self.state.canRemove(account),
onReauthenticate: { self.reauthenticateAccount(account) },
onRemove: { self.removeAccount(account) })
}
}
}
if let notice = self.state.notice {
Text(notice.text)
.font(.footnote)
.foregroundStyle(notice.tone == .warning ? .red : .secondary)
.fixedSize(horizontal: false, vertical: true)
}
Button(self.state.addAccountTitle) {
self.addAccount()
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(self.state.canAddAccount == false)
} header: {
Text(L("Accounts"))
}
}
private var activeSelectionBinding: Binding<String>? {
guard self.state.showsActivePicker else { return nil }
let fallbackID = self.state.activeVisibleAccountID ?? self.state.visibleAccounts.first?.id
guard let fallbackID else { return nil }
return Binding(
get: { self.state.activeVisibleAccountID ?? fallbackID },
set: { self.setActiveVisibleAccount($0) })
}
private var systemSelectionBinding: Binding<String>? {
guard self.state.showsSystemPicker else { return nil }
guard let liveVisibleAccountID = self.state.liveVisibleAccountID else { return nil }
return Binding(
get: { self.state.liveVisibleAccountID ?? liveVisibleAccountID },
set: { self.requestSystemVisibleAccount($0) })
}
@ViewBuilder
private func systemRow(selection: Binding<String>?) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(L("System"))
.font(.subheadline.weight(.semibold))
.frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading)
if let selection {
Picker("", selection: selection) {
ForEach(self.state.visibleAccounts) { account in
Text(account.displayName)
.tag(account.id)
}
}
.labelsHidden()
.pickerStyle(.menu)
.controlSize(.small)
.disabled(self.state.isSystemSelectionDisabled)
} else if self.state.showsSystemPicker {
Menu {
ForEach(self.state.visibleAccounts) { account in
Button(account.displayName) {
self.requestSystemVisibleAccount(account.id)
}
.disabled(self.state.canPromoteToSystem(account) == false)
}
} label: {
Text(self.state.systemDisplayName)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.disabled(self.state.isSystemSelectionDisabled)
} else {
Text(self.state.systemDisplayName)
.font(.subheadline)
.foregroundStyle(self.state.systemVisibleAccount == nil ? .secondary : .primary)
}
Spacer(minLength: 0)
}
Text(L("The default Codex account on this Mac."))
.font(.footnote)
.foregroundStyle(.secondary)
}
}
private struct CodexAccountsSectionRowView: View {
let account: CodexVisibleAccount
let showsSystemBadge: Bool
let reauthenticateTitle: String
let canReauthenticate: Bool
let canRemove: Bool
let onReauthenticate: () -> Void
let onRemove: () -> Void
var body: some View {
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(self.account.displayName)
.font(.subheadline.weight(.semibold))
if self.showsSystemBadge {
Text(L("(System)"))
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
}
if let health = self.account.authenticationHealthLabel {
Text(health)
.font(.caption)
.foregroundStyle(.orange)
}
}
Spacer(minLength: 8)
if self.account.canReauthenticate {
Button(self.reauthenticateTitle) {
self.onReauthenticate()
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(self.canReauthenticate == false)
}
if self.account.canRemove {
Button(L("Remove")) {
self.onRemove()
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(self.canRemove == false)
}
}
}
}