467 lines · 18.7 KB · Swift
RawDownload
import SwiftUI struct ProviderSettingsSection<Content: View>: View {    let title: String    let spacing: CGFloat    let verticalPadding: CGFloat    let horizontalPadding: CGFloat    @ViewBuilder let content: () -> Content     init(        title: String,        spacing: CGFloat = 12,        verticalPadding: CGFloat = 10,        horizontalPadding: CGFloat = 4,        @ViewBuilder content: @escaping () -> Content)    {        self.title = title        self.spacing = spacing        self.verticalPadding = verticalPadding        self.horizontalPadding = horizontalPadding        self.content = content    }     var body: some View {        VStack(alignment: .leading, spacing: self.spacing) {            Text(L(self.title))                .font(.headline)            self.content()        }        .frame(maxWidth: .infinity, alignment: .leading)        .padding(.vertical, self.verticalPadding)        .padding(.horizontal, self.horizontalPadding)    }} @MainActorstruct ProviderSettingsToggleRowView: View {    let toggle: ProviderSettingsToggleDescriptor     var body: some View {        let isEnabled = self.toggle.isEnabled?() ?? true        VStack(alignment: .leading, spacing: 8) {            HStack(alignment: .firstTextBaseline, spacing: 12) {                VStack(alignment: .leading, spacing: 4) {                    Text(L(self.toggle.title))                        .font(.subheadline.weight(.semibold))                        .foregroundStyle(isEnabled ? .primary : .tertiary)                    Text(L(self.toggle.subtitle))                        .font(.footnote)                        .foregroundStyle(isEnabled ? .secondary : .tertiary)                        .fixedSize(horizontal: false, vertical: true)                }                Spacer(minLength: 8)                Toggle("", isOn: self.toggle.binding)                    .labelsHidden()                    .toggleStyle(.switch)            }             if self.toggle.binding.wrappedValue {                if let status = self.toggle.statusText?(), !status.isEmpty {                    Text(status)                        .font(.footnote)                        .foregroundStyle(.secondary)                        .lineLimit(4)                        .fixedSize(horizontal: false, vertical: true)                }                 let actions = self.toggle.actions.filter { $0.isVisible?() ?? true }                if !actions.isEmpty {                    HStack(spacing: 10) {                        ForEach(actions) { action in                            Button(L(action.title)) {                                Task { @MainActor in                                    await action.perform()                                }                            }                            .applyProviderSettingsButtonStyle(action.style)                            .controlSize(.small)                        }                    }                }            }        }        .disabled(!isEnabled)        .onChange(of: self.toggle.binding.wrappedValue) { _, enabled in            guard let onChange = self.toggle.onChange else { return }            Task { @MainActor in                await onChange(enabled)            }        }        .task(id: self.toggle.binding.wrappedValue) {            guard self.toggle.binding.wrappedValue else { return }            guard let onAppear = self.toggle.onAppearWhenEnabled else { return }            await onAppear()        }    }} @MainActorstruct ProviderSettingsPickerRowView: View {    let picker: ProviderSettingsPickerDescriptor     var body: some View {        let isEnabled = self.picker.isEnabled?() ?? true        VStack(alignment: .leading, spacing: 6) {            HStack(alignment: .firstTextBaseline, spacing: 10) {                Text(L(self.picker.title))                    .font(.subheadline.weight(.semibold))                    .frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading)                 Picker("", selection: self.picker.binding) {                    ForEach(self.picker.options) { option in                        Text(L(option.title)).tag(option.id)                    }                }                .labelsHidden()                .pickerStyle(.menu)                .controlSize(.small)                 if let trailingText = self.picker.trailingText?(), !trailingText.isEmpty {                    Text(trailingText)                        .font(.footnote)                        .foregroundStyle(.secondary)                        .lineLimit(1)                        .truncationMode(.tail)                        .padding(.leading, 4)                }                 Spacer(minLength: 0)            }             let subtitle = self.picker.dynamicSubtitle?() ?? self.picker.subtitle            if !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {                Text(L(subtitle))                    .font(.footnote)                    .foregroundStyle(.secondary)                    .fixedSize(horizontal: false, vertical: true)            }        }        .disabled(!isEnabled)        .onChange(of: self.picker.binding.wrappedValue) { _, selection in            guard let onChange = self.picker.onChange else { return }            Task { @MainActor in                await onChange(selection)            }        }    }} @MainActorstruct ProviderSettingsFieldRowView: View {    let field: ProviderSettingsFieldDescriptor     var body: some View {        VStack(alignment: .leading, spacing: 8) {            let trimmedTitle = self.field.title.trimmingCharacters(in: .whitespacesAndNewlines)            let trimmedSubtitle = self.field.subtitle.trimmingCharacters(in: .whitespacesAndNewlines)            let hasHeader = !trimmedTitle.isEmpty || !trimmedSubtitle.isEmpty             if hasHeader {                VStack(alignment: .leading, spacing: 4) {                    if !trimmedTitle.isEmpty {                        Text(L(trimmedTitle))                            .font(.subheadline.weight(.semibold))                    }                    if !trimmedSubtitle.isEmpty {                        Text(L(trimmedSubtitle))                            .font(.footnote)                            .foregroundStyle(.secondary)                            .fixedSize(horizontal: false, vertical: true)                    }                }            }             switch self.field.kind {            case .plain:                TextField(L(self.field.placeholder ?? ""), text: self.field.binding)                    .textFieldStyle(.roundedBorder)                    .font(.footnote)                    .onTapGesture { self.field.onActivate?() }            case .secure:                SecureField(L(self.field.placeholder ?? ""), text: self.field.binding)                    .textFieldStyle(.roundedBorder)                    .font(.footnote)                    .onTapGesture { self.field.onActivate?() }            }             let actions = self.field.actions.filter { $0.isVisible?() ?? true }            if !actions.isEmpty {                HStack(spacing: 10) {                    ForEach(actions) { action in                        Button(L(action.title)) {                            Task { @MainActor in                                await action.perform()                            }                        }                        .applyProviderSettingsButtonStyle(action.style)                        .controlSize(.small)                    }                }            }             if let footer = self.field.footerText, !footer.isEmpty {                Text(L(footer))                    .font(.footnote)                    .foregroundStyle(.secondary)                    .fixedSize(horizontal: false, vertical: true)            }        }    }} @MainActorstruct ProviderSettingsActionsRowView: View {    let descriptor: ProviderSettingsActionsDescriptor     var body: some View {        VStack(alignment: .leading, spacing: 8) {            Text(L(self.descriptor.title))                .font(.subheadline.weight(.semibold))             if !self.descriptor.subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {                Text(L(self.descriptor.subtitle))                    .font(.footnote)                    .foregroundStyle(.secondary)                    .fixedSize(horizontal: false, vertical: true)            }             let actions = self.descriptor.actions.filter { $0.isVisible?() ?? true }            if !actions.isEmpty {                HStack(spacing: 10) {                    ForEach(actions) { action in                        Button(L(action.title)) {                            Task { @MainActor in                                await action.perform()                            }                        }                        .applyProviderSettingsButtonStyle(action.style)                        .controlSize(.small)                    }                }            }        }    }} @MainActorstruct ProviderSettingsTokenAccountsRowView: View {    let descriptor: ProviderSettingsTokenAccountsDescriptor    @State private var newLabel: String = ""    @State private var newToken: String = ""    @State private var newOrgID: String = ""     var body: some View {        VStack(alignment: .leading, spacing: 10) {            HStack(alignment: .center, spacing: 12) {                Text(L(self.descriptor.title))                    .font(.subheadline.weight(.semibold))                Spacer(minLength: 8)                if let title = self.descriptor.primaryAddActionTitle,                   let action = self.descriptor.primaryAddAction                {                    Button(L(title)) {                        Task { @MainActor in                            await action()                        }                    }                    .buttonStyle(.bordered)                    .controlSize(.small)                }            }             if !self.descriptor.subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {                Text(L(self.descriptor.subtitle))                    .font(.footnote)                    .foregroundStyle(.secondary)                    .fixedSize(horizontal: false, vertical: true)            }             let accounts = self.descriptor.accounts()            if accounts.isEmpty {                Text(L("No token accounts yet."))                    .font(.footnote)                    .foregroundStyle(.secondary)            } else {                VStack(alignment: .leading, spacing: 8) {                    ForEach(Array(accounts.enumerated()), id: \.element.id) { index, account in                        HStack(alignment: .center, spacing: 10) {                            Button {                                self.descriptor.setActiveIndex(index)                            } label: {                                HStack(alignment: .center, spacing: 8) {                                    Image(systemName: self.isActive(index: index, accountCount: accounts.count) ?                                        "checkmark.circle.fill" : "circle")                                        .font(.system(size: 13, weight: .medium))                                        .foregroundStyle(self.isActive(index: index, accountCount: accounts.count) ?                                            Color.accentColor : Color.secondary)                                    Text(account.displayName)                                        .font(                                            .footnote.weight(                                                self.isActive(index: index, accountCount: accounts.count) ?                                                    .semibold : .regular))                                        .foregroundStyle(.primary)                                    Spacer(minLength: 0)                                }                                .contentShape(Rectangle())                            }                            .buttonStyle(.plain)                             Button(L("Remove")) {                                self.descriptor.removeAccount(account.id)                            }                            .buttonStyle(.bordered)                            .controlSize(.small)                        }                        if index < accounts.count - 1 {                            Divider()                        }                    }                }            }             if self.descriptor.primaryAddAction == nil {                VStack(alignment: .leading, spacing: 6) {                    HStack(spacing: 8) {                        TextField(L("Label"), text: self.$newLabel)                            .textFieldStyle(.roundedBorder)                            .font(.footnote)                        SecureField(L(self.descriptor.placeholder), text: self.$newToken)                            .textFieldStyle(.roundedBorder)                            .font(.footnote)                        Button(L("Add")) {                            let label = self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines)                            let token = self.newToken.trimmingCharacters(in: .whitespacesAndNewlines)                            guard !label.isEmpty, !token.isEmpty else { return }                            let orgID = self.descriptor.showsOrganizationField                                ? self.newOrgID.trimmingCharacters(in: .whitespacesAndNewlines)                                : ""                            self.descriptor.addAccount(label, token, orgID.isEmpty ? nil : orgID)                            self.newLabel = ""                            self.newToken = ""                            self.newOrgID = ""                        }                        .buttonStyle(.bordered)                        .controlSize(.small)                        .disabled(self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||                            self.newToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)                    }                    if self.descriptor.showsOrganizationField {                        TextField(L("Org ID (optional)"), text: self.$newOrgID)                            .textFieldStyle(.roundedBorder)                            .font(.footnote)                            .help(                                L("Optional organization ID for accounts linked to multiple Anthropic organizations."))                    }                }            }             HStack(spacing: 10) {                Button(L("Open token file")) {                    self.descriptor.openConfigFile()                }                .buttonStyle(.link)                .controlSize(.small)                Button(L("Reload")) {                    self.descriptor.reloadFromDisk()                }                .buttonStyle(.link)                .controlSize(.small)            }        }    }     private func isActive(index: Int, accountCount: Int) -> Bool {        guard accountCount > 0 else { return false }        let selectedIndex = min(self.descriptor.activeIndex(), max(0, accountCount - 1))        return selectedIndex == index    }} extension View {    @ViewBuilder    fileprivate func applyProviderSettingsButtonStyle(_ style: ProviderSettingsActionDescriptor.Style) -> some View {        switch style {        case .bordered:            self.buttonStyle(.bordered)        case .link:            self.buttonStyle(.link)        }    }} @MainActorstruct ProviderSettingsOrganizationsRowView: View {    let descriptor: ProviderSettingsOrganizationsDescriptor    @State private var errorMessage: String?    @State private var isRefreshing = false     var body: some View {        VStack(alignment: .leading, spacing: 10) {            HStack(alignment: .center, spacing: 12) {                Text(L(self.descriptor.title))                    .font(.subheadline.weight(.semibold))                Spacer(minLength: 8)            }             if let subtitle = self.descriptor.subtitle,               !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty            {                Text(L(subtitle))                    .font(.footnote)                    .foregroundStyle(.secondary)                    .fixedSize(horizontal: false, vertical: true)            }             let entries = self.descriptor.entries()            if entries.allSatisfy(\.isLocked) {                Text(L("No organizations loaded. Click Refresh after setting your API key."))                    .font(.footnote)                    .foregroundStyle(.secondary)            } else {                VStack(alignment: .leading, spacing: 6) {                    ForEach(entries) { entry in                        Toggle(isOn: Binding(                            get: { entry.isEnabled },                            set: { newValue in                                self.descriptor.onToggle(entry.id, newValue)                            })) {                                VStack(alignment: .leading, spacing: 1) {                                    Text(entry.localizesTitle ? L(entry.title) : entry.title)                                        .font(.footnote)                                    if let subtitle = entry.subtitle,                                       !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty                                    {                                        Text(entry.localizesSubtitle ? L(subtitle) : subtitle)                                            .font(.caption)                                            .foregroundStyle(.secondary)                                    }                                }                            }                            .toggleStyle(.checkbox)                                .disabled(entry.isLocked)                    }                }            }             HStack(spacing: 10) {                Button(L("Refresh organizations")) {                    Task { @MainActor in                        self.isRefreshing = true                        let result = await self.descriptor.onRefresh()                        self.isRefreshing = false                        self.errorMessage = result.success ? nil : result.errorMessage                    }                }                .buttonStyle(.bordered)                .controlSize(.small)                .disabled(!self.descriptor.canRefresh() || self.isRefreshing)                if let errorMessage = self.errorMessage, !errorMessage.isEmpty {                    Text(errorMessage)                        .font(.caption)                        .foregroundStyle(.red)                }            }        }    }}