Buttons.swift (13155B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 import Foundation 10 import AVFoundation 11 12 extension ShapeStyle where Self == Color { 13 static var random: Color { 14 Color( 15 red: .random(in: 0...1), 16 green: .random(in: 0...1), 17 blue: .random(in: 0...1) 18 ) 19 } 20 } 21 22 struct HamburgerButton : View { 23 let action: () -> Void 24 25 var body: some View { 26 Button(action: action) { 27 Image(systemName: "line.3.horizontal") 28 // Image(systemName: "sidebar.squares.leading") 29 } 30 .talerFont(.title) 31 .accessibilityLabel(Text("Main Menu", comment: "a11y")) 32 } 33 } 34 35 struct LinkButton: View { 36 let destination: URL 37 let hintTitle: String 38 let buttonTitle: String 39 let a11yHint: String 40 let badge: String 41 42 @AppStorage("minimalistic") var minimalistic: Bool = false 43 44 var body: some View { 45 VStack(alignment: .leading) { 46 if !minimalistic { // show hint that the user should authorize on bank website 47 Text(hintTitle) 48 .fixedSize(horizontal: false, vertical: true) // wrap in scrollview 49 .multilineTextAlignment(.leading) // otherwise 50 .listRowSeparator(.hidden) 51 } 52 Link(destination: destination) { 53 HStack(spacing: 8.0) { 54 Image(systemName: "link") 55 Text(buttonTitle) 56 } 57 } 58 .buttonStyle(TalerButtonStyle(type: .prominent, badge: badge)) 59 .accessibilityHint(a11yHint) 60 } 61 } 62 } 63 64 struct QRButton : View { 65 let hideTitle: Bool 66 let action: () -> Void 67 68 @AppStorage("minimalistic") var minimalistic: Bool = false 69 @State private var showCameraAlert: Bool = false 70 71 private var openSettingsButton: some View { 72 Button("Open Settings") { 73 showCameraAlert = false 74 UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) 75 } 76 } 77 let closingAnnouncement = String(localized: "Closing Camera", comment: "a11y") 78 79 var defaultPriorityAnnouncement = String(localized: "Opening Camera", comment: "a11y") 80 81 var highPriorityAnnouncement: AttributedString { 82 var highPriorityString = AttributedString(localized: "Camera Active", comment: "a11y") 83 if #available(iOS 17.0, *) { 84 highPriorityString.accessibilitySpeechAnnouncementPriority = .high 85 } 86 return highPriorityString 87 } 88 @MainActor 89 private func checkCameraAvailable() -> Void { 90 // Open Camera when QR-Button was tapped 91 announce(defaultPriorityAnnouncement) 92 93 AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) -> Void in 94 if granted { 95 action() 96 if #available(iOS 17.0, *) { 97 AccessibilityNotification.Announcement(highPriorityAnnouncement).post() 98 } else { 99 let cameraActive = String(localized: "Camera Active", comment: "a11y") 100 announce(cameraActive) 101 } 102 } else { 103 showCameraAlert = true 104 } 105 }) 106 } 107 108 var body: some View { 109 let dismissAlertButton = Button("Cancel", role: .cancel) { 110 announce(closingAnnouncement) 111 showCameraAlert = false 112 } 113 let scanText = String(localized: "Scan QR code", comment: "Button title, a11y") 114 let qrImage = Image(systemName: QRBUTTON) 115 let qrText = Text("\(qrImage)", comment: "QR Image") 116 Button(action: checkCameraAvailable) { 117 if hideTitle { 118 qrImage 119 .resizable() 120 .scaledToFit() 121 .foregroundStyle(Color.accentColor) 122 } else if minimalistic { 123 let width = UIScreen.screenWidth / 7 124 qrText.talerFont(.largeTitle) 125 .padding(.horizontal, width) 126 } else { 127 HStack(spacing: 16) { 128 qrText.talerFont(.title) 129 Text(scanText) 130 }.padding(.horizontal) 131 } 132 } 133 .accessibilityLabel(scanText) 134 .alert("Scanning QR-codes requires access to the camera", 135 isPresented: $showCameraAlert, 136 actions: { openSettingsButton 137 dismissAlertButton }, 138 message: { Text("Please allow camera access in settings.") }) // Scanning QR-codes 139 } 140 } 141 142 struct PlusButton : View { 143 let accessibilityLabelStr: String 144 let action: () -> Void 145 146 var body: some View { 147 Button(action: action) { 148 Image(systemName: "plus") 149 } 150 .tint(.accentColor) 151 .talerFont(.title) 152 .accessibilityLabel(accessibilityLabelStr) 153 } 154 } 155 156 struct ZoomInButton : View { 157 let accessibilityLabelStr: String 158 let action: () -> Void 159 160 var body: some View { 161 Button(action: action) { 162 let name = ICONNAME_ZOOM_IN 163 if UIImage(named: name) != nil { 164 Image(name) 165 } else { 166 Image(systemName: SYSTEM_ZOOM_IN) 167 } 168 } 169 .tint(.accentColor) 170 .talerFont(.title) 171 .accessibilityLabel(accessibilityLabelStr) 172 } 173 } 174 175 struct ZoomOutButton : View { 176 let accessibilityLabelStr: String 177 let action: () -> Void 178 179 var body: some View { 180 Button(action: action) { 181 let name = ICONNAME_ZOOM_OUT 182 if UIImage(named: name) != nil { 183 Image(name) 184 } else { 185 Image(systemName: SYSTEM_ZOOM_OUT) 186 } 187 } 188 .tint(.accentColor) 189 .talerFont(.title) 190 .accessibilityLabel(accessibilityLabelStr) 191 } 192 } 193 194 struct BackButton : View { 195 let action: () -> Void 196 197 var body: some View { 198 Button(action: action) { 199 let name = ICONNAME_INCOMING + ICONNAME_FILL 200 let sysName = SYSTEM_INCOMING4 + ICONNAME_FILL 201 if UIImage(named: name) != nil { 202 Image(name) 203 } else if UIImage(systemName: sysName) != nil { 204 Image(systemName: sysName) 205 } else { 206 // TODO: ultralight vs black 207 Image(systemName: FALLBACK_INCOMING) 208 } 209 } 210 .tint(.accentColor) 211 .talerFont(.largeTitle) 212 .accessibilityLabel(Text("Back", comment: "a11y")) 213 } 214 } 215 216 struct ForwardButton : View { 217 let enabled: Bool 218 let action: () -> Void 219 220 var body: some View { 221 let myAction = { 222 if enabled { 223 action() 224 } 225 } 226 Button(action: myAction) { 227 let imageName = enabled ? ICONNAME_OUTGOING + ICONNAME_FILL 228 : ICONNAME_OUTGOING 229 let sysName = enabled ? SYSTEM_OUTGOING4 + ICONNAME_FILL 230 : SYSTEM_OUTGOING4 231 if UIImage(named: imageName) != nil { 232 Image(imageName) 233 } else if UIImage(systemName: sysName) != nil { 234 Image(systemName: sysName) 235 } else { 236 // TODO: ultralight vs black 237 Image(systemName: FALLBACK_OUTGOING) 238 } 239 } 240 .tint(.accentColor) 241 .talerFont(.largeTitle) 242 .accessibilityLabel(Text("Continue", comment: "a11y")) 243 } 244 } 245 246 struct ArrowUpButton : View { 247 let action: () -> Void 248 249 var body: some View { 250 Button(action: action) { 251 Image(systemName: "arrow.up.to.line") 252 } 253 .tint(.accentColor) 254 .talerFont(.title2) 255 .accessibilityLabel(Text("Scroll up", comment: "a11y")) 256 } 257 } 258 259 struct ArrowDownButton : View { 260 let action: () -> Void 261 262 var body: some View { 263 Button(action: action) { 264 Image(systemName: "arrow.down.to.line") 265 } 266 .tint(.accentColor) 267 .talerFont(.title2) 268 .accessibilityLabel(Text("Scroll down", comment: "a11y")) 269 } 270 } 271 272 struct ReloadButton : View { 273 let disabled: Bool 274 let action: () -> Void 275 276 var body: some View { 277 Button(action: action) { 278 Image(systemName: "arrow.clockwise") 279 } 280 .tint(.accentColor) 281 .talerFont(.title) 282 .accessibilityLabel(Text("Reload", comment: "a11y")) 283 .disabled(disabled) 284 } 285 } 286 287 struct TalerButtonStyle: ButtonStyle { 288 enum TalerButtonStyleType { 289 case plain 290 case bordered 291 case prominent 292 } 293 var type: TalerButtonStyleType = .plain 294 var dimmed: Bool = false 295 var narrow: Bool = false 296 var disabled: Bool = false 297 var aligned: TextAlignment = .center 298 var badge: String = EMPTYSTRING 299 300 public func makeBody(configuration: ButtonStyle.Configuration) -> some View { 301 // configuration.role = type == .prominent ? .primary : .normal Only on macOS 302 MyBigButton(foreColor: foreColor(type: type, pressed: configuration.isPressed, disabled: disabled), 303 backColor: backColor(type: type, pressed: configuration.isPressed, disabled: disabled), 304 dimmed: dimmed, 305 configuration: configuration, 306 disabled: disabled, 307 narrow: narrow, 308 aligned: aligned, 309 badge: badge) 310 } 311 312 func foreColor(type: TalerButtonStyleType, pressed: Bool, disabled: Bool) -> Color { 313 if type == .plain { 314 return WalletColors().fieldForeground // primary text color 315 } 316 return WalletColors().buttonForeColor(pressed: pressed, 317 disabled: disabled, 318 prominent: type == .prominent) 319 } 320 func backColor(type: TalerButtonStyleType, pressed: Bool, disabled: Bool) -> Color { 321 if type == .plain && !pressed { 322 return Color.clear 323 } 324 return WalletColors().buttonBackColor(pressed: pressed, 325 disabled: disabled, 326 prominent: type == .prominent) 327 } 328 329 struct BackgroundView: View { 330 let color: Color 331 let dimmed: Bool 332 var body: some View { 333 RoundedRectangle( 334 cornerRadius: 15, 335 style: .continuous 336 ) 337 .fill(color) 338 .opacity(dimmed ? 0.6 : 1.0) 339 } 340 } 341 342 struct MyBigButton: View { 343 // var type: TalerButtonStyleType 344 let foreColor: Color 345 let backColor: Color 346 let dimmed: Bool 347 let configuration: ButtonStyle.Configuration 348 let disabled: Bool 349 let narrow: Bool 350 let aligned: TextAlignment 351 var badge: String 352 353 var body: some View { 354 let aligned2: Alignment = (aligned == .center) ? Alignment.center 355 : (aligned == .leading) ? Alignment.leading 356 : Alignment.trailing 357 let hasBadge = badge.count > 0 358 let buttonLabel = configuration.label 359 .multilineTextAlignment(aligned) 360 .talerFont(.title3) // narrow ? .title3 : .title2 361 .frame(maxWidth: narrow ? nil : .infinity, alignment: aligned2) 362 .padding(.vertical, 10) 363 .padding(.horizontal, hasBadge ? 0 : 6) 364 .foregroundColor(foreColor) 365 .background(BackgroundView(color: backColor, dimmed: dimmed)) 366 .contentShape(Rectangle()) // make sure the button can be pressed even if backgroundColor == clear 367 .scaleEffect(configuration.isPressed ? 0.95 : 1) 368 .animation(.spring(response: 0.1), value: configuration.isPressed) 369 .disabled(disabled) 370 if hasBadge { 371 let badgeColor: Color = (badge == CONFIRM_BANK) ? WalletColors().confirm 372 : WalletColors().attention 373 let badgeV = Image(systemName: badge) 374 .talerFont(.caption) 375 HStack(alignment: .top, spacing: 0) { 376 badgeV.foregroundColor(.clear) 377 buttonLabel 378 badgeV.foregroundColor(badgeColor) 379 } 380 } else { 381 buttonLabel 382 } 383 } 384 } 385 } 386 // MARK: - 387 #if DEBUG 388 fileprivate struct ContentView_Previews: PreviewProvider { 389 static var previews: some View { 390 let testButtonTitle = String("Placeholder") 391 Button(testButtonTitle) {} 392 .buttonStyle(TalerButtonStyle(type: .bordered, aligned: .trailing)) 393 } 394 } 395 #endif