chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,604 @@
|
||||
import CodexBarCore
|
||||
import SwiftUI
|
||||
|
||||
enum ProviderMetricInlinePresentation: Equatable {
|
||||
case progress
|
||||
case status(String)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct ProviderDetailView<SupplementaryContent: View>: View {
|
||||
let provider: UsageProvider
|
||||
@Bindable var store: UsageStore
|
||||
@Binding var isEnabled: Bool
|
||||
let subtitle: String
|
||||
let model: UsageMenuCardView.Model
|
||||
let openAIWebDiagnostic: String?
|
||||
let settingsPickers: [ProviderSettingsPickerDescriptor]
|
||||
let settingsToggles: [ProviderSettingsToggleDescriptor]
|
||||
let settingsFields: [ProviderSettingsFieldDescriptor]
|
||||
let settingsActions: [ProviderSettingsActionsDescriptor]
|
||||
let settingsTokenAccounts: ProviderSettingsTokenAccountsDescriptor?
|
||||
let settingsOrganizations: ProviderSettingsOrganizationsDescriptor?
|
||||
let errorDisplay: ProviderErrorDisplay?
|
||||
@Binding var isErrorExpanded: Bool
|
||||
let onCopyError: (String) -> Void
|
||||
let onRefresh: () -> Void
|
||||
let supplementarySettingsContent: SupplementaryContent
|
||||
let showsSupplementarySettingsContent: Bool
|
||||
|
||||
init(
|
||||
provider: UsageProvider,
|
||||
store: UsageStore,
|
||||
isEnabled: Binding<Bool>,
|
||||
subtitle: String,
|
||||
model: UsageMenuCardView.Model,
|
||||
openAIWebDiagnostic: String?,
|
||||
settingsPickers: [ProviderSettingsPickerDescriptor],
|
||||
settingsToggles: [ProviderSettingsToggleDescriptor],
|
||||
settingsFields: [ProviderSettingsFieldDescriptor],
|
||||
settingsActions: [ProviderSettingsActionsDescriptor] = [],
|
||||
settingsTokenAccounts: ProviderSettingsTokenAccountsDescriptor?,
|
||||
settingsOrganizations: ProviderSettingsOrganizationsDescriptor? = nil,
|
||||
errorDisplay: ProviderErrorDisplay?,
|
||||
isErrorExpanded: Binding<Bool>,
|
||||
onCopyError: @escaping (String) -> Void,
|
||||
onRefresh: @escaping () -> Void,
|
||||
showsSupplementarySettingsContent: Bool = false,
|
||||
@ViewBuilder supplementarySettingsContent: () -> SupplementaryContent)
|
||||
{
|
||||
self.provider = provider
|
||||
self.store = store
|
||||
self._isEnabled = isEnabled
|
||||
self.subtitle = subtitle
|
||||
self.model = model
|
||||
self.openAIWebDiagnostic = openAIWebDiagnostic
|
||||
self.settingsPickers = settingsPickers
|
||||
self.settingsToggles = settingsToggles
|
||||
self.settingsFields = settingsFields
|
||||
self.settingsActions = settingsActions
|
||||
self.settingsTokenAccounts = settingsTokenAccounts
|
||||
self.settingsOrganizations = settingsOrganizations
|
||||
self.errorDisplay = errorDisplay
|
||||
self._isErrorExpanded = isErrorExpanded
|
||||
self.onCopyError = onCopyError
|
||||
self.onRefresh = onRefresh
|
||||
self.showsSupplementarySettingsContent = showsSupplementarySettingsContent
|
||||
self.supplementarySettingsContent = supplementarySettingsContent()
|
||||
}
|
||||
|
||||
static func metricTitle(provider: UsageProvider, metric: UsageMenuCardView.Model.Metric) -> String {
|
||||
L(UsageMenuCardView.popupMetricTitle(provider: provider, metric: metric))
|
||||
}
|
||||
|
||||
static func metricInlinePresentation(
|
||||
_ metric: UsageMenuCardView.Model.Metric) -> ProviderMetricInlinePresentation
|
||||
{
|
||||
if let statusText = metric.statusText {
|
||||
return .status(statusText)
|
||||
}
|
||||
return .progress
|
||||
}
|
||||
|
||||
static func planRow(provider: UsageProvider, planText: String?) -> (label: String, value: String)? {
|
||||
guard let rawPlan = planText?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!rawPlan.isEmpty
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
guard provider == .openrouter || provider == .mimo || provider == .moonshot || provider == .poe else {
|
||||
return (label: L("Plan"), value: rawPlan)
|
||||
}
|
||||
|
||||
let prefix = "Balance:"
|
||||
if rawPlan.hasPrefix(prefix) {
|
||||
let valueStart = rawPlan.index(rawPlan.startIndex, offsetBy: prefix.count)
|
||||
let trimmedValue = rawPlan[valueStart...].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmedValue.isEmpty {
|
||||
return (label: L("Balance"), value: trimmedValue)
|
||||
}
|
||||
}
|
||||
if provider == .mimo {
|
||||
return (label: L("Plan"), value: rawPlan)
|
||||
}
|
||||
return (label: L("Balance"), value: rawPlan)
|
||||
}
|
||||
|
||||
private var menuBarSettingsPickers: [ProviderSettingsPickerDescriptor] {
|
||||
self.settingsPickers.filter { $0.placement == .menuBar }
|
||||
}
|
||||
|
||||
private var connectionSettingsPickers: [ProviderSettingsPickerDescriptor] {
|
||||
self.settingsPickers.filter { $0.placement == .connection }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
ProviderDetailHeaderRow(
|
||||
provider: self.provider,
|
||||
store: self.store,
|
||||
isEnabled: self.$isEnabled,
|
||||
subtitle: self.subtitle,
|
||||
onRefresh: self.onRefresh)
|
||||
|
||||
ProviderDetailInfoRows(
|
||||
provider: self.provider,
|
||||
store: self.store,
|
||||
isEnabled: self.isEnabled,
|
||||
model: self.model)
|
||||
}
|
||||
|
||||
Section {
|
||||
ProviderMetricsInlineView(
|
||||
provider: self.provider,
|
||||
model: self.model,
|
||||
openAIWebDiagnostic: self.openAIWebDiagnostic,
|
||||
isEnabled: self.isEnabled)
|
||||
} header: {
|
||||
Text(L("Usage"))
|
||||
}
|
||||
|
||||
if let errorDisplay {
|
||||
Section {
|
||||
ProviderErrorView(
|
||||
title: String(
|
||||
format: L("last_fetch_failed_with_provider"),
|
||||
self.store.metadata(for: self.provider).displayName),
|
||||
display: errorDisplay,
|
||||
isExpanded: self.$isErrorExpanded,
|
||||
onCopy: { self.onCopyError(errorDisplay.full) })
|
||||
}
|
||||
}
|
||||
|
||||
if !self.menuBarSettingsPickers.isEmpty {
|
||||
Section {
|
||||
ForEach(self.menuBarSettingsPickers) { picker in
|
||||
ProviderSettingsPickerRowView(picker: picker)
|
||||
}
|
||||
} header: {
|
||||
Text(L("provider_section_menu_bar"))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.connectionSettingsPickers.isEmpty || !self.settingsActions.isEmpty {
|
||||
Section {
|
||||
ForEach(self.connectionSettingsPickers) { picker in
|
||||
ProviderSettingsPickerRowView(picker: picker)
|
||||
}
|
||||
ForEach(self.settingsActions) { descriptor in
|
||||
ProviderSettingsActionsRowView(descriptor: descriptor)
|
||||
}
|
||||
} header: {
|
||||
Text(L("provider_section_connection"))
|
||||
}
|
||||
}
|
||||
|
||||
if let tokenAccounts = self.settingsTokenAccounts,
|
||||
tokenAccounts.isVisible?() ?? true
|
||||
{
|
||||
ProviderSettingsTokenAccountsRowView(descriptor: tokenAccounts)
|
||||
}
|
||||
|
||||
ForEach(self.settingsFields) { field in
|
||||
ProviderSettingsFieldRowView(field: field)
|
||||
}
|
||||
|
||||
if let organizations = self.settingsOrganizations {
|
||||
ProviderSettingsOrganizationsRowView(descriptor: organizations)
|
||||
}
|
||||
|
||||
if self.showsSupplementarySettingsContent {
|
||||
self.supplementarySettingsContent
|
||||
}
|
||||
|
||||
ProviderQuotaWarningSettingsView(provider: self.provider, settings: self.store.settings)
|
||||
|
||||
if !self.settingsToggles.isEmpty {
|
||||
Section {
|
||||
ForEach(self.settingsToggles) { toggle in
|
||||
ProviderSettingsToggleRowView(toggle: toggle)
|
||||
}
|
||||
} header: {
|
||||
Text(L("Options"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct ProviderDetailHeaderRow: View {
|
||||
let provider: UsageProvider
|
||||
@Bindable var store: UsageStore
|
||||
@Binding var isEnabled: Bool
|
||||
let subtitle: String
|
||||
let onRefresh: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
ProviderDetailBrandIcon(provider: self.provider)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.store.metadata(for: self.provider).displayName)
|
||||
.font(.title3.weight(.semibold))
|
||||
|
||||
Text(self.detailSubtitle)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer(minLength: 12)
|
||||
|
||||
Button {
|
||||
self.onRefresh()
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help(L("Refresh"))
|
||||
|
||||
Toggle(L("Enabled"), isOn: self.$isEnabled)
|
||||
.labelsHidden()
|
||||
.toggleStyle(.switch)
|
||||
.controlSize(.small)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private var detailSubtitle: String {
|
||||
let lines = self.subtitle.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
guard lines.count >= 2 else { return self.subtitle }
|
||||
let first = lines[0]
|
||||
let rest = lines.dropFirst().joined(separator: "\n")
|
||||
let tail = rest.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if tail.isEmpty { return String(first) }
|
||||
return "\(first) • \(tail)"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct ProviderDetailBrandIcon: View {
|
||||
let provider: UsageProvider
|
||||
|
||||
var body: some View {
|
||||
if let brand = ProviderBrandIcon.image(for: self.provider) {
|
||||
Image(nsImage: brand)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
Image(systemName: "circle.dotted")
|
||||
.font(.system(size: 24, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct ProviderDetailInfoRows: View {
|
||||
let provider: UsageProvider
|
||||
@Bindable var store: UsageStore
|
||||
let isEnabled: Bool
|
||||
let model: UsageMenuCardView.Model
|
||||
|
||||
var body: some View {
|
||||
ProviderDetailInfoRow(label: L("Source"), value: self.store.sourceLabel(for: self.provider))
|
||||
ProviderDetailInfoRow(label: L("Version"), value: self.store.version(for: self.provider) ?? L("not detected"))
|
||||
ProviderDetailInfoRow(label: L("Updated"), value: self.updatedText)
|
||||
|
||||
if let status = self.store.status(for: self.provider) {
|
||||
ProviderDetailInfoRow(label: L("Status"), value: status.description ?? status.indicator.label)
|
||||
}
|
||||
|
||||
if !self.model.email.isEmpty {
|
||||
ProviderDetailInfoRow(label: L("Account"), value: self.model.email)
|
||||
}
|
||||
|
||||
if self.provider == .kiro,
|
||||
let authMethod = self.store.snapshot(for: self.provider)?.loginMethod(for: .kiro),
|
||||
!authMethod.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
{
|
||||
ProviderDetailInfoRow(label: L("Auth"), value: authMethod)
|
||||
}
|
||||
|
||||
if let planRow = ProviderDetailView<EmptyView>.planRow(
|
||||
provider: self.provider,
|
||||
planText: self.model.planText)
|
||||
{
|
||||
ProviderDetailInfoRow(label: planRow.label, value: planRow.value)
|
||||
}
|
||||
}
|
||||
|
||||
private var updatedText: String {
|
||||
if let updated = self.store.snapshot(for: self.provider)?.updatedAt {
|
||||
return UsageFormatter.updatedString(from: updated)
|
||||
}
|
||||
if self.store.refreshingProviders.contains(self.provider) {
|
||||
return L("Refreshing")
|
||||
}
|
||||
if self.store.unavailableMessage(for: self.provider) != nil {
|
||||
return L("Unavailable")
|
||||
}
|
||||
return L("Not fetched yet")
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProviderDetailInfoRow: View {
|
||||
let label: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
LabeledContent(self.label) {
|
||||
Text(self.value)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.lineLimit(2)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct ProviderMetricsInlineView: View {
|
||||
let provider: UsageProvider
|
||||
let model: UsageMenuCardView.Model
|
||||
let openAIWebDiagnostic: String?
|
||||
let isEnabled: Bool
|
||||
|
||||
struct InfoRow: Identifiable, Equatable {
|
||||
enum ID: Hashable {
|
||||
case credits
|
||||
case openAIWeb
|
||||
}
|
||||
|
||||
let id: ID
|
||||
let label: String
|
||||
let value: String
|
||||
}
|
||||
|
||||
static func infoRows(
|
||||
for model: UsageMenuCardView.Model,
|
||||
openAIWebDiagnostic: String?) -> [InfoRow]
|
||||
{
|
||||
var rows: [InfoRow] = []
|
||||
if let credits = model.creditsText {
|
||||
rows.append(InfoRow(id: .credits, label: L("Credits"), value: credits))
|
||||
}
|
||||
if let diagnostic = openAIWebDiagnostic {
|
||||
rows.append(InfoRow(id: .openAIWeb, label: L("OpenAI web extras"), value: diagnostic))
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let hasMetrics = !self.model.metrics.isEmpty
|
||||
let hasUsageNotes = !self.model.usageNotes.isEmpty
|
||||
let infoRows = Self.infoRows(for: self.model, openAIWebDiagnostic: self.openAIWebDiagnostic)
|
||||
let hasProviderCost = self.model.providerCost != nil
|
||||
let hasTokenUsage = self.model.tokenUsage != nil
|
||||
let hasResetCredits = self.model.codexResetCredits != nil
|
||||
|
||||
if !hasMetrics, !hasUsageNotes, !hasProviderCost, infoRows.isEmpty, !hasTokenUsage, !hasResetCredits {
|
||||
Text(self.placeholderText)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(self.model.metrics, id: \.id) { metric in
|
||||
ProviderMetricInlineRow(
|
||||
metric: metric,
|
||||
title: ProviderDetailView<EmptyView>.metricTitle(provider: self.provider, metric: metric),
|
||||
progressColor: self.model.progressColor)
|
||||
}
|
||||
|
||||
if hasUsageNotes {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(Array(self.model.usageNotes.enumerated()), id: \.offset) { _, note in
|
||||
Text(note)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(infoRows) { row in
|
||||
ProviderDetailInfoRow(label: row.label, value: row.value)
|
||||
}
|
||||
|
||||
if let resetCredits = self.model.codexResetCredits {
|
||||
ProviderCodexResetCreditsInlineRow(presentation: resetCredits)
|
||||
}
|
||||
|
||||
if let providerCost = self.model.providerCost {
|
||||
ProviderMetricInlineCostRow(
|
||||
section: providerCost,
|
||||
progressColor: self.model.progressColor)
|
||||
}
|
||||
|
||||
if let tokenUsage = self.model.tokenUsage {
|
||||
ProviderMetricInlineTextRow(title: L("Cost"), value: tokenUsage.sessionLine)
|
||||
ProviderMetricInlineTextRow(title: "", value: tokenUsage.monthLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var placeholderText: String {
|
||||
if !self.isEnabled {
|
||||
return L("Disabled — no recent data")
|
||||
}
|
||||
return self.model.placeholder.map(L) ?? L("No usage yet")
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProviderMetricInlineRow: View {
|
||||
let metric: UsageMenuCardView.Model.Metric
|
||||
let title: String
|
||||
let progressColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
switch ProviderDetailView<EmptyView>.metricInlinePresentation(self.metric) {
|
||||
case let .status(statusText):
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(self.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 8)
|
||||
Text(statusText)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
case .progress:
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(self.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 8)
|
||||
Text(self.metric.percentLabel)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
|
||||
UsageProgressBar(
|
||||
percent: self.metric.percent,
|
||||
tint: self.progressColor,
|
||||
accessibilityLabel: self.metric.percentStyle.accessibilityLabel,
|
||||
pacePercent: self.metric.pacePercent,
|
||||
paceOnTop: self.metric.paceOnTop,
|
||||
warningMarkerPercents: self.metric.warningMarkerPercents,
|
||||
workdayMarkerPercents: self.metric.workdayMarkerPercents)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
let hasLeftDetail = self.metric.detailLeftText?.isEmpty == false
|
||||
let hasRightDetail = self.metric.detailRightText?.isEmpty == false
|
||||
let resetText = self.metric.resetText ?? ""
|
||||
if hasLeftDetail || hasRightDetail || !resetText.isEmpty {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
if let leftDetail = self.metric.detailLeftText, !leftDetail.isEmpty {
|
||||
Text(leftDetail)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
if let rightDetail = self.metric.detailRightText, !rightDetail.isEmpty {
|
||||
Text(rightDetail)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} else if !resetText.isEmpty {
|
||||
Text(resetText)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasRightDetail, !resetText.isEmpty {
|
||||
Text(resetText)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
|
||||
if let detail = self.metric.detailText, !detail.isEmpty {
|
||||
Text(detail)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProviderCodexResetCreditsInlineRow: View {
|
||||
let presentation: CodexResetCreditsPresentation
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(L("Limit Reset Credits"))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Spacer(minLength: 8)
|
||||
Text(self.presentation.text)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Image(systemName: "clock")
|
||||
.font(.caption2)
|
||||
Text(self.presentation.expirySummaryText)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(self.presentation.accessibilityLabel)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProviderMetricInlineTextRow: View {
|
||||
let title: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
if !self.title.isEmpty {
|
||||
Text(self.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(self.value)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProviderMetricInlineCostRow: View {
|
||||
let section: UsageMenuCardView.Model.ProviderCostSection
|
||||
let progressColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(self.section.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Spacer(minLength: 8)
|
||||
if let percentLine = self.section.percentLine {
|
||||
Text(percentLine)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
|
||||
if let percentUsed = self.section.percentUsed {
|
||||
UsageProgressBar(
|
||||
percent: percentUsed,
|
||||
tint: self.progressColor,
|
||||
accessibilityLabel: L("Usage used"))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
Text(self.section.spendLine)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user