Buttons.swift (15511B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 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: HAMBURGER) 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) 116 Button(action: checkCameraAvailable) { 117 if hideTitle { 118 qrImage 119 .resizable() 120 .scaledToFit() 121 .foregroundStyle(WalletColors().primaryAccent) 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 DoneButton : View { 143 let titleStr: String? 144 let accessibilityLabelStr: String 145 let action: () -> Void 146 147 var body: some View { 148 Button(action: action) { 149 if let titleStr { 150 Text(titleStr) 151 } else { 152 Image(systemName: CHECKMARK) // 153 } 154 } 155 .tint(WalletColors().primaryAccent) 156 .talerFont(.title) 157 .accessibilityLabel(accessibilityLabelStr) 158 } 159 } 160 161 struct PlusButton : View { 162 let accessibilityLabelStr: String 163 let action: () -> Void 164 165 var body: some View { 166 Button(action: action) { 167 Image(systemName: PLUS) 168 } 169 .tint(WalletColors().primaryAccent) 170 .talerFont(.title) 171 .accessibilityLabel(accessibilityLabelStr) 172 } 173 } 174 175 struct MoreButton : View { 176 let accessibilityLabelStr: String 177 let action: () -> Void 178 179 var body: some View { 180 Button(action: action) { 181 Image(systemName: MORE) 182 } 183 .tint(WalletColors().primaryAccent) 184 .talerFont(.title) 185 .accessibilityLabel(accessibilityLabelStr) 186 } 187 } 188 189 @available(iOS 26.0, *) 190 struct SettingsButton26 : View { 191 let sortPriority: Double 192 193 var body: some View { 194 Button { 195 // will trigger NavigationLink 196 NotificationCenter.default.post(name: .SettingsAction, object: nil) 197 } label: { 198 Label(TalerTab.settings.title, systemImage: TalerTab.settings.sysImg) 199 .labelStyle(.iconOnly) 200 } 201 // .padding() 202 .buttonStyle(.glass) 203 .accessibilitySortPriority(sortPriority) 204 } 205 } 206 207 struct SettingsButton : View { 208 let accessibilityLabelStr: String 209 let action: () -> Void 210 211 var body: some View { 212 let button = Button(action: action) { 213 Image(systemName: SETTINGS) 214 } 215 .tint(WalletColors().primaryAccent) 216 .talerFont(.title) 217 .accessibilityLabel(accessibilityLabelStr) 218 // if #available(iOS 26.0, *) { 219 // return button.buttonStyle(.glass) 220 // } 221 return button 222 } 223 } 224 struct TalerPasteButton : View { 225 let accessibilityLabelStr: String 226 let action: () -> Void 227 228 var body: some View { 229 let button = Button(action: action) { 230 Image(systemName: PASTE) 231 } 232 .tint(WalletColors().primaryAccent) 233 .talerFont(.title) 234 .accessibilityLabel(accessibilityLabelStr) 235 // if #available(iOS 26.0, *) { 236 // return button.buttonStyle(.glass) 237 // } 238 return button 239 } 240 } 241 242 struct ZoomInButton : View { 243 let accessibilityLabelStr: String 244 let action: () -> Void 245 246 var body: some View { 247 Button(action: action) { 248 Image(ICONNAME_ZOOM_IN, SYSTEM_ZOOM_IN) 249 } 250 .tint(WalletColors().primaryAccent) 251 .talerFont(.title) 252 .accessibilityLabel(accessibilityLabelStr) 253 } 254 } 255 256 struct ZoomOutButton : View { 257 let accessibilityLabelStr: String 258 let action: () -> Void 259 260 var body: some View { 261 Button(action: action) { 262 Image(ICONNAME_ZOOM_OUT, SYSTEM_ZOOM_OUT) 263 } 264 .tint(WalletColors().primaryAccent) 265 .talerFont(.title) 266 .accessibilityLabel(accessibilityLabelStr) 267 } 268 } 269 270 struct BackButton : View { 271 let action: () -> Void 272 273 var body: some View { 274 Button(action: action) { 275 let name = ICONNAME_INCOMING + ICONNAME_FILL 276 let sysName = SYSTEM_INCOMING4 + ICONNAME_FILL 277 Image(name, sysName, fallback: FALLBACK_INCOMING) 278 } 279 .tint(WalletColors().primaryAccent) 280 .talerFont(.largeTitle) 281 .accessibilityLabel(Text("Back", comment: "a11y")) 282 } 283 } 284 285 struct ForwardButton : View { 286 let enabled: Bool 287 let action: () -> Void 288 289 var body: some View { 290 let myAction = { 291 if enabled { 292 action() 293 } 294 } 295 Button(action: myAction) { 296 let imageName = enabled ? ICONNAME_OUTGOING + ICONNAME_FILL 297 : ICONNAME_OUTGOING 298 let sysName = enabled ? SYSTEM_OUTGOING4 + ICONNAME_FILL 299 : SYSTEM_OUTGOING4 300 Image(imageName, sysName, fallback: FALLBACK_OUTGOING) 301 } 302 .tint(WalletColors().primaryAccent) 303 .talerFont(.largeTitle) 304 .accessibilityLabel(Text("Continue", comment: "a11y")) 305 } 306 } 307 308 struct ArrowUpButton : View { 309 let action: () -> Void 310 311 var body: some View { 312 Button(action: action) { 313 Image(systemName: ARROW_TOP) // 314 } 315 .tint(WalletColors().primaryAccent) 316 .talerFont(.title2) 317 .accessibilityLabel(Text("Scroll up", comment: "a11y")) 318 } 319 } 320 321 struct ArrowDownButton : View { 322 let action: () -> Void 323 324 var body: some View { 325 Button(action: action) { 326 Image(systemName: ARROW_BOT) // 327 } 328 .tint(WalletColors().primaryAccent) 329 .talerFont(.title2) 330 .accessibilityLabel(Text("Scroll down", comment: "a11y")) 331 } 332 } 333 334 struct ReloadButton : View { 335 let disabled: Bool 336 let action: () -> Void 337 338 var body: some View { 339 Button(action: action) { 340 Image(systemName: RELOAD) // 341 } 342 .tint(WalletColors().primaryAccent) 343 .talerFont(.title) 344 .accessibilityLabel(Text("Reload", comment: "a11y")) 345 .disabled(disabled) 346 } 347 } 348 349 enum TalerButtonStyleType { 350 case plain 351 case bordered 352 case prominent 353 } 354 struct TalerButtonStyle: ButtonStyle { 355 var type: TalerButtonStyleType = .plain 356 var dimmed: Bool = false 357 var narrow: Bool = false 358 var one: Bool = false 359 var disabled: Bool = false 360 var aligned: TextAlignment = .center 361 var badge: String = EMPTYSTRING 362 363 @Environment(\.colorScheme) private var colorScheme 364 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 365 366 public func makeBody(configuration: ButtonStyleConfiguration) -> some View { 367 // configuration.role = type == .prominent ? .primary : .normal Only on macOS 368 MyBigButton(foreColor: foreColor(type: type, pressed: configuration.isPressed, 369 scheme: colorScheme, contrast: colorSchemeContrast, disabled: disabled), 370 backColor: backColor(type: type, pressed: configuration.isPressed, disabled: disabled), 371 dimmed: dimmed, 372 configuration: configuration, 373 disabled: disabled, 374 narrow: narrow, 375 one: one, 376 aligned: aligned, 377 badge: badge, 378 scheme: colorScheme, 379 contrast: colorSchemeContrast) 380 } 381 382 func foreColor(type: TalerButtonStyleType, 383 pressed: Bool, 384 scheme: ColorScheme, 385 contrast: ColorSchemeContrast, 386 disabled: Bool) -> Color { 387 if type == .plain { 388 return WalletColors().fieldForeground // primary text color 389 } 390 return WalletColors().buttonForeColor(pressed: pressed, 391 disabled: disabled, 392 scheme: scheme, 393 contrast: contrast, 394 prominent: type == .prominent) 395 } 396 func backColor(type: TalerButtonStyleType, pressed: Bool, disabled: Bool) -> Color { 397 if type == .plain && !pressed { 398 return Color.clear 399 } 400 return WalletColors().buttonBackColor(pressed: pressed, 401 disabled: disabled, 402 prominent: type == .prominent) 403 } 404 405 struct BackgroundView: View { 406 let color: Color 407 let dimmed: Bool 408 var body: some View { 409 RoundedRectangle( 410 cornerRadius: 15, 411 style: .continuous 412 ) 413 .fill(color) 414 .opacity(dimmed ? 0.6 : 1.0) 415 } 416 } 417 418 struct MyBigButton: View { 419 // var type: TalerButtonStyleType 420 let foreColor: Color 421 let backColor: Color 422 let dimmed: Bool 423 let configuration: ButtonStyle.Configuration 424 let disabled: Bool 425 let narrow: Bool 426 let one: Bool 427 let aligned: TextAlignment 428 var badge: String 429 var scheme: ColorScheme 430 var contrast: ColorSchemeContrast 431 432 var body: some View { 433 let aligned2: Alignment = (aligned == .center) ? Alignment.center 434 : (aligned == .leading) ? Alignment.leading 435 : Alignment.trailing 436 let hasBadge = !badge.isEmpty 437 let buttonLabel = configuration.label 438 .multilineTextAlignment(aligned) 439 .lineLimit(one ? 1 : 3) 440 .talerFont(.title3) // narrow ? .title3 : .title2 441 .frame(maxWidth: narrow ? nil : .infinity, alignment: aligned2) 442 .padding(.vertical, 10) 443 .padding(.horizontal, hasBadge ? 0 : 6) 444 .foregroundColor(foreColor) 445 .background(BackgroundView(color: backColor, dimmed: dimmed)) 446 .contentShape(Rectangle()) // make sure the button can be pressed even if backgroundColor == clear 447 .scaleEffect(configuration.isPressed ? 0.95 : 1) 448 .animation(.spring(response: 0.1), value: configuration.isPressed) 449 .disabled(disabled) 450 if hasBadge { 451 let badgeColor: Color = (badge == CONFIRM_BANK) ? WalletColors().confirm 452 : WalletColors().attention 453 let badgeV = Image(systemName: badge) 454 .talerFont(.caption) 455 HStack(alignment: .top, spacing: 0) { 456 badgeV.foregroundColor(.clear) 457 buttonLabel 458 badgeV.foregroundColor(badgeColor) 459 } 460 } else { 461 buttonLabel 462 } 463 } 464 } 465 } 466 // MARK: - 467 #if DEBUG 468 fileprivate struct ContentView_Previews: PreviewProvider { 469 static var previews: some View { 470 let testButtonTitle = String("Placeholder") 471 Button(testButtonTitle) {} 472 .buttonStyle(TalerButtonStyle(type: .bordered, aligned: .trailing)) 473 } 474 } 475 #endif