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