GradientBorder.swift (11050B)
1 /* MIT License 2 * Copyright (c) 2024 Sucodee 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 */ 22 /** 23 * @author Marc Stibane 24 */ 25 import SwiftUI 26 27 fileprivate let nfcLogo = Image(systemName: NFCLOGO) // "wave.3.right.circle" 28 29 struct NFCbutton: View { 30 let title: String 31 let action: () -> Void 32 33 @AppStorage("minimalistic") var minimalistic: Bool = false 34 var body: some View { 35 let logo = nfcLogo 36 let nfcButton = Button(minimalistic ? "\(logo)" 37 : "\(logo) \(title)") { 38 action() 39 }.buttonStyle(TalerButtonStyle(type: .prominent)) 40 } 41 } 42 43 @available(iOS 17.7, *) 44 struct BorderWithHCE<Content: View>: View { 45 let talerURI: String 46 let nfcHint: Bool 47 let size: CGFloat 48 let scanHints: (String, String)? 49 var content: () -> Content 50 51 @AppStorage("minimalistic") var minimalistic: Bool = false 52 @AppStorage("showQRauto16") var showQRauto16: Bool = true // iOS 16 doesn't have NFC emulation 53 @AppStorage("showQRauto17") var showQRauto17: Bool = false // but iOS 17 does 54 @StateObject private var tagEmulation = TagEmulation.shared 55 @State private var showQRcode = false 56 57 var body: some View { 58 // let _ = Self._printChanges() 59 let qrCode = Image(systemName: QRCODE) // "qrcode" 60 let qrButton = Button(minimalistic ? "\(qrCode)" 61 : "\(qrCode) Show QR code") { 62 withAnimation { showQRcode = true } 63 } 64 65 let hint = Group { 66 if let scanHints { 67 Text(scanHints.0) 68 .accessibilityLabel(scanHints.1) 69 .multilineTextAlignment(.leading) 70 .talerFont(.title3) 71 } 72 } 73 74 if tagEmulation.canUseHCE { 75 VStack { 76 if nfcHint { // QRCodeDetailView 77 if showQRcode { 78 if !minimalistic { 79 Text("\(nfcLogo) Tap for NFC") 80 .talerFont(.subheadline) 81 .padding(.vertical, -4) 82 } 83 // let screenWidth = UIScreen.screenWidth 84 GradientBorder(size: size + 20.0, 85 color: WalletColors().talerColor, 86 background: WalletColors().backgroundColor) 87 { 88 content() 89 .onTapGesture(count: 1) { tagEmulation.emulateTag(talerURI) } 90 } 91 hint 92 } else { 93 NFCbutton(title: String(localized: "Start NFC")) { 94 tagEmulation.emulateTag(talerURI) 95 } 96 qrButton.buttonStyle(TalerButtonStyle(type: .bordered)) 97 } 98 } else { // AboutView 99 content() 100 .onTapGesture(count: 2) { tagEmulation.emulateTag(talerURI) } 101 } 102 }.listRowSeparator(.hidden) 103 .onDisappear() { 104 tagEmulation.killEmulation() 105 }.task { 106 withAnimation { 107 if #available(iOS 17.7, *) { 108 showQRcode = showQRauto17 109 } else { 110 showQRcode = showQRauto16 111 } 112 } 113 } 114 } else { 115 if showQRcode { 116 content() 117 hint 118 } else { 119 qrButton.buttonStyle(TalerButtonStyle(type: .prominent)) 120 } 121 } 122 } 123 } 124 // MARK: - 125 extension FixedWidthInteger { 126 var data: Data { 127 let data = withUnsafeBytes(of: self) { Data($0) } 128 return data 129 } 130 } 131 132 struct BorderWithNFC<Content: View>: View { 133 let totpString: String // might be a TOTP code 134 let nfcHint: Bool 135 let size: CGFloat 136 let scanHints: (String, String)? 137 var content: () -> Content 138 139 @AppStorage("minimalistic") var minimalistic: Bool = false 140 @AppStorage("showQRauto16") var showQRauto16: Bool = true // iOS 16 doesn't have NFC emulation 141 @AppStorage("showQRauto17") var showQRauto17: Bool = false // but iOS 17 does 142 @State private var showTOTP = false 143 @ObservedObject var nfcWriter = NFCWriter() 144 145 var lockImage: Image { // "lock.badge.clock" 146 Image(ICONNAME_LOCKCLOCK, SYSTEM_LOCKCLOCK, fallback: FALLBACK_LOCK) 147 } 148 149 private var MAGIC_HEADER: Data { 150 Data(fromUInt8Array: [ 151 0x42, // totp magic header 152 ]) 153 } // 42 154 155 var totpData: Data { 156 var data = MAGIC_HEADER // 0x42 = "B" 157 let totpArray = totpString.components(separatedBy: "\n") 158 for totpCode in totpArray { 159 if let totpInt = UInt32(totpCode) { 160 let intData = totpInt.data 161 // print(data, totpInt, intData) 162 data.append(intData) 163 } 164 } 165 // print(data) 166 return data 167 } 168 169 var body: some View { 170 // let _ = Self._printChanges() 171 let totpCode = lockImage 172 let totpButton = Button(minimalistic ? "\(totpCode)" : "\(totpCode) Show TOTP code") { 173 withAnimation { showTOTP = true } 174 } 175 176 let hint = Group { 177 if let scanHints { 178 Text(scanHints.0) 179 .accessibilityLabel(scanHints.1) 180 .multilineTextAlignment(.leading) 181 .talerFont(.title3) 182 } 183 } 184 185 if true { // TODO: check for write capabilities 186 VStack { 187 if nfcHint { // QRCodeDetailView 188 if showTOTP { 189 if !minimalistic { 190 Text("\(nfcLogo) Tap for NFC") 191 .talerFont(.subheadline) 192 .padding(.vertical, -4) 193 } 194 // let screenWidth = UIScreen.screenWidth 195 GradientBorder(size: size + 20.0, 196 color: WalletColors().talerColor, 197 background: WalletColors().backgroundColor) 198 { 199 content() 200 .onTapGesture(count: 1) { nfcWriter.write(totpData) } 201 } 202 hint 203 } else { 204 NFCbutton(title: String(localized: "Write NFC")) { 205 nfcWriter.write(totpData) 206 } 207 totpButton.buttonStyle(TalerButtonStyle(type: .bordered)) 208 } 209 } 210 }.listRowSeparator(.hidden) 211 .onDisappear() { 212 213 }.task { 214 withAnimation { 215 if #available(iOS 17.7, *) { 216 showTOTP = showQRauto17 217 } else { 218 showTOTP = showQRauto16 219 } 220 } 221 } 222 } else { 223 if showTOTP { 224 content() 225 hint 226 } else { 227 totpButton.buttonStyle(TalerButtonStyle(type: .prominent)) 228 } 229 } 230 } 231 } 232 // MARK: - 233 // Use radius: 0 for rect instead of rounded rect 234 struct GradientBorder<Content: View>: View { 235 let size: CGFloat 236 let radius: CGFloat 237 let lineWidth: CGFloat 238 let color: Color 239 let background: Color 240 var content: () -> Content 241 242 @State var rotation: CGFloat = 0 243 244 var body: some View { 245 HStack { 246 Spacer() 247 ZStack { 248 RoundedRectangle(cornerRadius: radius, style: .continuous) 249 .frame(width: size, height: size).foregroundStyle(background) 250 .shadow(color: background.opacity(0.5), radius: 10, x: 0, y: 10) 251 let gradient = Gradient(colors: [ 252 color.opacity(INVISIBLE), 253 color, 254 color, 255 color.opacity(INVISIBLE)] 256 ) 257 let linearGradient = LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom) 258 let rotatingRect = Rectangle() 259 .frame(width: size*2, height: size/2) 260 .foregroundStyle(linearGradient) 261 .rotationEffect(.degrees(rotation)) 262 let border = lineWidth - 0.5 263 rotatingRect 264 .mask { 265 RoundedRectangle(cornerRadius: radius - lineWidth/2, style: .continuous) 266 .stroke(lineWidth: lineWidth) 267 .frame(width: size - border, height: size - border) 268 } 269 content() 270 } 271 .frame(width: size, height: size) 272 Spacer() 273 } 274 .onAppear { 275 withAnimation(.linear(duration: 8).repeatForever(autoreverses: false)) { 276 rotation = 720 277 } 278 } 279 } 280 } 281 extension GradientBorder { 282 init(size: CGFloat, radius: CGFloat = 20.0, lineWidth: CGFloat = 4.0, color: Color, background: Color, content: @escaping () -> Content) { 283 self.size = size 284 self.radius = radius 285 self.lineWidth = lineWidth 286 self.color = color 287 self.background = background 288 self.content = content 289 } 290 } 291 // MARK: - 292 struct GradientBorder_Previews: PreviewProvider { 293 static var previews: some View { 294 295 GradientBorder(size: 260, 296 // radius: 1, 297 // lineWidth: 2, 298 color: .blue, 299 background: .yellow) { 300 Text(verbatim: "Preview") 301 } 302 } 303 }