taler-ios

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

CopyShare.swift (8391B)


      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 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 /// The Share function can NOT be called from a sheet.
    164 ///  Thus we need to know whether we were called from the .sheet view hierarchy
    165 ///  If yes, then we must first dismiss the sheet before posting the .ShareAction notification
    166 extension EnvironmentValues {
    167     @Entry var fromSheet: Bool = false
    168 }
    169 // MARK: -
    170 @MainActor
    171 struct ShareButton: View {
    172     private let symLog = SymLogV(0)
    173     let textToShare: String
    174     let image: UIImage?
    175     let title: String
    176 
    177     init(_ textToShare: String, image: UIImage? = nil) {
    178         self.textToShare = textToShare
    179         self.image = image
    180         self.title = String(localized: "Share")
    181     }
    182     init(_ textToShare: String, title: String, image: UIImage? = nil) {
    183         self.textToShare = textToShare
    184         self.image = image
    185         self.title = title
    186     }
    187 
    188     @Environment(\.fromSheet) private var fromSheet
    189     @Environment(\.isEnabled) private var isEnabled: Bool
    190     @EnvironmentObject private var controller: Controller
    191 
    192     @State private var scale: CGFloat = 1.0
    193 
    194     @MainActor
    195     func dismissAndPost() {
    196         if fromSheet {
    197             dismissTop()
    198         }
    199         let shareType = ShareType(textToShare: textToShare, image: image)
    200         let userInfo = [NOTIFICATIONSHARE: shareType]
    201         NotificationCenter.default.post(name: .ShareAction, object: nil, userInfo: userInfo)                // will trigger NavigationLink
    202     }
    203 
    204     func shareAction() -> Void {
    205         symLog.log(textToShare)
    206         controller.hapticFeedback(.soft)
    207         Task {
    208             // First scale down quickly
    209             withAnimation(.easeIn(duration: 0.1)) {
    210                 scale = 0.8
    211             }
    212             // Then bounce back bigger
    213             DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
    214                 withAnimation(.easeOut(duration: 0.25)) {
    215                     scale = 1.15
    216                 }
    217                 dismissAndPost()
    218             }
    219             // Finally settle to normal
    220             DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
    221                 withAnimation(.easeInOut(duration: 0.25)) {
    222                     scale = 1.0
    223                 }
    224             }
    225         }
    226     }
    227 
    228     var body: some View {
    229         Button(action: shareAction) {
    230             HStack {
    231                 Image(systemName: SHARE)        // 􀈂
    232                     .accessibility(hidden: true)
    233                 Text(title)
    234             }
    235         }
    236         .tint(WalletColors().primaryAccent)
    237         .talerFont(.body)
    238         .scaleEffect(scale)
    239         .disabled(!isEnabled)
    240     }
    241 }
    242 // MARK: -
    243 struct CopyShare: View {
    244     @Environment(\.isEnabled) private var isEnabled: Bool
    245 
    246     let textToCopy: String
    247     let image: UIImage?
    248 
    249     init(_ string: String, image: UIImage? = nil) {
    250         self.textToCopy = string
    251         self.image = image
    252     }
    253 
    254     var body: some View {
    255         let copyB = CopyButton(textToCopy, vertical: false, image: image)
    256                 .buttonStyle(TalerButtonStyle(type: .bordered))
    257         let share = ShareButton(textToCopy, image: image)
    258                 .buttonStyle(TalerButtonStyle(type: .bordered))
    259 
    260         let vLayout = VStack {
    261             copyB
    262             share
    263         }
    264 
    265         if #available(iOS 16.0, *) {
    266             let hLayout = HStack(spacing: HSPACING) {
    267                 copyB
    268                 share
    269             }
    270             ViewThatFits(in: .horizontal) {
    271                 hLayout
    272                 vLayout
    273             }
    274         } else {
    275             vLayout
    276         }
    277     }
    278 }
    279 // MARK: -
    280 struct CopyShare_Previews: PreviewProvider {
    281     static var previews: some View {
    282         CopyShare("Hallö", image: nil)
    283     }
    284 }