taler-ios

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

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