204 lines
8.0 KiB
Swift
204 lines
8.0 KiB
Swift
import CodexBarCore
|
|
import SwiftUI
|
|
|
|
@MainActor
|
|
struct MenuPane: View {
|
|
@Bindable var settings: SettingsStore
|
|
@Bindable var store: UsageStore
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
SettingsMenuPicker(
|
|
selection: self.$settings.usageBarsFillOption,
|
|
options: MenuSettingsMenuOptions.usageBarsFill,
|
|
label: { Text(L("usage_bars_fill_title")) },
|
|
optionLabel: { option in
|
|
Text(option.label)
|
|
})
|
|
|
|
Toggle(isOn: self.$settings.quotaWarningMarkersVisible) {
|
|
SettingsRowLabel(
|
|
L("show_quota_warning_markers_title"),
|
|
subtitle: L("show_quota_warning_markers_subtitle"))
|
|
}
|
|
|
|
SettingsMenuPicker(
|
|
selection: self.$settings.weeklyProgressWorkDays,
|
|
options: MenuSettingsMenuOptions.weeklyProgressWorkDays,
|
|
label: {
|
|
Text(L("weekly_progress_work_days_title"))
|
|
},
|
|
optionLabel: { workDays in
|
|
Text(MenuSettingsMenuOptions.weeklyProgressWorkDaysLabel(workDays))
|
|
})
|
|
|
|
SettingsMenuPicker(
|
|
selection: self.$settings.resetTimesOption,
|
|
options: MenuSettingsMenuOptions.resetTimes,
|
|
label: { Text(L("reset_times_title")) },
|
|
optionLabel: { option in
|
|
Text(option.label)
|
|
})
|
|
} header: {
|
|
Text(L("section_usage"))
|
|
}
|
|
|
|
Section {
|
|
Toggle(L("show_provider_changelog_links_title"), isOn: self.$settings.providerChangelogLinksEnabled)
|
|
|
|
Toggle(isOn: self.$settings.showOptionalCreditsAndExtraUsage) {
|
|
SettingsRowLabel(
|
|
L("show_credits_extra_usage_title"),
|
|
subtitle: L("show_credits_extra_usage_subtitle"))
|
|
}
|
|
|
|
SettingsMenuPicker(
|
|
selection: self.$settings.multiAccountMenuLayout,
|
|
options: MenuSettingsMenuOptions.multiAccountLayouts,
|
|
label: {
|
|
Text(L("multi_account_layout_title"))
|
|
},
|
|
optionLabel: { layout in
|
|
Text(layout.label)
|
|
})
|
|
} header: {
|
|
Text(L("section_content"))
|
|
}
|
|
|
|
CostSummarySettingsSection(settings: self.settings, store: self.store)
|
|
|
|
Section {
|
|
Toggle(isOn: self.$settings.agentSessionsEnabled) {
|
|
SettingsRowLabel(
|
|
L("agent_sessions_title"),
|
|
subtitle: L("agent_sessions_subtitle"))
|
|
}
|
|
|
|
TextField(L("agent_sessions_hosts_title"), text: self.$settings.agentSessionsManualHosts)
|
|
.disabled(!self.settings.agentSessionsEnabled)
|
|
} header: {
|
|
Text(L("section_agent_sessions"))
|
|
} footer: {
|
|
SettingsSectionFooter(L("agent_sessions_footer"))
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
.toggleStyle(.switch)
|
|
.scrollContentBackground(.hidden)
|
|
.background(FocusResigningBackground())
|
|
}
|
|
}
|
|
|
|
/// Cost summary settings grouped-form section, including per-provider fetch status in the footer.
|
|
@MainActor
|
|
struct CostSummarySettingsSection: View {
|
|
@Bindable var settings: SettingsStore
|
|
@Bindable var store: UsageStore
|
|
|
|
var body: some View {
|
|
Section {
|
|
SettingsMenuPicker(
|
|
selection: self.$settings.costSummaryOption,
|
|
options: MenuSettingsMenuOptions.costSummaries,
|
|
label: {
|
|
SettingsRowLabel(L("cost_summary_title"), subtitle: L("show_cost_summary_subtitle"))
|
|
},
|
|
optionLabel: { option in
|
|
Text(option.label)
|
|
})
|
|
|
|
if self.settings.costUsageEnabled {
|
|
CostHistoryDaysEditor(settings: self.settings)
|
|
|
|
Toggle(isOn: self.$settings.costComparisonPeriodsEnabled) {
|
|
SettingsRowLabel(
|
|
L("cost_comparison_periods_title"),
|
|
subtitle: L("cost_comparison_periods_subtitle"))
|
|
}
|
|
}
|
|
} header: {
|
|
Text(L("section_cost_summary"))
|
|
} footer: {
|
|
if self.settings.costUsageEnabled {
|
|
SettingsSectionFooter {
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text(L("cost_auto_refresh_info"))
|
|
self.costStatusLine(provider: .claude)
|
|
self.costStatusLine(provider: .codex)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func costStatusLine(provider: UsageProvider) -> Text {
|
|
let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
|
|
|
|
guard provider == .claude || provider == .codex else {
|
|
return Text(String(format: L("cost_status_unsupported"), name))
|
|
}
|
|
|
|
if self.store.isTokenRefreshInFlight(for: provider) {
|
|
let elapsed: String = {
|
|
guard let startedAt = self.store.tokenLastAttemptAt(for: provider) else { return "" }
|
|
let seconds = max(0, Date().timeIntervalSince(startedAt))
|
|
let formatter = DateComponentsFormatter()
|
|
formatter.allowedUnits = seconds < 60 ? [.second] : [.minute, .second]
|
|
formatter.unitsStyle = .abbreviated
|
|
return formatter.string(from: seconds).map { " (\($0))" } ?? ""
|
|
}()
|
|
return Text(String(format: L("cost_status_fetching"), name, elapsed))
|
|
}
|
|
if let snapshot = self.store.tokenSnapshot(for: provider) {
|
|
let updated = UsageFormatter.updatedString(from: snapshot.updatedAt)
|
|
let cost = snapshot.last30DaysCostUSD
|
|
.map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—"
|
|
let window = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "today" : "\(snapshot.historyDays)d")
|
|
return Text(String(format: L("cost_status_snapshot"), name, updated, window, cost))
|
|
}
|
|
if let error = self.store.tokenError(for: provider), !error.isEmpty {
|
|
let truncated = UsageFormatter.truncatedSingleLine(error, max: 120)
|
|
return Text(String(format: L("cost_status_error"), name, truncated))
|
|
}
|
|
if let lastAttempt = self.store.tokenLastAttemptAt(for: provider) {
|
|
let rel = RelativeDateTimeFormatter()
|
|
rel.locale = Locale(identifier: "en_US")
|
|
rel.unitsStyle = .abbreviated
|
|
let when = rel.localizedString(for: lastAttempt, relativeTo: Date())
|
|
return Text(String(format: L("cost_status_last_attempt"), name, when))
|
|
}
|
|
return Text(String(format: L("cost_status_no_data"), name))
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
struct CostHistoryDaysEditor: View {
|
|
@Bindable var settings: SettingsStore
|
|
|
|
static func title(days: Int) -> String {
|
|
String(format: L("cost_history_days_title"), days)
|
|
}
|
|
|
|
var body: some View {
|
|
LabeledContent(Self.title(days: self.settings.costUsageHistoryDays)) {
|
|
HStack(spacing: 8) {
|
|
TextField(
|
|
Self.title(days: self.settings.costUsageHistoryDays),
|
|
value: self.$settings.costUsageHistoryDays,
|
|
format: .number)
|
|
.labelsHidden()
|
|
.textFieldStyle(.roundedBorder)
|
|
.multilineTextAlignment(.trailing)
|
|
.monospacedDigit()
|
|
.frame(width: 64)
|
|
|
|
Stepper(value: self.$settings.costUsageHistoryDays, in: 1...365, step: 1) {
|
|
EmptyView()
|
|
}
|
|
.labelsHidden()
|
|
}
|
|
}
|
|
}
|
|
}
|