GradientBorder.swift (11079B)
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 35 var body: some View { 36 let logo = nfcLogo 37 Button(minimalistic ? "\(logo)" 38 : "\(logo) \(title)") { 39 action() 40 }.buttonStyle(TalerButtonStyle(type: .prominent)) 41 } 42 } 43 44 @available(iOS 17.7, *) 45 struct BorderWithHCE<Content: View>: View { 46 let talerURI: String 47 let nfcHint: Bool 48 let size: CGFloat 49 let scanHints: (String, String)? 50 var content: () -> Content 51 52 @AppStorage("minimalistic") var minimalistic: Bool = false 53 @AppStorage("showQRauto16") var showQRauto16: Bool = true // iOS 16 doesn't have NFC emulation 54 @AppStorage("showQRauto17") var showQRauto17: Bool = false // but iOS 17 does 55 @StateObject private var tagEmulation = TagEmulation.shared 56 @State private var showQRcode = false 57 58 var body: some View { 59 // let _ = Self._printChanges() 60 let qrCode = Image(systemName: QRCODE) // "qrcode" 61 let qrButton = Button(minimalistic ? "\(qrCode)" 62 : "\(qrCode) Show QR code") { 63 withAnimation { showQRcode = true } 64 } 65 66 let hint = Group { 67 if let scanHints { 68 Text(scanHints.0) 69 .accessibilityLabel(scanHints.1) 70 .multilineTextAlignment(.leading) 71 .talerFont(.title3) 72 } 73 } 74 75 if tagEmulation.canUseHCE { 76 VStack { 77 if nfcHint { // QRCodeDetailView 78 if showQRcode { 79 if !minimalistic { 80 Text("\(nfcLogo) Tap for NFC") 81 .talerFont(.subheadline) 82 .padding(.vertical, -4) 83 } 84 // let screenWidth = UIScreen.screenWidth 85 content() 86 .onTapGesture(count: 1) { tagEmulation.emulateTag(talerURI) } 87 hint 88 } else { 89 NFCbutton(title: String(localized: "Start NFC")) { 90 tagEmulation.emulateTag(talerURI) 91 } 92 qrButton.buttonStyle(TalerButtonStyle(type: .bordered)) 93 } 94 } else { // AboutView 95 content() 96 .onTapGesture(count: 2) { tagEmulation.emulateTag(talerURI) } 97 } 98 }.listRowSeparator(.hidden) 99 .onDisappear() { 100 tagEmulation.killEmulation() 101 }.task { 102 withAnimation { 103 if #available(iOS 17.7, *) { 104 showQRcode = showQRauto17 105 } else { 106 showQRcode = showQRauto16 107 } 108 } 109 } 110 } else if showQRcode { 111 content() 112 hint 113 } else { 114 qrButton.buttonStyle(TalerButtonStyle(type: .prominent)) 115 } 116 } 117 } 118 // MARK: - 119 extension FixedWidthInteger { 120 var data: Data { 121 let data = withUnsafeBytes(of: self) { Data($0) } 122 return data 123 } 124 } 125 126 struct BorderWithNFC<Content: View>: View { 127 let totpString: String // might be a TOTP code 128 let nfcHint: Bool 129 let automaticNfc: Bool? 130 let size: CGFloat 131 let scanHints: (String, String)? 132 var content: () -> Content 133 134 @AppStorage("minimalistic") var minimalistic: Bool = false 135 @AppStorage("showQRauto16") var showQRauto16: Bool = true // iOS 16 doesn't have NFC emulation 136 @AppStorage("showQRauto17") var showQRauto17: Bool = false // but iOS 17 does 137 @State private var showTOTP = false 138 @State private var didWriteTOTP = false 139 @ObservedObject var nfcWriter = NFCWriter() 140 141 var lockImage: Image { // "lock.badge.clock" 142 Image(ICONNAME_LOCKCLOCK, SYSTEM_LOCKCLOCK, fallback: FALLBACK_LOCK) 143 } 144 145 private var MAGIC_HEADER: Data { 146 Data(fromUInt8Array: [ 147 0x42, // totp magic header 148 ]) 149 } // 42 150 151 var totpData: Data { 152 var data = MAGIC_HEADER // 0x42 = "B" 153 let totpArray = totpString.components(separatedBy: LINEFEED) 154 for totpCode in totpArray { 155 if let totpInt = UInt32(totpCode) { 156 let intData = totpInt.data 157 // print(data, totpInt, intData) 158 data.append(intData) 159 } 160 } 161 // print(data) 162 return data 163 } 164 165 var body: some View { 166 // let _ = Self._printChanges() 167 let totpCode = lockImage 168 let totpButton = Button(minimalistic ? "\(totpCode)" : "\(totpCode) Show TOTP code") { 169 withAnimation { showTOTP = true } 170 } 171 172 let hint = Group { 173 if let scanHints { 174 Text(scanHints.0) 175 .accessibilityLabel(scanHints.1) 176 .multilineTextAlignment(.leading) 177 .talerFont(.title3) 178 } 179 } 180 181 if true { // We always have NFC write capabilities 182 VStack { 183 if let automaticNfc, automaticNfc, !didWriteTOTP { 184 Text("\(nfcLogo) Point your iPhone to the NFC device") 185 .tint(WalletColors().primaryAccent) 186 .task { 187 DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) { 188 nfcWriter.write(totpData) 189 didWriteTOTP = true 190 } 191 } 192 } else if nfcHint { // QRCodeDetailView 193 if showTOTP { 194 if !minimalistic { 195 Text("\(nfcLogo) Tap for NFC") 196 .talerFont(.subheadline) 197 .padding(.vertical, -4) 198 } 199 ZStack { 200 GradientBorder(size: size + 20.0, 201 color: WalletColors().primaryAccent, 202 background: WalletColors().backgroundColor) 203 content() 204 .onTapGesture(count: 1) { nfcWriter.write(totpData) } 205 } 206 hint 207 } else { 208 NFCbutton(title: String(localized: "Write NFC")) { 209 nfcWriter.write(totpData) 210 } 211 totpButton.buttonStyle(TalerButtonStyle(type: .bordered)) 212 } 213 } 214 }.listRowSeparator(.hidden) 215 .onDisappear() { 216 217 }.task { 218 withAnimation { 219 if #available(iOS 17.7, *) { 220 showTOTP = showQRauto17 221 } else { 222 showTOTP = showQRauto16 223 } 224 } 225 } 226 } else { 227 if showTOTP { 228 content() 229 hint 230 } else { 231 totpButton.buttonStyle(TalerButtonStyle(type: .prominent)) 232 } 233 } 234 } 235 } 236 // MARK: - 237 // Use radius: 0 for rect instead of rounded rect 238 struct GradientBorder: View { 239 let size: CGFloat 240 let radius: CGFloat 241 let lineWidth: CGFloat 242 let color: Color 243 let background: Color 244 245 @State private var rotation: CGFloat = 0 246 247 var body: some View { 248 HStack { 249 Spacer() 250 ZStack { 251 RoundedRectangle(cornerRadius: radius, style: .continuous) 252 .frame(width: size, height: size).foregroundStyle(background) 253 .shadow(color: background.opacity(0.5), radius: 10, x: 0, y: 10) 254 let gradient = Gradient(colors: [ 255 color.opacity(INVISIBLE), 256 color, 257 color, 258 color.opacity(INVISIBLE)] 259 ) 260 let linearGradient = LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom) 261 let rotatingRect = Rectangle() 262 .frame(width: size*2, height: size/2) 263 .foregroundStyle(linearGradient) 264 .rotationEffect(.degrees(rotation)) 265 let border = lineWidth - 0.5 266 rotatingRect 267 .mask { 268 RoundedRectangle(cornerRadius: radius - lineWidth/2, style: .continuous) 269 .stroke(lineWidth: lineWidth) 270 .frame(width: size - border, height: size - border) 271 } 272 } 273 .frame(width: size, height: size) 274 Spacer() 275 } 276 .onAppear { 277 withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) { 278 rotation = 360 279 } 280 } 281 } 282 } 283 extension GradientBorder { 284 init(size: CGFloat, color: Color, background: Color) { 285 self.size = size 286 self.radius = 20.0 287 self.lineWidth = 4.0 288 self.color = color 289 self.background = background 290 } 291 } 292 // MARK: - 293 struct GradientBorder_Previews: PreviewProvider { 294 static var previews: some View { 295 296 ZStack { 297 GradientBorder(size: 260, 298 // radius: 1, 299 // lineWidth: 2, 300 color: .blue, 301 background: .yellow) 302 Text(verbatim: "Preview") 303 } 304 } 305 }