taler-ios

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

CopyShare.swift (7990B)


      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 UniformTypeIdentifiers
      9 import SwiftUI
     10 import SymLog
     11 
     12 @MainActor
     13 struct FeedbackButton: View {
     14     private let symLog = SymLogV(0)
     15     let title: String?
     16     let image: UIImage?
     17     let isDisabled: Bool
     18     let action: () -> Void
     19 
     20     @EnvironmentObject private var controller: Controller
     21     @State private var scale: CGFloat = 1.0
     22 
     23     public init(_ title: String? = nil,
     24                   image: UIImage? = nil,
     25                disabled: Bool = false,
     26                  action: @escaping @MainActor () -> Void) {
     27         self.title = title
     28         self.image = image
     29         self.isDisabled = disabled
     30         self.action = action
     31     }
     32 
     33     private func triggerPulse() {
     34         // First scale down quickly
     35         withAnimation(.easeIn(duration: 0.1)) {
     36             scale = 0.8
     37         }
     38         // Then bounce back bigger
     39         DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
     40             withAnimation(.easeOut(duration: 0.25)) {
     41                 scale = 1.15
     42             }
     43         }
     44         // Finally settle to normal
     45         DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
     46             withAnimation(.easeInOut(duration: 0.25)) {
     47                 scale = 1.0
     48             }
     49         }
     50     }
     51 
     52     var body: some View {
     53         Button(title ?? EMPTYSTRING) {
     54             symLog.log(title ?? EMPTYSTRING)
     55             controller.hapticFeedback(.medium)
     56             action()
     57             triggerPulse()
     58         }
     59             .buttonStyle(TalerButtonStyle(type: .bordered, disabled: isDisabled))
     60     }
     61 }
     62 // MARK: -
     63 @MainActor
     64 struct CopyButton: View {
     65     private let symLog = SymLogV(0)
     66     let textToCopy: String
     67     @Binding var isCopied: Bool?
     68     let image: UIImage?
     69     let vertical: Bool
     70     let title: String?
     71 
     72     @Environment(\.isEnabled) private var isEnabled: Bool
     73     @EnvironmentObject private var controller: Controller
     74 
     75     @State private var scale: CGFloat = 1.0
     76 
     77     init(_ textToCopy: String, isCopied: Binding<Bool?>? = nil,
     78              vertical: Bool, image: UIImage? = nil) {
     79         self.textToCopy = textToCopy
     80         self._isCopied = isCopied ?? Binding.constant(nil)
     81         self.image = image
     82         self.vertical = vertical
     83         self.title = nil
     84     }
     85 
     86     init(_ textToCopy: String, isCopied: Binding<Bool?>? = nil,
     87          title: String, image: UIImage? = nil) {
     88         self.textToCopy = textToCopy
     89         self._isCopied = isCopied ?? Binding.constant(nil)
     90         self.image = image
     91         self.vertical = false
     92         self.title = title
     93     }
     94 
     95     func copyAction() -> Void {
     96         symLog.log(textToCopy)
     97         triggerPulse()
     98         controller.hapticFeedback(.medium)
     99         let pasteboard = UIPasteboard.general
    100         if let image {
    101 //            pasteboard.image = image
    102             let strItem = [UTType.plainText.identifier : textToCopy]
    103             let imgItem = [UTType.image.identifier : image]
    104             pasteboard.items = [imgItem, strItem]       // iOS27 Notes.app crashes if text comes first!
    105         } else {
    106             pasteboard.string = textToCopy
    107         }
    108         if isCopied != nil {
    109             isCopied = true
    110         }
    111     }
    112 
    113     private func triggerPulse() {
    114         // First scale down quickly
    115         withAnimation(.easeIn(duration: 0.1)) {
    116             scale = 0.8
    117         }
    118         // Then bounce back bigger
    119         DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
    120             withAnimation(.easeOut(duration: 0.25)) {
    121                 scale = 1.15
    122             }
    123         }
    124         // Finally settle to normal
    125         DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
    126             withAnimation(.easeInOut(duration: 0.25)) {
    127                 scale = 1.0
    128             }
    129         }
    130     }
    131 
    132     var body: some View {
    133         Button(action: copyAction) {
    134             let image = Image(systemName: COPY1)    // 􀉁
    135                 .accessibility(hidden: true)
    136 
    137             if vertical {
    138                 VStack {
    139                     let shortCopy = String(localized: "Copy.short", defaultValue: "Copy", comment: "5 letters max, else abbreviate")
    140                     image
    141                     Text(shortCopy)
    142                 }
    143             } else {
    144                 let longCopy = String(localized: "Copy.long", defaultValue: "Copy", comment: "may be a bit longer")
    145                 HStack {
    146                     image
    147                     Text(title ?? longCopy)
    148                 }
    149             }
    150         }
    151         .tint(WalletColors().primaryAccent)
    152         .talerFont(.body)
    153         .scaleEffect(scale)
    154         .disabled(!isEnabled)
    155     }
    156 }
    157 // MARK: -
    158 struct ShareType: Hashable {
    159     let textToShare: String
    160     let image: UIImage?
    161 }
    162 // MARK: -
    163 @MainActor
    164 struct ShareButton: View {
    165     private let symLog = SymLogV(0)
    166     let textToShare: String
    167     let image: UIImage?
    168     let title: String
    169 
    170     @State private var scale: CGFloat = 1.0
    171 
    172     init(_ textToShare: String, image: UIImage? = nil) {
    173         self.textToShare = textToShare
    174         self.image = image
    175         self.title = String(localized: "Share")
    176     }
    177     init(_ textToShare: String, title: String, image: UIImage? = nil) {
    178         self.textToShare = textToShare
    179         self.image = image
    180         self.title = title
    181     }
    182 
    183     @Environment(\.isEnabled) private var isEnabled: Bool
    184     @EnvironmentObject private var controller: Controller
    185 
    186     @MainActor
    187     func dismissAndPost() {
    188         dismissTop()
    189         let shareType = ShareType(textToShare: textToShare, image: image)
    190         let userInfo = [NOTIFICATIONSHARE: shareType]
    191         NotificationCenter.default.post(name: .ShareAction, object: nil, userInfo: userInfo)                // will trigger NavigationLink
    192     }
    193 
    194     func shareAction() -> Void {
    195         symLog.log(textToShare)
    196         controller.hapticFeedback(.soft)
    197         Task {
    198             // First scale down quickly
    199             withAnimation(.easeIn(duration: 0.1)) {
    200                 scale = 0.8
    201             }
    202             // Then bounce back bigger
    203             DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
    204                 withAnimation(.easeOut(duration: 0.25)) {
    205                     scale = 1.15
    206                 }
    207                 dismissAndPost()
    208             }
    209             // Finally settle to normal
    210             DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
    211                 withAnimation(.easeInOut(duration: 0.25)) {
    212                     scale = 1.0
    213                 }
    214             }
    215         }
    216     }
    217 
    218     var body: some View {
    219         Button(action: shareAction) {
    220             HStack {
    221                 Image(systemName: SHARE)        // 􀈂
    222                     .accessibility(hidden: true)
    223                 Text(title)
    224             }
    225         }
    226         .tint(WalletColors().primaryAccent)
    227         .talerFont(.body)
    228         .scaleEffect(scale)
    229         .disabled(!isEnabled)
    230     }
    231 }
    232 // MARK: -
    233 struct CopyShare: View {
    234     @Environment(\.isEnabled) private var isEnabled: Bool
    235 
    236     let textToCopy: String
    237     let image: UIImage?
    238 
    239     init(_ string: String, image: UIImage? = nil) {
    240         self.textToCopy = string
    241         self.image = image
    242     }
    243 
    244     var body: some View {
    245         let copyB = CopyButton(textToCopy, vertical: false, image: image)
    246                 .buttonStyle(TalerButtonStyle(type: .bordered))
    247         let share = ShareButton(textToCopy, image: image)
    248                 .buttonStyle(TalerButtonStyle(type: .bordered))
    249 
    250         let vLayout = VStack {
    251             copyB
    252             share
    253         }
    254 
    255         if #available(iOS 16.0, *) {
    256             let hLayout = HStack(spacing: HSPACING) {
    257                 copyB
    258                 share
    259             }
    260             ViewThatFits(in: .horizontal) {
    261                 hLayout
    262                 vLayout
    263             }
    264         } else {
    265             vLayout
    266         }
    267     }
    268 }
    269 // MARK: -
    270 struct CopyShare_Previews: PreviewProvider {
    271     static var previews: some View {
    272         CopyShare("Hallö", image: nil)
    273     }
    274 }