Skip to content

Commit e2cebe3

Browse files
committed
feat: calculator v61 redesign
1 parent e662538 commit e2cebe3

17 files changed

Lines changed: 1493 additions & 384 deletions

Bitkit/AppScene.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct AppScene: View {
3030
@StateObject private var pubkyProfile = PubkyProfileManager()
3131
@StateObject private var contactsManager = ContactsManager()
3232
@State private var keyboardManager = KeyboardManager()
33+
@State private var calculatorInputManager = CalculatorInputManager()
3334

3435
@State private var hideSplash = false
3536
@State private var removeSplash = false
@@ -135,6 +136,7 @@ struct AppScene: View {
135136
.environmentObject(pubkyProfile)
136137
.environmentObject(contactsManager)
137138
.environment(keyboardManager)
139+
.environment(calculatorInputManager)
138140
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
139141
if authState == .authenticated, let pk = pubkyProfile.publicKey {
140142
Task { try? await contactsManager.loadContacts(for: pk) }

Bitkit/Components/Header.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import SwiftUI
22

33
struct Header: View {
4+
@Environment(CalculatorInputManager.self) private var calculatorInput
45
@EnvironmentObject var app: AppViewModel
56
@EnvironmentObject var navigation: NavigationViewModel
67
@EnvironmentObject var pubkyProfile: PubkyProfileManager
@@ -25,12 +26,14 @@ struct Header: View {
2526
AppStatus(
2627
testID: "HeaderAppStatus",
2728
onPress: {
29+
if dismissCalculatorIfNeeded() { return }
2830
navigation.navigate(.appStatus)
2931
}
3032
)
3133

3234
if showWidgetEditButton {
3335
Button(action: {
36+
if dismissCalculatorIfNeeded() { return }
3437
isEditingWidgets.toggle()
3538
}) {
3639
Image(isEditingWidgets ? "check-mark" : "pencil")
@@ -45,6 +48,8 @@ struct Header: View {
4548
}
4649

4750
Button {
51+
if dismissCalculatorIfNeeded() { return }
52+
4853
withAnimation {
4954
app.showDrawer = true
5055
}
@@ -65,9 +70,10 @@ struct Header: View {
6570
.padding(.trailing, 10)
6671
}
6772

68-
@ViewBuilder
6973
private var profileButton: some View {
7074
Button {
75+
if dismissCalculatorIfNeeded() { return }
76+
7177
if pubkyProfile.isAuthenticated || pubkyProfile.cachedName != nil {
7278
navigation.navigate(.profile)
7379
} else if pubkyProfile.initializationErrorMessage != nil {
@@ -96,6 +102,12 @@ struct Header: View {
96102
.accessibilityIdentifier("ProfileButton")
97103
}
98104

105+
private func dismissCalculatorIfNeeded() -> Bool {
106+
guard calculatorInput.isPresented else { return false }
107+
calculatorInput.dismiss()
108+
return true
109+
}
110+
99111
@ViewBuilder
100112
private var profileAvatar: some View {
101113
if let imageUri = pubkyProfile.displayImageUri {

Bitkit/Components/NumberPad.swift

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,38 @@ enum NumberPadType {
88

99
struct NumberPad: View {
1010
let type: NumberPadType
11+
let decimalSeparator: String
1112
let errorKey: String?
13+
let onDeleteLongPress: (() -> Void)?
1214
let onPress: (String) -> Void
1315

14-
init(type: NumberPadType = .simple, errorKey: String? = nil, onPress: @escaping (String) -> Void) {
16+
static var contentHeight: CGFloat {
17+
buttonHeight * 4
18+
}
19+
20+
private static var buttonHeight: CGFloat {
21+
UIScreen.main.isSmall ? 65 : 44 + 34
22+
}
23+
24+
init(
25+
type: NumberPadType = .simple,
26+
decimalSeparator: String = ".",
27+
errorKey: String? = nil,
28+
onDeleteLongPress: (() -> Void)? = nil,
29+
onPress: @escaping (String) -> Void
30+
) {
1531
self.type = type
32+
self.decimalSeparator = decimalSeparator
1633
self.errorKey = errorKey
34+
self.onDeleteLongPress = onDeleteLongPress
1735
self.onPress = onPress
1836
}
1937

20-
private let buttonHeight: CGFloat = UIScreen.main.isSmall ? 65 : 44 + 34
2138
private let gridItems = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3)
2239
private let numbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
40+
private var buttonHeight: CGFloat {
41+
Self.buttonHeight
42+
}
2343

2444
var body: some View {
2545
VStack(spacing: 0) {
@@ -59,7 +79,7 @@ struct NumberPad: View {
5979
}
6080
case .decimal:
6181
NumberPadButton(
62-
text: ".",
82+
text: decimalSeparator,
6383
height: buttonHeight,
6484
hasError: errorKey == ".",
6585
testID: "NDecimal"
@@ -98,6 +118,13 @@ struct NumberPad: View {
98118
.buttonStyle(NumberPadButtonStyle())
99119
.accessibilityIdentifier("NRemove")
100120
.frame(maxWidth: .infinity)
121+
.simultaneousGesture(
122+
LongPressGesture(minimumDuration: 0.45).onEnded { _ in
123+
guard let onDeleteLongPress else { return }
124+
Haptics.play(.buttonTap)
125+
onDeleteLongPress()
126+
}
127+
)
101128
}
102129
}
103130
}

Bitkit/Components/TabBar/TabBar.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import SwiftUI
22

33
struct TabBar: View {
4+
@Environment(CalculatorInputManager.self) private var calculatorInput
45
@EnvironmentObject var navigation: NavigationViewModel
56
@EnvironmentObject var sheets: SheetViewModel
67
@EnvironmentObject var wallet: WalletViewModel
78

89
var shouldShow: Bool {
10+
if calculatorInput.isPresented { return false }
11+
912
let routesWithTabBar = Set<Route>([.activityList, .savingsWallet, .spendingWallet])
1013
if navigation.path.isEmpty { return true }
1114
return navigation.currentRoute.map { routesWithTabBar.contains($0) } ?? false
@@ -66,6 +69,7 @@ struct TabBar: View {
6669
.frame(maxWidth: .infinity, maxHeight: .infinity)
6770
.overlay {
6871
TabBar()
72+
.environment(CalculatorInputManager())
6973
.environmentObject(NavigationViewModel())
7074
.environmentObject(SheetViewModel())
7175
}
@@ -79,6 +83,7 @@ struct TabBar: View {
7983
.frame(maxWidth: .infinity, maxHeight: .infinity)
8084
.overlay {
8185
TabBar()
86+
.environment(CalculatorInputManager())
8287
.environmentObject(NavigationViewModel())
8388
.environmentObject(SheetViewModel())
8489
}

Bitkit/Components/Widgets/BaseWidget.swift

Lines changed: 83 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -125,104 +125,104 @@ struct BaseWidget<Content: View>: View {
125125
}
126126

127127
var body: some View {
128-
Button {} label: {
129-
VStack(spacing: 0) {
130-
if type == .suggestions ? isEditing : (settings.showWidgetTitles || isEditing) {
131-
HStack {
132-
HStack(spacing: 16) {
133-
Image(metadata.icon)
134-
.resizable()
135-
.frame(width: 32, height: 32)
128+
widgetContent
129+
.accessibilityIdentifierIfPresent(isEditing ? nil : "\(type.rawValue.capitalized)Widget")
130+
.frame(maxWidth: .infinity)
131+
.padding((hasBackground || isEditing) ? 16 : 0)
132+
.background((hasBackground || isEditing) ? Color.gray6 : Color.clear)
133+
.cornerRadius(hasBackground || isEditing ? 16 : 0)
134+
.alert(
135+
t("widgets__delete__title"),
136+
isPresented: $showDeleteDialog,
137+
actions: {
138+
Button(t("common__cancel"), role: .cancel) {
139+
showDeleteDialog = false
140+
}
136141

137-
BodyMSBText(truncate(metadata.name, 18))
138-
.lineLimit(1)
139-
}
142+
Button(t("common__delete_yes"), role: .destructive) {
143+
widgets.deleteWidget(type)
144+
showDeleteDialog = false
145+
}
146+
},
147+
message: {
148+
Text(t("widgets__delete__description", variables: ["name": metadata.name]))
149+
}
150+
)
151+
}
140152

141-
Spacer()
142-
143-
// Action buttons when in edit mode
144-
if isEditing {
145-
HStack(spacing: 8) {
146-
// Delete button
147-
Button {
148-
onDelete()
149-
} label: {
150-
Image("trash")
151-
.resizable()
152-
.foregroundColor(.textPrimary)
153-
.frame(width: 24, height: 24)
154-
}
155-
.frame(width: 32, height: 32)
156-
.contentShape(Rectangle())
157-
.accessibilityIdentifier("\(metadata.name)_WidgetActionDelete")
158-
159-
// Edit button
160-
Button {
161-
onEdit()
162-
} label: {
163-
Image("gear-six")
164-
.resizable()
165-
.foregroundColor(.textPrimary)
166-
.frame(width: 24, height: 24)
167-
}
168-
.frame(width: 32, height: 32)
169-
.contentShape(Rectangle())
170-
.accessibilityIdentifier("\(metadata.name)_WidgetActionEdit")
153+
private var widgetContent: some View {
154+
VStack(spacing: 0) {
155+
if type == .suggestions ? isEditing : (settings.showWidgetTitles || isEditing) {
156+
HStack {
157+
HStack(spacing: 16) {
158+
Image(metadata.icon)
159+
.resizable()
160+
.frame(width: 32, height: 32)
161+
162+
BodyMSBText(truncate(metadata.name, 18))
163+
.lineLimit(1)
164+
}
165+
166+
Spacer()
171167

172-
Image("burger")
168+
// Action buttons when in edit mode
169+
if isEditing {
170+
HStack(spacing: 8) {
171+
// Delete button
172+
Button {
173+
onDelete()
174+
} label: {
175+
Image("trash")
173176
.resizable()
174177
.foregroundColor(.textPrimary)
175178
.frame(width: 24, height: 24)
176-
.frame(width: 32, height: 32)
177-
.contentShape(Rectangle())
178-
.overlay {
179-
Color.clear
180-
.frame(width: 44, height: 44)
181-
.contentShape(Rectangle())
182-
.trackDragHandle()
183-
}
184-
.accessibilityIdentifier("\(metadata.name)_WidgetActionReorder")
185179
}
186-
}
187-
}
180+
.frame(width: 32, height: 32)
181+
.contentShape(Rectangle())
182+
.accessibilityIdentifier("\(metadata.name)_WidgetActionDelete")
183+
184+
// Edit button
185+
Button {
186+
onEdit()
187+
} label: {
188+
Image("gear-six")
189+
.resizable()
190+
.foregroundColor(.textPrimary)
191+
.frame(width: 24, height: 24)
192+
}
193+
.frame(width: 32, height: 32)
194+
.contentShape(Rectangle())
195+
.accessibilityIdentifier("\(metadata.name)_WidgetActionEdit")
188196

189-
// Add spacer only when showing title and not editing
190-
if settings.showWidgetTitles && !isEditing {
191-
Spacer()
192-
.frame(height: 16)
197+
Image("burger")
198+
.resizable()
199+
.foregroundColor(.textPrimary)
200+
.frame(width: 24, height: 24)
201+
.frame(width: 32, height: 32)
202+
.contentShape(Rectangle())
203+
.overlay {
204+
Color.clear
205+
.frame(width: 44, height: 44)
206+
.contentShape(Rectangle())
207+
.trackDragHandle()
208+
}
209+
.accessibilityIdentifier("\(metadata.name)_WidgetActionReorder")
210+
}
193211
}
194212
}
195213

196-
// Widget content (only shown when not editing)
197-
if !isEditing {
198-
content
214+
// Add spacer only when showing title and not editing
215+
if settings.showWidgetTitles && !isEditing {
216+
Spacer()
217+
.frame(height: 16)
199218
}
200219
}
201-
.contentShape(Rectangle())
202-
}
203-
.accessibilityIdentifier("\(type.rawValue.capitalized)Widget")
204-
.buttonStyle(WidgetButtonStyle())
205-
.frame(maxWidth: .infinity)
206-
.padding((hasBackground || isEditing) ? 16 : 0)
207-
.background((hasBackground || isEditing) ? Color.gray6 : Color.clear)
208-
.cornerRadius(hasBackground || isEditing ? 16 : 0)
209-
.alert(
210-
t("widgets__delete__title"),
211-
isPresented: $showDeleteDialog,
212-
actions: {
213-
Button(t("common__cancel"), role: .cancel) {
214-
showDeleteDialog = false
215-
}
216220

217-
Button(t("common__delete_yes"), role: .destructive) {
218-
widgets.deleteWidget(type)
219-
showDeleteDialog = false
220-
}
221-
},
222-
message: {
223-
Text(t("widgets__delete__description", variables: ["name": metadata.name]))
221+
// Widget content (only shown when not editing)
222+
if !isEditing {
223+
content
224224
}
225-
)
225+
}
226226
}
227227

228228
/// Truncate a string to a maximum length
@@ -236,14 +236,6 @@ struct BaseWidget<Content: View>: View {
236236
}
237237
}
238238

239-
/// Custom button style for widgets
240-
struct WidgetButtonStyle: ButtonStyle {
241-
func makeBody(configuration: Configuration) -> some View {
242-
configuration.label
243-
.opacity(configuration.isPressed ? 0.9 : 1.0)
244-
}
245-
}
246-
247239
// Preview for the BaseWidget
248240
#Preview {
249241
VStack {

0 commit comments

Comments
 (0)