11import CodexBarCore
22import SwiftUI
33
4- struct CodexResetCreditsContent : View {
4+ struct CodexResetCreditsPresentation : Equatable {
55 let text : String
66 let detailText : String ?
7+ let helpText : String ?
8+ let creditToConsume : CodexRateLimitResetCredit ?
9+ }
10+
11+ struct CodexResetCreditsContent : View {
12+ let model : CodexResetCreditsPresentation
13+
14+ @State private var showsDetails = false
15+
716 @Environment ( \. menuItemHighlighted) private var isHighlighted
17+ @Environment ( \. codexResetCreditConsumer) private var consumeCredit
818
919 var body : some View {
10- VStack ( alignment: . leading, spacing: 4 ) {
11- HStack ( alignment: . firstTextBaseline) {
12- Text ( L ( " Limit Reset Credits " ) )
13- . font ( . body)
14- . fontWeight ( . medium)
15- . lineLimit ( 1 )
16- Spacer ( )
17- Text ( self . text)
18- . font ( . footnote)
19- . foregroundStyle ( MenuHighlightStyle . secondary ( self . isHighlighted) )
20- . lineLimit ( 1 )
20+ VStack ( alignment: . leading, spacing: 6 ) {
21+ Button {
22+ self . showsDetails. toggle ( )
23+ } label: {
24+ HStack ( alignment: . firstTextBaseline) {
25+ Text ( self . model. text)
26+ . font ( . body)
27+ . fontWeight ( . medium)
28+ . lineLimit ( 1 )
29+ Spacer ( )
30+ Image ( systemName: self . showsDetails ? " chevron.down " : " chevron.right " )
31+ . font ( . caption)
32+ . foregroundStyle ( MenuHighlightStyle . secondary ( self . isHighlighted) )
33+ }
2134 }
22- if let detailText, !detailText. isEmpty {
23- Text ( detailText)
24- . font ( . footnote)
25- . foregroundStyle ( MenuHighlightStyle . secondary ( self . isHighlighted) )
26- . lineLimit ( 1 )
35+ . buttonStyle ( . plain)
36+ . help ( self . model. helpText ?? self . model. text)
37+
38+ if self . showsDetails {
39+ ForEach ( self . detailLines, id: \. self) { line in
40+ Text ( line)
41+ . font ( . footnote)
42+ . foregroundStyle ( MenuHighlightStyle . secondary ( self . isHighlighted) )
43+ . lineLimit ( 2 )
44+ }
45+ }
46+
47+ HStack ( alignment: . firstTextBaseline, spacing: 8 ) {
48+ if let detailText = self . model. detailText, !detailText. isEmpty {
49+ Text ( detailText)
50+ . font ( . footnote)
51+ . foregroundStyle ( MenuHighlightStyle . secondary ( self . isHighlighted) )
52+ . lineLimit ( 1 )
53+ }
54+ Spacer ( )
55+ if let creditToConsume = self . model. creditToConsume, let consumeCredit {
56+ Button ( L ( " Use Reset " ) ) {
57+ consumeCredit ( creditToConsume)
58+ }
59+ . buttonStyle ( . borderless)
60+ . controlSize ( . small)
61+ . help ( L ( " Use the next expiring Codex reset credit " ) )
62+ }
2763 }
2864 }
2965 . frame ( maxWidth: . infinity, alignment: . leading)
66+ . help ( self . model. helpText ?? self . model. text)
3067 . accessibilityElement ( children: . combine)
3168 . accessibilityLabel ( [
32- L ( " Limit Reset Credits " ) ,
33- self . text,
34- self . detailText,
69+ self . model. text,
70+ self . model. detailText,
3571 ] . compactMap ( \. self) . joined ( separator: " , " ) )
3672 }
73+
74+ private var detailLines : [ String ] {
75+ self . model. helpText? . components ( separatedBy: " \n " ) . filter { !$0. isEmpty } ?? [ ]
76+ }
3777}
3878
3979extension UsageMenuCardView . Model {
80+ static func codexResetCredits( input: Input ) -> CodexResetCreditsPresentation ? {
81+ guard let text = codexResetCreditsText ( input: input) else { return nil }
82+ return CodexResetCreditsPresentation (
83+ text: text,
84+ detailText: Self . codexResetCreditsDetailText ( input: input) ,
85+ helpText: Self . codexResetCreditsHelpText ( input: input) ,
86+ creditToConsume: Self . codexResetCreditToConsume ( input: input) )
87+ }
88+
4089 static func codexResetCreditsText( input: Input ) -> String ? {
4190 guard input. provider == . codex,
42- input. showOptionalCreditsAndExtraUsage,
4391 let resetCredits = input. snapshot? . codexResetCredits,
4492 resetCredits. availableCount > 0
4593 else {
4694 return nil
4795 }
48- let count = resetCredits. availableCount
49- if count == 1 {
96+ if resetCredits. availableCount == 1 {
5097 return L ( " 1 manual reset available " )
5198 }
52- return String ( format: L ( " %d manual resets available " ) , count)
99+ return String ( format: L ( " %d manual resets available " ) , resetCredits. availableCount)
100+ }
101+
102+ static func codexResetCreditToConsume( input: Input ) -> CodexRateLimitResetCredit ? {
103+ guard input. provider == . codex
104+ else {
105+ return nil
106+ }
107+ return input. snapshot? . codexResetCredits? . nextExpiringAvailableCredit
53108 }
54109
55110 static func codexResetCreditsDetailText( input: Input ) -> String ? {
56111 guard input. provider == . codex,
57- input. showOptionalCreditsAndExtraUsage,
58112 let resetCredits = input. snapshot? . codexResetCredits,
59113 let expiresAt = resetCredits. nextExpiringAvailableCredit? . expiresAt
60114 else {
@@ -70,4 +124,29 @@ extension UsageMenuCardView.Model {
70124 }
71125 return String ( format: L ( " Next expires %@ " ) , timeText)
72126 }
127+
128+ static func codexResetCreditsHelpText( input: Input ) -> String ? {
129+ guard input. provider == . codex,
130+ let resetCredits = input. snapshot? . codexResetCredits
131+ else {
132+ return nil
133+ }
134+ let lines = resetCredits. credits. map { credit in
135+ let expires = Self . codexResetCreditExpiryText ( credit, now: input. now)
136+ return " \( credit. status. rawValue) , \( expires) "
137+ }
138+ return lines. isEmpty ? nil : lines. joined ( separator: " \n " )
139+ }
140+
141+ private static func codexResetCreditExpiryText(
142+ _ credit: CodexRateLimitResetCredit ,
143+ now: Date )
144+ -> String
145+ {
146+ guard let expiresAt = credit. expiresAt else { return L ( " No expiry " ) }
147+ let absolute = UsageFormatter . resetDescription ( from: expiresAt, now: now)
148+ guard credit. status == . available, expiresAt > now else { return absolute }
149+ let countdown = UsageFormatter . resetCountdownDescription ( from: expiresAt, now: now)
150+ return " \( countdown) ( \( absolute) ) "
151+ }
73152}
0 commit comments