taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

Buttons.swift (16269B)


      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         .accessibilityIdentifier(A11yID.hamburgerButton)
     33     }
     34 }
     35 
     36 struct LinkButton: View {
     37     let destination: URL
     38     let hintTitle: String
     39     let buttonTitle: String
     40     let a11yHint: String
     41     let badge: String
     42 
     43     @AppStorage("minimalistic") var minimalistic: Bool = false
     44 
     45     var body: some View {
     46         VStack(alignment: .leading) {
     47             if !minimalistic {      // show hint that the user should authorize on bank website
     48                 Text(hintTitle)
     49                     .fixedSize(horizontal: false, vertical: true)       // wrap in scrollview
     50                     .multilineTextAlignment(.leading)                   // otherwise
     51                     .listRowSeparator(.hidden)
     52             }
     53             Link(destination: destination) {
     54                 HStack(spacing: 8.0) {
     55                     Image(systemName: LINK)
     56                     Text(buttonTitle)
     57                 }
     58             }
     59             .buttonStyle(TalerButtonStyle(type: .prominent, badge: badge))
     60             .accessibilityHint(a11yHint)
     61         }
     62     }
     63 }
     64 
     65 struct QRButton : View  {
     66     let hideTitle: Bool
     67     let action: () -> Void
     68 
     69     @AppStorage("minimalistic") var minimalistic: Bool = false
     70     @State private var showCameraAlert: Bool = false
     71 
     72     private var openSettingsButton: some View {
     73         Button("Open Settings") {
     74             showCameraAlert = false
     75             UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
     76         }
     77     }
     78     let closingAnnouncement = String(localized: "Closing Camera", comment: "a11y")
     79 
     80     var defaultPriorityAnnouncement = String(localized: "Opening Camera", comment: "a11y")
     81 
     82     var highPriorityAnnouncement: AttributedString {
     83         var highPriorityString = AttributedString(localized: "Camera Active", comment: "a11y")
     84         if #available(iOS 17.0, *) {
     85             highPriorityString.accessibilitySpeechAnnouncementPriority = .high
     86         }
     87         return highPriorityString
     88     }
     89     @MainActor
     90     private func checkCameraAvailable() -> Void {
     91         // Open Camera when QR-Button was tapped
     92         announce(defaultPriorityAnnouncement)
     93 
     94         AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) -> Void in
     95             if granted {
     96                 action()
     97                 if #available(iOS 17.0, *) {
     98                     AccessibilityNotification.Announcement(highPriorityAnnouncement).post()
     99                 } else {
    100                     let cameraActive = String(localized: "Camera Active", comment: "a11y")
    101                     announce(cameraActive)
    102                 }
    103             } else {
    104                 showCameraAlert = true
    105             }
    106         })
    107     }
    108 
    109     var body: some View {
    110         let dismissAlertButton = Button("Cancel", role: .cancel) {
    111             announce(closingAnnouncement)
    112             showCameraAlert = false
    113         }
    114         let scanText = String(localized: "Scan QR code", comment: "Button title, a11y")
    115         let qrImage = Image(systemName: QRBUTTON)
    116         let qrText = Text(qrImage)
    117         Button(action: checkCameraAvailable) {
    118             if hideTitle {
    119                 qrImage
    120                     .resizable()
    121                     .scaledToFit()
    122                     .foregroundStyle(WalletColors().primaryAccent)
    123             } else if minimalistic {
    124                 let width = UIScreen.screenWidth / 7
    125                 qrText.talerFont(.largeTitle)
    126                     .padding(.horizontal, width)
    127             } else {
    128                 HStack(spacing: 16) {
    129                     qrText.talerFont(.title)
    130                     Text(scanText)
    131                 }.padding(.horizontal)
    132             }
    133         }
    134         .accessibilityLabel(scanText)
    135         .accessibilityIdentifier(A11yID.qrButton)
    136         .alert("Scanning QR-codes requires access to the camera",
    137                isPresented: $showCameraAlert,
    138                    actions: {   openSettingsButton
    139                                 dismissAlertButton },
    140                    message: {   Text("Please allow camera access in Settings.") }) // Scanning QR-codes
    141     }
    142 }
    143 
    144 struct DoneButton : View  {
    145     let titleStr: String?
    146     let accessibilityLabelStr: String
    147     let action: () -> Void
    148 
    149     var body: some View {
    150         Button(action: action) {
    151             if let titleStr {
    152                 Text(titleStr)
    153             } else {
    154                 Image(systemName: CHECKMARK)    // 􀆅
    155             }
    156         }
    157         .tint(WalletColors().primaryAccent)
    158         .talerFont(.title)
    159         .accessibilityLabel(accessibilityLabelStr)
    160         .accessibilityIdentifier(A11yID.doneButton)
    161     }
    162 }
    163 
    164 struct PlusButton : View  {
    165     let accessibilityLabelStr: String
    166     let action: () -> Void
    167 
    168     var body: some View {
    169         Button(action: action) {
    170             Image(systemName: PLUS)
    171         }
    172         .tint(WalletColors().primaryAccent)
    173         .talerFont(.title)
    174         .accessibilityLabel(accessibilityLabelStr)
    175         .accessibilityIdentifier(A11yID.plusButton)
    176     }
    177 }
    178 
    179 struct MoreButton : View  {
    180     let accessibilityLabelStr: String
    181     let action: () -> Void
    182 
    183     var body: some View {
    184         Button(action: action) {
    185             Image(systemName: MORE)
    186         }
    187         .tint(WalletColors().primaryAccent)
    188         .talerFont(.title)
    189         .accessibilityLabel(accessibilityLabelStr)
    190         .accessibilityIdentifier(A11yID.moreButton)
    191     }
    192 }
    193 
    194 @available(iOS 26.0, *)
    195 struct SettingsButton26 : View  {
    196     let sortPriority: Double
    197 
    198     var body: some View {
    199         Button {
    200             // will trigger NavigationLink
    201             NotificationCenter.default.post(name: .SettingsAction, object: nil)
    202         } label: {
    203             Label(TalerTab.settings.title, systemImage: TalerTab.settings.sysImg)
    204                 .labelStyle(.iconOnly)
    205         }
    206 //        .padding()
    207         .buttonStyle(.glass)
    208         .accessibilitySortPriority(sortPriority)
    209     }
    210 }
    211 
    212 struct SettingsButton : View  {
    213     let accessibilityLabelStr: String
    214     let action: () -> Void
    215 
    216     var body: some View {
    217         let button = Button(action: action) {
    218             Image(systemName: SETTINGS)
    219         }
    220         .tint(WalletColors().primaryAccent)
    221         .talerFont(.title)
    222         .accessibilityLabel(accessibilityLabelStr)
    223         .accessibilityIdentifier(A11yID.settingsButton)
    224 //        if #available(iOS 26.0, *) {
    225 //            return button.buttonStyle(.glass)
    226 //        }
    227         return button
    228     }
    229 }
    230 struct TalerPasteButton : View  {
    231     let accessibilityLabelStr: String
    232     let action: () -> Void
    233 
    234     var body: some View {
    235         let button = Button(action: action) {
    236             Image(systemName: PASTE)
    237         }
    238             .tint(WalletColors().primaryAccent)
    239             .talerFont(.title)
    240             .accessibilityLabel(accessibilityLabelStr)
    241             .accessibilityIdentifier(A11yID.pasteButton)
    242 //        if #available(iOS 26.0, *) {
    243 //            return button.buttonStyle(.glass)
    244 //        }
    245         return button
    246     }
    247 }
    248 
    249 struct ZoomInButton : View  {
    250     let accessibilityLabelStr: String
    251     let action: () -> Void
    252 
    253     var body: some View {
    254         Button(action: action) {
    255             Image(ICONNAME_ZOOM_IN, SYSTEM_ZOOM_IN)
    256         }
    257         .tint(WalletColors().primaryAccent)
    258         .talerFont(.title)
    259         .accessibilityLabel(accessibilityLabelStr)
    260         .accessibilityIdentifier(A11yID.zoomInButton)
    261     }
    262 }
    263 
    264 struct ZoomOutButton : View  {
    265     let accessibilityLabelStr: String
    266     let action: () -> Void
    267 
    268     var body: some View {
    269         Button(action: action) {
    270             Image(ICONNAME_ZOOM_OUT, SYSTEM_ZOOM_OUT)
    271         }
    272         .tint(WalletColors().primaryAccent)
    273         .talerFont(.title)
    274         .accessibilityLabel(accessibilityLabelStr)
    275         .accessibilityIdentifier(A11yID.zoomOutButton)
    276     }
    277 }
    278 
    279 struct BackButton : View  {
    280     let action: () -> Void
    281 
    282     var body: some View {
    283         Button(action: action) {
    284             let name = ICONNAME_INCOMING + ICONNAME_FILL
    285             let sysName = SYSTEM_INCOMING4 + ICONNAME_FILL
    286             Image(name, sysName, fallback: FALLBACK_INCOMING)
    287         }
    288         .tint(WalletColors().primaryAccent)
    289         .talerFont(.largeTitle)
    290         .accessibilityLabel(Text("Back", comment: "a11y"))
    291         .accessibilityIdentifier(A11yID.backButton)
    292     }
    293 }
    294 
    295 struct ForwardButton : View  {
    296     let enabled: Bool
    297     let action: () -> Void
    298 
    299     var body: some View {
    300         let myAction = {
    301             if enabled {
    302                 action()
    303             }
    304         }
    305         Button(action: myAction) {
    306             let imageName = enabled ? ICONNAME_OUTGOING + ICONNAME_FILL
    307                                     : ICONNAME_OUTGOING
    308             let sysName   = enabled ? SYSTEM_OUTGOING4 + ICONNAME_FILL
    309                                     : SYSTEM_OUTGOING4
    310             Image(imageName, sysName, fallback: FALLBACK_OUTGOING)
    311         }
    312         .tint(WalletColors().primaryAccent)
    313         .talerFont(.largeTitle)
    314         .accessibilityLabel(Text("Continue", comment: "a11y"))
    315         .accessibilityIdentifier(A11yID.forwardButton)
    316     }
    317 }
    318 
    319 struct ArrowUpButton : View  {
    320     let action: () -> Void
    321 
    322     var body: some View {
    323         Button(action: action) {
    324             Image(systemName: ARROW_TOP)       // 􀄿
    325         }
    326         .tint(WalletColors().primaryAccent)
    327         .talerFont(.title2)
    328         .accessibilityLabel(Text("Scroll up", comment: "a11y"))
    329         .accessibilityIdentifier(A11yID.arrowUpButton)
    330     }
    331 }
    332 
    333 struct ArrowDownButton : View  {
    334     let action: () -> Void
    335 
    336     var body: some View {
    337         Button(action: action) {
    338             Image(systemName: ARROW_BOT)        // 􀅀
    339         }
    340         .tint(WalletColors().primaryAccent)
    341         .talerFont(.title2)
    342         .accessibilityLabel(Text("Scroll down", comment: "a11y"))
    343         .accessibilityIdentifier(A11yID.arrowDownButton)
    344     }
    345 }
    346 
    347 struct ReloadButton : View  {
    348     let disabled: Bool
    349     let action: () -> Void
    350 
    351     var body: some View {
    352         Button(action: action) {
    353             Image(systemName: RELOAD)           // 􀅈
    354         }
    355         .tint(WalletColors().primaryAccent)
    356         .talerFont(.title)
    357         .accessibilityLabel(Text("Reload", comment: "a11y"))
    358         .accessibilityIdentifier(A11yID.reloadButton)
    359         .disabled(disabled)
    360     }
    361 }
    362 
    363 enum TalerButtonStyleType {
    364     case plain
    365     case bordered
    366     case prominent
    367 }
    368 struct TalerButtonStyle: ButtonStyle {
    369     var type: TalerButtonStyleType = .plain
    370     var dimmed: Bool = false
    371     var narrow: Bool = false
    372     var one: Bool = false
    373     var disabled: Bool = false
    374     var aligned: TextAlignment = .center
    375     var badge: String = EMPTYSTRING
    376 
    377     @Environment(\.colorScheme) private var colorScheme
    378     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
    379 
    380     public func makeBody(configuration: ButtonStyleConfiguration) -> some View {
    381         //        configuration.role = type == .prominent ? .primary : .normal          Only on macOS
    382         MyBigButton(foreColor: foreColor(type: type, pressed: configuration.isPressed,
    383                                        scheme: colorScheme, contrast: colorSchemeContrast, disabled: disabled),
    384                     backColor: backColor(type: type, pressed: configuration.isPressed, disabled: disabled),
    385                        dimmed: dimmed,
    386                 configuration: configuration,
    387                      disabled: disabled,
    388                        narrow: narrow,
    389                           one: one,
    390                       aligned: aligned,
    391                         badge: badge,
    392                        scheme: colorScheme,
    393                      contrast: colorSchemeContrast)
    394     }
    395 
    396     func foreColor(type: TalerButtonStyleType,
    397                 pressed: Bool,
    398                  scheme: ColorScheme,
    399                contrast: ColorSchemeContrast,
    400                disabled: Bool) -> Color {
    401         if type == .plain {
    402             return WalletColors().fieldForeground      // primary text color
    403         }
    404         return WalletColors().buttonForeColor(pressed: pressed,
    405                                              disabled: disabled,
    406                                                scheme: scheme,
    407                                              contrast: contrast,
    408                                             prominent: type == .prominent)
    409     }
    410     func backColor(type: TalerButtonStyleType, pressed: Bool, disabled: Bool) -> Color {
    411         if type == .plain && !pressed {
    412             return Color.clear
    413         }
    414         return WalletColors().buttonBackColor(pressed: pressed,
    415                                              disabled: disabled,
    416                                             prominent: type == .prominent)
    417     }
    418 
    419     struct BackgroundView: View {
    420         let color: Color
    421         let dimmed: Bool
    422         var body: some View {
    423             RoundedRectangle(
    424                 cornerRadius: 15,
    425                 style: .continuous
    426             )
    427             .fill(color)
    428             .opacity(dimmed ? 0.6 : 1.0)
    429         }
    430     }
    431 
    432     struct MyBigButton: View {
    433 //        var type: TalerButtonStyleType
    434         let foreColor: Color
    435         let backColor: Color
    436         let dimmed: Bool
    437         let configuration: ButtonStyle.Configuration
    438         let disabled: Bool
    439         let narrow: Bool
    440         let one: Bool
    441         let aligned: TextAlignment
    442         var badge: String
    443         var scheme: ColorScheme
    444         var contrast: ColorSchemeContrast
    445 
    446         var body: some View {
    447             let aligned2: Alignment = (aligned == .center) ? Alignment.center
    448                                     : (aligned == .leading) ? Alignment.leading
    449                                     : Alignment.trailing
    450             let hasBadge = !badge.isEmpty
    451             let buttonLabel = configuration.label
    452                                 .multilineTextAlignment(aligned)
    453                                 .lineLimit(one ? 1 : 3)
    454                                 .talerFont(.title3)         //   narrow ? .title3 : .title2
    455                                 .frame(maxWidth: narrow ? nil : .infinity, alignment: aligned2)
    456                                 .padding(.vertical, 10)
    457                                 .padding(.horizontal, hasBadge ? 0 : 6)
    458                                 .foregroundColor(foreColor)
    459                                 .background(BackgroundView(color: backColor, dimmed: dimmed))
    460                                 .contentShape(Rectangle())      // make sure the button can be pressed even if backgroundColor == clear
    461                                 .scaleEffect(configuration.isPressed ? 0.95 : 1)
    462                                 .animation(.spring(response: 0.1), value: configuration.isPressed)
    463                                 .disabled(disabled)
    464             if hasBadge {
    465                 let badgeColor: Color = (badge == CONFIRM_BANK) ? WalletColors().confirm
    466                                                                 : WalletColors().attention
    467                 let badgeV = Image(systemName: badge)
    468                                 .talerFont(.caption)
    469                 HStack(alignment: .top, spacing: 0) {
    470                     badgeV.foregroundColor(.clear)
    471                     buttonLabel
    472                     badgeV.foregroundColor(badgeColor)
    473                 }
    474             } else {
    475                 buttonLabel
    476             }
    477         }
    478     }
    479 }
    480 // MARK: -
    481 #if DEBUG
    482 fileprivate struct ContentView_Previews: PreviewProvider {
    483     static var previews: some View {
    484         let testButtonTitle = String("Placeholder")
    485         Button(testButtonTitle) {}
    486             .buttonStyle(TalerButtonStyle(type: .bordered, aligned: .trailing))
    487     }
    488 }
    489 #endif