taler-ios

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

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