GradientBorder.swift (11277B)
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: "\n") 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 // let screenWidth = UIScreen.screenWidth 200 GradientBorder(size: size + 20.0, 201 color: WalletColors().primaryAccent, 202 background: WalletColors().backgroundColor) 203 { 204 content() 205 .onTapGesture(count: 1) { nfcWriter.write(totpData) } 206 } 207 hint 208 } else { 209 NFCbutton(title: String(localized: "Write NFC")) { 210 nfcWriter.write(totpData) 211 } 212 totpButton.buttonStyle(TalerButtonStyle(type: .bordered)) 213 } 214 } 215 }.listRowSeparator(.hidden) 216 .onDisappear() { 217 218 }.task { 219 withAnimation { 220 if #available(iOS 17.7, *) { 221 showTOTP = showQRauto17 222 } else { 223 showTOTP = showQRauto16 224 } 225 } 226 } 227 } else { 228 if showTOTP { 229 content() 230 hint 231 } else { 232 totpButton.buttonStyle(TalerButtonStyle(type: .prominent)) 233 } 234 } 235 } 236 } 237 // MARK: - 238 // Use radius: 0 for rect instead of rounded rect 239 struct GradientBorder<Content: View>: View { 240 let size: CGFloat 241 let radius: CGFloat 242 let lineWidth: CGFloat 243 let color: Color 244 let background: Color 245 var content: () -> Content 246 247 @State var rotation: CGFloat = 0 248 249 var body: some View { 250 HStack { 251 Spacer() 252 ZStack { 253 RoundedRectangle(cornerRadius: radius, style: .continuous) 254 .frame(width: size, height: size).foregroundStyle(background) 255 .shadow(color: background.opacity(0.5), radius: 10, x: 0, y: 10) 256 let gradient = Gradient(colors: [ 257 color.opacity(INVISIBLE), 258 color, 259 color, 260 color.opacity(INVISIBLE)] 261 ) 262 let linearGradient = LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom) 263 let rotatingRect = Rectangle() 264 .frame(width: size*2, height: size/2) 265 .foregroundStyle(linearGradient) 266 .rotationEffect(.degrees(rotation)) 267 let border = lineWidth - 0.5 268 rotatingRect 269 .mask { 270 RoundedRectangle(cornerRadius: radius - lineWidth/2, style: .continuous) 271 .stroke(lineWidth: lineWidth) 272 .frame(width: size - border, height: size - border) 273 } 274 content() 275 } 276 .frame(width: size, height: size) 277 Spacer() 278 } 279 .onAppear { 280 withAnimation(.linear(duration: 8).repeatForever(autoreverses: false)) { 281 rotation = 720 282 } 283 } 284 } 285 } 286 extension GradientBorder { 287 init(size: CGFloat, radius: CGFloat = 20.0, lineWidth: CGFloat = 4.0, color: Color, background: Color, content: @escaping () -> Content) { 288 self.size = size 289 self.radius = radius 290 self.lineWidth = lineWidth 291 self.color = color 292 self.background = background 293 self.content = content 294 } 295 } 296 // MARK: - 297 struct GradientBorder_Previews: PreviewProvider { 298 static var previews: some View { 299 300 GradientBorder(size: 260, 301 // radius: 1, 302 // lineWidth: 2, 303 color: .blue, 304 background: .yellow) { 305 Text(verbatim: "Preview") 306 } 307 } 308 }