334 lines
12 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
}
|