GradientBorder.swift (10711B)
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 size: CGFloat 130 let scanHints: (String, String)? 131 var content: () -> Content 132 133 @AppStorage("minimalistic") var minimalistic: Bool = false 134 @AppStorage("showQRauto16") var showQRauto16: Bool = true // iOS 16 doesn't have NFC emulation 135 @AppStorage("showQRauto17") var showQRauto17: Bool = false // but iOS 17 does 136 @State private var showTOTP = false 137 @ObservedObject var nfcWriter = NFCWriter() 138 139 var lockImage: Image { // "lock.badge.clock" 140 Image(ICONNAME_LOCKCLOCK, SYSTEM_LOCKCLOCK, fallback: FALLBACK_LOCK) 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 { // TODO: check for write capabilities 180 VStack { 181 if nfcHint { // QRCodeDetailView 182 if showTOTP { 183 if !minimalistic { 184 Text("\(nfcLogo) Tap for NFC") 185 .talerFont(.subheadline) 186 .padding(.vertical, -4) 187 } 188 // let screenWidth = UIScreen.screenWidth 189 GradientBorder(size: size + 20.0, 190 color: WalletColors().primaryAccent, 191 background: WalletColors().backgroundColor) 192 { 193 content() 194 .onTapGesture(count: 1) { nfcWriter.write(totpData) } 195 } 196 hint 197 } else { 198 NFCbutton(title: String(localized: "Write NFC")) { 199 nfcWriter.write(totpData) 200 } 201 totpButton.buttonStyle(TalerButtonStyle(type: .bordered)) 202 } 203 } 204 }.listRowSeparator(.hidden) 205 .onDisappear() { 206 207 }.task { 208 withAnimation { 209 if #available(iOS 17.7, *) { 210 showTOTP = showQRauto17 211 } else { 212 showTOTP = showQRauto16 213 } 214 } 215 } 216 } else { 217 if showTOTP { 218 content() 219 hint 220 } else { 221 totpButton.buttonStyle(TalerButtonStyle(type: .prominent)) 222 } 223 } 224 } 225 } 226 // MARK: - 227 // Use radius: 0 for rect instead of rounded rect 228 struct GradientBorder<Content: View>: View { 229 let size: CGFloat 230 let radius: CGFloat 231 let lineWidth: CGFloat 232 let color: Color 233 let background: Color 234 var content: () -> Content 235 236 @State var rotation: CGFloat = 0 237 238 var body: some View { 239 HStack { 240 Spacer() 241 ZStack { 242 RoundedRectangle(cornerRadius: radius, style: .continuous) 243 .frame(width: size, height: size).foregroundStyle(background) 244 .shadow(color: background.opacity(0.5), radius: 10, x: 0, y: 10) 245 let gradient = Gradient(colors: [ 246 color.opacity(INVISIBLE), 247 color, 248 color, 249 color.opacity(INVISIBLE)] 250 ) 251 let linearGradient = LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom) 252 let rotatingRect = Rectangle() 253 .frame(width: size*2, height: size/2) 254 .foregroundStyle(linearGradient) 255 .rotationEffect(.degrees(rotation)) 256 let border = lineWidth - 0.5 257 rotatingRect 258 .mask { 259 RoundedRectangle(cornerRadius: radius - lineWidth/2, style: .continuous) 260 .stroke(lineWidth: lineWidth) 261 .frame(width: size - border, height: size - border) 262 } 263 content() 264 } 265 .frame(width: size, height: size) 266 Spacer() 267 } 268 .onAppear { 269 withAnimation(.linear(duration: 8).repeatForever(autoreverses: false)) { 270 rotation = 720 271 } 272 } 273 } 274 } 275 extension GradientBorder { 276 init(size: CGFloat, radius: CGFloat = 20.0, lineWidth: CGFloat = 4.0, color: Color, background: Color, content: @escaping () -> Content) { 277 self.size = size 278 self.radius = radius 279 self.lineWidth = lineWidth 280 self.color = color 281 self.background = background 282 self.content = content 283 } 284 } 285 // MARK: - 286 struct GradientBorder_Previews: PreviewProvider { 287 static var previews: some View { 288 289 GradientBorder(size: 260, 290 // radius: 1, 291 // lineWidth: 2, 292 color: .blue, 293 background: .yellow) { 294 Text(verbatim: "Preview") 295 } 296 } 297 }