import CodexBarCoreimport SwiftUI @MainActorstruct DisplayPane: View { private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit static func overviewProviderLimitText(limit: Int = Self.maxOverviewProviders) -> String { L("overview_choose_providers", String(limit)) } @State private var isOverviewProviderPopoverPresented = false @Bindable var settings: SettingsStore @Bindable var store: UsageStore var body: some View { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 16) { SettingsSection(contentSpacing: 12) { Text(L("section_menu_bar")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) PreferenceToggleRow( title: L("merge_icons_title"), subtitle: L("merge_icons_subtitle"), binding: self.$settings.mergeIcons) PreferenceToggleRow( title: L("switcher_shows_icons_title"), subtitle: L("switcher_shows_icons_subtitle"), binding: self.$settings.switcherShowsIcons) .disabled(!self.settings.mergeIcons) .opacity(self.settings.mergeIcons ? 1 : 0.5) PreferenceToggleRow( title: L("show_most_used_provider_title"), subtitle: L("show_most_used_provider_subtitle"), binding: self.$settings.menuBarShowsHighestUsage) .disabled(!self.settings.mergeIcons) .opacity(self.settings.mergeIcons ? 1 : 0.5) PreferenceToggleRow( title: L("menu_bar_shows_percent_title"), subtitle: L("menu_bar_shows_percent_subtitle"), binding: self.$settings.menuBarShowsBrandIconWithPercent) HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(L("display_mode_title")) .font(.body) Text(L("display_mode_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) } Spacer() Picker(L("Display mode"), selection: self.$settings.menuBarDisplayMode) { ForEach(MenuBarDisplayMode.allCases) { mode in Text(mode.label).tag(mode) } } .labelsHidden() .pickerStyle(.menu) .frame(maxWidth: 200) } .disabled(!self.settings.menuBarShowsBrandIconWithPercent) .opacity(self.settings.menuBarShowsBrandIconWithPercent ? 1 : 0.5) } Divider() SettingsSection(contentSpacing: 12) { Text(L("section_menu_content")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) PreferenceToggleRow( title: L("show_usage_as_used_title"), subtitle: L("show_usage_as_used_subtitle"), binding: self.$settings.usageBarsShowUsed) PreferenceToggleRow( title: L("show_quota_warning_markers_title"), subtitle: L("show_quota_warning_markers_subtitle"), binding: self.$settings.quotaWarningMarkersVisible) HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(L("weekly_progress_work_days_title")) .font(.body) Text(L("weekly_progress_work_days_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) } Spacer() Picker(L("weekly_progress_work_days_title"), selection: self.$settings.weeklyProgressWorkDays) { Text(L("Off")).tag(nil as Int?) Text(L("4 days")).tag(4 as Int?) Text(L("5 days")).tag(5 as Int?) Text(L("7 days")).tag(7 as Int?) } .labelsHidden() .pickerStyle(.menu) .frame(maxWidth: 100) } PreferenceToggleRow( title: L("show_reset_time_as_clock_title"), subtitle: L("show_reset_time_as_clock_subtitle"), binding: self.$settings.resetTimesShowAbsolute) PreferenceToggleRow( title: L("show_provider_changelog_links_title"), subtitle: L("show_provider_changelog_links_subtitle"), binding: self.$settings.providerChangelogLinksEnabled) PreferenceToggleRow( title: L("show_credits_extra_usage_title"), subtitle: L("show_credits_extra_usage_subtitle"), binding: self.$settings.showOptionalCreditsAndExtraUsage) HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(L("multi_account_layout_title")) .font(.body) Text(L("multi_account_layout_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) } Spacer() Picker(L("multi_account_layout_title"), selection: self.$settings.multiAccountMenuLayout) { ForEach(MultiAccountMenuLayout.allCases) { layout in Text(layout.label).tag(layout) } } .labelsHidden() .pickerStyle(.menu) .frame(maxWidth: 200) } self.overviewProviderSelector } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 20) .padding(.vertical, 12) .onAppear { self.reconcileOverviewSelection() } .onChange(of: self.settings.mergeIcons) { _, isEnabled in guard isEnabled else { self.isOverviewProviderPopoverPresented = false return } self.reconcileOverviewSelection() } .onChange(of: self.activeProvidersInOrder) { _, _ in if self.activeProvidersInOrder.isEmpty { self.isOverviewProviderPopoverPresented = false } self.reconcileOverviewSelection() } } } private var overviewProviderSelector: some View { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .center, spacing: 12) { Text(L("overview_tab_providers_title")) .font(.body) Spacer(minLength: 0) if self.showsOverviewConfigureButton { Button(L("configure")) { self.isOverviewProviderPopoverPresented = true } .offset(y: 1) .popover(isPresented: self.$isOverviewProviderPopoverPresented, arrowEdge: .bottom) { self.overviewProviderPopover } } } if !self.settings.mergeIcons { Text(L("overview_enable_merge_icons_hint")) .font(.footnote) .foregroundStyle(.tertiary) } else if self.activeProvidersInOrder.isEmpty { Text(L("overview_no_providers_hint")) .font(.footnote) .foregroundStyle(.tertiary) } else { Text(self.overviewProviderSelectionSummary) .font(.footnote) .foregroundStyle(.tertiary) .lineLimit(2) .truncationMode(.tail) } } } private var overviewProviderPopover: some View { VStack(alignment: .leading, spacing: 10) { Text(Self.overviewProviderLimitText()) .font(.headline) Text(L("overview_rows_follow_order")) .font(.footnote) .foregroundStyle(.tertiary) ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 6) { ForEach(self.activeProvidersInOrder, id: \.self) { provider in Toggle( isOn: Binding( get: { self.overviewSelectedProviders.contains(provider) }, set: { shouldSelect in self.setOverviewProviderSelection(provider: provider, isSelected: shouldSelect) })) { Text(self.providerDisplayName(provider)) .font(.body) } .toggleStyle(.checkbox) .disabled( !self.overviewSelectedProviders.contains(provider) && self.overviewSelectedProviders.count >= Self.maxOverviewProviders) } } } .frame(maxHeight: 220) } .padding(12) .frame(width: 280) } private var activeProvidersInOrder: [UsageProvider] { self.store.enabledProviders() } private var overviewSelectedProviders: [UsageProvider] { self.settings.resolvedMergedOverviewProviders( activeProviders: self.activeProvidersInOrder, maxVisibleProviders: Self.maxOverviewProviders) } private var showsOverviewConfigureButton: Bool { self.settings.mergeIcons && !self.activeProvidersInOrder.isEmpty } private var overviewProviderSelectionSummary: String { let selectedNames = self.overviewSelectedProviders.map(self.providerDisplayName) guard !selectedNames.isEmpty else { return L("overview_no_providers_selected") } return selectedNames.joined(separator: ", ") } private func providerDisplayName(_ provider: UsageProvider) -> String { ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName } private func setOverviewProviderSelection(provider: UsageProvider, isSelected: Bool) { _ = self.settings.setMergedOverviewProviderSelection( provider: provider, isSelected: isSelected, activeProviders: self.activeProvidersInOrder, maxVisibleProviders: Self.maxOverviewProviders) } private func reconcileOverviewSelection() { _ = self.settings.reconcileMergedOverviewSelectedProviders( activeProviders: self.activeProvidersInOrder, maxVisibleProviders: Self.maxOverviewProviders) }}
import CodexBarCore
import SwiftUI
@MainActor
struct DisplayPane: View {
private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit
static func overviewProviderLimitText(limit: Int = Self.maxOverviewProviders) -> String {
L("overview_choose_providers", String(limit))
}
@State private var isOverviewProviderPopoverPresented = false
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
var body: some View {
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 16) {
SettingsSection(contentSpacing: 12) {
Text(L("section_menu_bar"))
.font(.caption)
.foregroundStyle(.secondary)
.textCase(.uppercase)
PreferenceToggleRow(
title: L("merge_icons_title"),
subtitle: L("merge_icons_subtitle"),
binding: self.$settings.mergeIcons)
PreferenceToggleRow(
title: L("switcher_shows_icons_title"),
subtitle: L("switcher_shows_icons_subtitle"),
binding: self.$settings.switcherShowsIcons)
.disabled(!self.settings.mergeIcons)
.opacity(self.settings.mergeIcons ? 1 : 0.5)
PreferenceToggleRow(
title: L("show_most_used_provider_title"),
subtitle: L("show_most_used_provider_subtitle"),
binding: self.$settings.menuBarShowsHighestUsage)
.disabled(!self.settings.mergeIcons)
.opacity(self.settings.mergeIcons ? 1 : 0.5)
PreferenceToggleRow(
title: L("menu_bar_shows_percent_title"),
subtitle: L("menu_bar_shows_percent_subtitle"),
binding: self.$settings.menuBarShowsBrandIconWithPercent)
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(L("display_mode_title"))
.font(.body)
Text(L("display_mode_subtitle"))
.font(.footnote)
.foregroundStyle(.tertiary)
}
Spacer()
Picker(L("Display mode"), selection: self.$settings.menuBarDisplayMode) {
ForEach(MenuBarDisplayMode.allCases) { mode in
Text(mode.label).tag(mode)
}
}
.labelsHidden()
.pickerStyle(.menu)
.frame(maxWidth: 200)
}
.disabled(!self.settings.menuBarShowsBrandIconWithPercent)
.opacity(self.settings.menuBarShowsBrandIconWithPercent ? 1 : 0.5)
}
Divider()
SettingsSection(contentSpacing: 12) {
Text(L("section_menu_content"))
.font(.caption)
.foregroundStyle(.secondary)
.textCase(.uppercase)
PreferenceToggleRow(
title: L("show_usage_as_used_title"),
subtitle: L("show_usage_as_used_subtitle"),
binding: self.$settings.usageBarsShowUsed)
PreferenceToggleRow(
title: L("show_quota_warning_markers_title"),
subtitle: L("show_quota_warning_markers_subtitle"),
binding: self.$settings.quotaWarningMarkersVisible)
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(L("weekly_progress_work_days_title"))
.font(.body)
Text(L("weekly_progress_work_days_subtitle"))
.font(.footnote)
.foregroundStyle(.tertiary)
}
Spacer()
Picker(L("weekly_progress_work_days_title"), selection: self.$settings.weeklyProgressWorkDays) {
Text(L("Off")).tag(nil as Int?)
Text(L("4 days")).tag(4 as Int?)
Text(L("5 days")).tag(5 as Int?)
Text(L("7 days")).tag(7 as Int?)
}
.labelsHidden()
.pickerStyle(.menu)
.frame(maxWidth: 100)
}
PreferenceToggleRow(
title: L("show_reset_time_as_clock_title"),
subtitle: L("show_reset_time_as_clock_subtitle"),
binding: self.$settings.resetTimesShowAbsolute)
PreferenceToggleRow(
title: L("show_provider_changelog_links_title"),
subtitle: L("show_provider_changelog_links_subtitle"),
binding: self.$settings.providerChangelogLinksEnabled)
PreferenceToggleRow(
title: L("show_credits_extra_usage_title"),
subtitle: L("show_credits_extra_usage_subtitle"),
binding: self.$settings.showOptionalCreditsAndExtraUsage)
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(L("multi_account_layout_title"))
.font(.body)
Text(L("multi_account_layout_subtitle"))
.font(.footnote)
.foregroundStyle(.tertiary)
}
Spacer()
Picker(L("multi_account_layout_title"), selection: self.$settings.multiAccountMenuLayout) {
ForEach(MultiAccountMenuLayout.allCases) { layout in
Text(layout.label).tag(layout)
}
}
.labelsHidden()
.pickerStyle(.menu)
.frame(maxWidth: 200)
}
self.overviewProviderSelector
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
.padding(.vertical, 12)
.onAppear {
self.reconcileOverviewSelection()
}
.onChange(of: self.settings.mergeIcons) { _, isEnabled in
guard isEnabled else {
self.isOverviewProviderPopoverPresented = false
return
}
self.reconcileOverviewSelection()
}
.onChange(of: self.activeProvidersInOrder) { _, _ in
if self.activeProvidersInOrder.isEmpty {
self.isOverviewProviderPopoverPresented = false
}
self.reconcileOverviewSelection()
}
}
}
private var overviewProviderSelector: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .center, spacing: 12) {
Text(L("overview_tab_providers_title"))
.font(.body)
Spacer(minLength: 0)
if self.showsOverviewConfigureButton {
Button(L("configure")) {
self.isOverviewProviderPopoverPresented = true
}
.offset(y: 1)
.popover(isPresented: self.$isOverviewProviderPopoverPresented, arrowEdge: .bottom) {
self.overviewProviderPopover
}
}
}
if !self.settings.mergeIcons {
Text(L("overview_enable_merge_icons_hint"))
.font(.footnote)
.foregroundStyle(.tertiary)
} else if self.activeProvidersInOrder.isEmpty {
Text(L("overview_no_providers_hint"))
.font(.footnote)
.foregroundStyle(.tertiary)
} else {
Text(self.overviewProviderSelectionSummary)
.font(.footnote)
.foregroundStyle(.tertiary)
.lineLimit(2)
.truncationMode(.tail)
}
}
}
private var overviewProviderPopover: some View {
VStack(alignment: .leading, spacing: 10) {
Text(Self.overviewProviderLimitText())
.font(.headline)
Text(L("overview_rows_follow_order"))
.font(.footnote)
.foregroundStyle(.tertiary)
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 6) {
ForEach(self.activeProvidersInOrder, id: \.self) { provider in
Toggle(
isOn: Binding(
get: { self.overviewSelectedProviders.contains(provider) },
set: { shouldSelect in
self.setOverviewProviderSelection(provider: provider, isSelected: shouldSelect)
})) {
Text(self.providerDisplayName(provider))
.font(.body)
}
.toggleStyle(.checkbox)
.disabled(
!self.overviewSelectedProviders.contains(provider) &&
self.overviewSelectedProviders.count >= Self.maxOverviewProviders)
}
}
}
.frame(maxHeight: 220)
}
.padding(12)
.frame(width: 280)
}
private var activeProvidersInOrder: [UsageProvider] {
self.store.enabledProviders()
}
private var overviewSelectedProviders: [UsageProvider] {
self.settings.resolvedMergedOverviewProviders(
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
}
private var showsOverviewConfigureButton: Bool {
self.settings.mergeIcons && !self.activeProvidersInOrder.isEmpty
}
private var overviewProviderSelectionSummary: String {
let selectedNames = self.overviewSelectedProviders.map(self.providerDisplayName)
guard !selectedNames.isEmpty else { return L("overview_no_providers_selected") }
return selectedNames.joined(separator: ", ")
}
private func providerDisplayName(_ provider: UsageProvider) -> String {
ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
}
private func setOverviewProviderSelection(provider: UsageProvider, isSelected: Bool) {
_ = self.settings.setMergedOverviewProviderSelection(
provider: provider,
isSelected: isSelected,
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
}
private func reconcileOverviewSelection() {
_ = self.settings.reconcileMergedOverviewSelectedProviders(
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
}
}