133 lines
4.8 KiB
Swift
133 lines
4.8 KiB
Swift
import CodexBarCore
|
|
import SwiftUI
|
|
|
|
struct CodexResetCreditPresentationItem: Equatable {
|
|
let expiryText: String
|
|
let compactExpiryText: String
|
|
}
|
|
|
|
struct CodexResetCreditsPresentation: Equatable {
|
|
let text: String
|
|
let items: [CodexResetCreditPresentationItem]
|
|
|
|
var expirySummaryText: String {
|
|
let visibleItems = self.items.prefix(4).map(\.compactExpiryText)
|
|
let hiddenCount = self.items.count - visibleItems.count
|
|
let suffix = hiddenCount > 0 ? ["+\(hiddenCount)"] : []
|
|
return (visibleItems + suffix).joined(separator: " · ")
|
|
}
|
|
|
|
var helpText: String {
|
|
self.items.enumerated().map { index, item in
|
|
"\(index + 1). \(item.expiryText)"
|
|
}.joined(separator: "\n")
|
|
}
|
|
|
|
var accessibilityLabel: String {
|
|
[L("Limit Reset Credits"), self.text, self.helpText]
|
|
.filter { !$0.isEmpty }
|
|
.joined(separator: ", ")
|
|
}
|
|
|
|
static func make(
|
|
snapshot: CodexRateLimitResetCreditsSnapshot,
|
|
resetStyle: ResetTimeDisplayStyle,
|
|
now: Date) -> CodexResetCreditsPresentation?
|
|
{
|
|
let inventory = snapshot.availableInventory(at: now)
|
|
guard !inventory.credits.isEmpty else { return nil }
|
|
let items = inventory.credits.map { credit in
|
|
Self.presentationItem(for: credit, resetStyle: resetStyle, now: now)
|
|
}
|
|
return CodexResetCreditsPresentation(
|
|
text: Self.availableText(count: inventory.count),
|
|
items: items)
|
|
}
|
|
|
|
private static func availableText(count: Int) -> String {
|
|
count == 1 ? L("1 available") : String(format: L("%d available"), count)
|
|
}
|
|
|
|
private static func presentationItem(
|
|
for credit: CodexRateLimitResetCredit,
|
|
resetStyle: ResetTimeDisplayStyle,
|
|
now: Date) -> CodexResetCreditPresentationItem
|
|
{
|
|
guard let expiresAt = credit.expiresAt else {
|
|
return CodexResetCreditPresentationItem(expiryText: L("No expiry"), compactExpiryText: L("No expiry"))
|
|
}
|
|
let formattedTime = Self.formattedTime(expiresAt, resetStyle: resetStyle, now: now)
|
|
let compactExpiryText = resetStyle == .countdown && formattedTime.hasPrefix("in ")
|
|
? String(formattedTime.dropFirst(3))
|
|
: formattedTime
|
|
return CodexResetCreditPresentationItem(
|
|
expiryText: String(format: L("Expires %@"), formattedTime),
|
|
compactExpiryText: compactExpiryText)
|
|
}
|
|
|
|
private static func formattedTime(
|
|
_ expiresAt: Date,
|
|
resetStyle: ResetTimeDisplayStyle,
|
|
now: Date) -> String
|
|
{
|
|
switch resetStyle {
|
|
case .absolute:
|
|
return UsageFormatter.resetDescription(from: expiresAt, now: now)
|
|
case .countdown:
|
|
let countdown = UsageFormatter.resetCountdownDescription(from: expiresAt, now: now)
|
|
return countdown == "now" ? L("now") : countdown
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CodexResetCreditsContent: View {
|
|
let presentation: CodexResetCreditsPresentation
|
|
@Environment(\.menuItemHighlighted) private var isHighlighted
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(L("Limit Reset Credits"))
|
|
.font(.body)
|
|
.fontWeight(.medium)
|
|
.lineLimit(1)
|
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
Text(self.presentation.text)
|
|
.font(.footnote.weight(.semibold))
|
|
.foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))
|
|
.lineLimit(1)
|
|
.layoutPriority(1)
|
|
Spacer(minLength: 8)
|
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
|
Image(systemName: "clock")
|
|
.font(.caption2)
|
|
Text(self.presentation.expirySummaryText)
|
|
.font(.caption)
|
|
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.8)
|
|
}
|
|
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
|
|
.accessibilityHidden(true)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.help(self.presentation.helpText)
|
|
.accessibilityElement(children: .combine)
|
|
.accessibilityLabel(self.presentation.accessibilityLabel)
|
|
}
|
|
}
|
|
|
|
extension UsageMenuCardView.Model {
|
|
static func codexResetCredits(input: Input) -> CodexResetCreditsPresentation? {
|
|
guard input.provider == .codex,
|
|
let resetCredits = input.snapshot?.codexResetCredits
|
|
else {
|
|
return nil
|
|
}
|
|
return CodexResetCreditsPresentation.make(
|
|
snapshot: resetCredits,
|
|
resetStyle: input.resetTimeDisplayStyle,
|
|
now: input.now)
|
|
}
|
|
}
|