You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

812 lines
31 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import SwiftUI
import CoreData
import CoreImage
// MARK: -
struct CreateQRCodeView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var coreDataManager: CoreDataManager
//
let selectedQRCodeType: QRCodeType
//
@State private var content = ""
@FocusState private var isContentFieldFocused: Bool
// Email
@State private var emailAddress = ""
@State private var emailSubject = ""
@State private var emailBody = ""
@State private var emailCc = ""
@State private var emailBcc = ""
@FocusState private var focusedEmailField: EmailInputView.EmailField?
// WiFi
@State private var wifiSSID = ""
@State private var wifiPassword = ""
@State private var wifiEncryptionType: WiFiInputView.WiFiEncryptionType = .wpa2
@FocusState private var focusedWiFiField: WiFiInputView.WiFiField?
//
@State private var contactFirstName = ""
@State private var contactLastName = ""
@State private var contactPhone = ""
@State private var contactEmail = ""
@State private var contactCompany = ""
@State private var contactTitle = ""
@State private var contactAddress = ""
@State private var contactWebsite = ""
@State private var contactNickname = ""
@State private var contactBirthday = Date()
@State private var contactNote = ""
@FocusState private var focusedContactField: ContactInputView.ContactField?
//
@State private var locationLatitude = ""
@State private var locationLongitude = ""
@State private var locationName = ""
@FocusState private var focusedLocationField: LocationInputView.LocationField?
//
@State private var eventTitle = ""
@State private var eventDescription = ""
@State private var eventLocation = ""
@State private var startDate = Date()
@State private var endDate = Date().addingTimeInterval(3600)
@FocusState private var focusedCalendarField: CalendarInputView.CalendarField?
//
@State private var socialUsername = ""
@State private var socialMessage = ""
@FocusState private var focusedSocialField: SocialInputView.SocialField?
//
@State private var phoneNumber = ""
@State private var phoneMessage = ""
@FocusState private var focusedPhoneField: PhoneInputView.PhoneField?
// URL
@State private var urlString = ""
@FocusState private var isURLFieldFocused: Bool
//
@State private var showingAlert = false
@State private var alertMessage = ""
@State private var navigateToStyleView = false
var body: some View {
VStack(spacing: 0) {
inputAndPreviewSection
}
.navigationTitle(selectedQRCodeType.displayName)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(NSLocalizedString("create", comment: "Create")) {
if canCreateQRCode() {
navigateToStyleView = true
}
}
.disabled(!canCreateQRCode())
.font(.system(size: 16, weight: .semibold))
}
}
.alert(NSLocalizedString("tip", comment: "Tip"), isPresented: $showingAlert) {
Button(NSLocalizedString("confirm", comment: "Confirm")) { }
} message: { Text(alertMessage) }
.background(
NavigationLink(
destination: QRCodeStyleView(qrCodeContent: generateQRCodeContent(), qrCodeType: selectedQRCodeType, existingStyleData: nil, historyItem: nil),
isActive: $navigateToStyleView
) {
EmptyView()
}
)
.onAppear {
setupInitialFocus()
}
.onTapGesture {
hideKeyboard()
}
}
// MARK: - UI Components
private var inputAndPreviewSection: some View {
ScrollView {
VStack(spacing: 24) {
// Content input area
VStack(spacing: 16) {
// Use InputComponentFactory to dynamically select input component
createInputComponentForType()
.padding(.horizontal, 20)
}
// Preview area
#if DEBUG
if canCreateQRCode() {
VStack(spacing: 16) {
// 使QRCodePreviewView
QRCodePreviewView(
qrCodeImage: generateQRCodeImage(),
formattedContent: formatContentForQRCodeType(),
qrCodeType: selectedQRCodeType
)
.padding(.horizontal, 20)
}
}
Spacer(minLength: 100)
#endif
}
.padding(.top, 20)
}
.background(Color(.systemGroupedBackground))
}
// MARK: - Helper Methods
private func createInputComponentForType() -> AnyView {
switch selectedQRCodeType {
case .mail:
let emailConfig = EmailInputConfig(
emailAddress: $emailAddress,
emailSubject: $emailSubject,
emailBody: $emailBody,
emailCc: $emailCc,
emailBcc: $emailBcc
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
emailConfig: emailConfig
)
case .wifi:
let wifiConfig = WiFiInputConfig(
ssid: $wifiSSID,
password: $wifiPassword,
encryptionType: $wifiEncryptionType
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
wifiConfig: wifiConfig
)
case .vcard, .mecard:
let contactConfig = ContactInputConfig(
firstName: $contactFirstName,
lastName: $contactLastName,
phone: $contactPhone,
email: $contactEmail,
company: $contactCompany,
title: $contactTitle,
address: $contactAddress,
website: $contactWebsite,
nickname: $contactNickname,
birthday: $contactBirthday,
note: $contactNote
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
contactConfig: contactConfig
)
case .location:
let locationConfig = LocationInputConfig(
latitude: $locationLatitude,
longitude: $locationLongitude,
locationName: $locationName
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
locationConfig: locationConfig
)
case .calendar:
let calendarConfig = CalendarInputConfig(
eventTitle: $eventTitle,
eventDescription: $eventDescription,
startDate: $startDate,
endDate: $endDate,
location: $eventLocation
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
calendarConfig: calendarConfig
)
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber:
let socialConfig = SocialInputConfig(
username: $socialUsername,
message: $socialMessage
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
socialConfig: socialConfig
)
case .phone, .sms:
let phoneConfig = PhoneInputConfig(
phoneNumber: $phoneNumber,
phoneMessage: $phoneMessage
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
phoneConfig: phoneConfig
)
case .url:
let urlConfig = URLInputConfig(
url: $urlString
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
urlConfig: urlConfig
)
default:
let textConfig = TextInputConfig(
content: $content
)
return InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
textConfig: textConfig
)
}
}
private func setupInitialFocus() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
switch selectedQRCodeType {
case .mail:
focusedEmailField = .address
case .wifi:
focusedWiFiField = .ssid
case .vcard, .mecard:
focusedContactField = .firstName
case .location:
focusedLocationField = .latitude
case .calendar:
focusedCalendarField = .title
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok:
focusedSocialField = .username
case .phone, .sms:
focusedPhoneField = .phoneNumber
case .url:
isURLFieldFocused = true
default:
isContentFieldFocused = true
}
}
}
private func hideKeyboard() {
switch selectedQRCodeType {
case .mail:
focusedEmailField = nil
case .wifi:
focusedWiFiField = nil
case .vcard, .mecard:
focusedContactField = nil
case .location:
focusedLocationField = nil
case .calendar:
focusedCalendarField = nil
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok:
focusedSocialField = nil
case .phone, .sms:
focusedPhoneField = nil
case .url:
isURLFieldFocused = false
default:
isContentFieldFocused = false
}
}
private func getInputIcon() -> String {
switch selectedQRCodeType {
case .mail: return "envelope"
case .wifi: return "wifi"
case .vcard, .mecard: return "person"
case .location: return "location"
case .calendar: return "calendar"
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber: return "globe"
case .phone, .sms: return "phone"
case .url: return "link"
default: return "textformat"
}
}
private func canCreateQRCode() -> Bool {
switch selectedQRCodeType {
case .mail:
return !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty
case .wifi:
return !wifiSSID.isEmpty
case .vcard, .mecard:
return !contactFirstName.isEmpty || !contactLastName.isEmpty
case .location:
return !locationLatitude.isEmpty && !locationLongitude.isEmpty
case .calendar:
return !eventTitle.isEmpty
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber:
return !socialUsername.isEmpty
case .phone, .sms:
return !phoneNumber.isEmpty
case .url:
return !urlString.isEmpty
default:
return !content.isEmpty
}
}
private func getContentHint() -> String {
InputComponentFactory.getPlaceholderText(for: selectedQRCodeType)
}
private func generateQRCodeImage() -> UIImage? {
guard canCreateQRCode() else { return nil }
let formattedContent = formatContentForQRCodeType()
let data = formattedContent.data(using: .utf8)
let qrFilter = CIFilter.qrCodeGenerator()
qrFilter.setValue(data, forKey: "inputMessage")
qrFilter.setValue("H", forKey: "inputCorrectionLevel")
guard let outputImage = qrFilter.outputImage else { return nil }
let context = CIContext()
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { return nil }
return UIImage(cgImage: cgImage)
}
private func formatContentForQRCodeType() -> String {
switch selectedQRCodeType {
case .text:
return content
case .url:
return urlString.hasPrefix("http") ? urlString : "https://\(urlString)"
case .mail:
var mailtoURL = "mailto:\(emailAddress)"
var queryParams: [String] = []
if !emailSubject.isEmpty {
queryParams.append("subject=\(emailSubject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? emailSubject)")
}
if !emailBody.isEmpty {
queryParams.append("body=\(emailBody.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? emailBody)")
}
if !emailCc.isEmpty {
queryParams.append("cc=\(emailCc)")
}
if !emailBcc.isEmpty {
queryParams.append("bcc=\(emailBcc)")
}
if !queryParams.isEmpty {
mailtoURL += "?" + queryParams.joined(separator: "&")
}
return mailtoURL
case .phone:
return "tel:\(phoneNumber)"
case .sms:
let smsContent = phoneMessage.isEmpty ? "Hello" : phoneMessage
return "SMSTO:\(phoneNumber):\(smsContent)"
case .wifi:
return "WIFI:T:\(wifiEncryptionType.rawValue);S:\(wifiSSID);P:\(wifiPassword);;"
case .vcard:
var vcard = "BEGIN:VCARD\nVERSION:3.0\n"
// (NFN)
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
let lastName = contactLastName.isEmpty ? "" : contactLastName
let firstName = contactFirstName.isEmpty ? "" : contactFirstName
vcard += "N:\(lastName);\(firstName);;;\n"
vcard += "FN:\(firstName) \(lastName)\n"
}
//
if !contactPhone.isEmpty {
vcard += "TEL;TYPE=WORK,CELL:\(contactPhone)\n"
}
//
if !contactEmail.isEmpty {
vcard += "EMAIL;TYPE=PREF,INTERNET:\(contactEmail)\n"
}
//
if !contactCompany.isEmpty {
vcard += "ORG:\(contactCompany)\n"
}
//
if !contactTitle.isEmpty {
vcard += "TITLE:\(contactTitle)\n"
}
//
if !contactAddress.isEmpty {
vcard += "ADR;TYPE=WORK:;;\(contactAddress);;;;\n"
}
//
if !contactWebsite.isEmpty {
vcard += "URL:\(contactWebsite)\n"
}
vcard += "END:VCARD"
return vcard
case .mecard:
var mecard = "MECARD:"
//
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
let lastName = contactLastName.isEmpty ? "" : contactLastName
let firstName = contactFirstName.isEmpty ? "" : contactFirstName
mecard += "N:\(lastName),\(firstName);"
}
//
if !contactNickname.isEmpty {
mecard += "NICKNAME:\(contactNickname);"
}
//
if !contactPhone.isEmpty {
mecard += "TEL:\(contactPhone);"
}
//
if !contactEmail.isEmpty {
mecard += "EMAIL:\(contactEmail);"
}
//
if !contactCompany.isEmpty {
mecard += "ORG:\(contactCompany);"
}
//
if !contactAddress.isEmpty {
mecard += "ADR:\(contactAddress);"
}
//
if !contactWebsite.isEmpty {
mecard += "URL:\(contactWebsite);"
}
//
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd"
let birthdayString = dateFormatter.string(from: contactBirthday)
mecard += "BDAY:\(birthdayString);"
//
if !contactNote.isEmpty {
mecard += "NOTE:\(contactNote);"
}
mecard += ";"
return mecard
case .location:
let coords = "\(locationLatitude),\(locationLongitude)"
return locationName.isEmpty ? "geo:\(coords)" : "geo:\(coords)?q=\(locationName)"
case .calendar:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
var ical = "BEGIN:VEVENT\n"
ical += "SUMMARY:\(eventTitle)\n"
if !eventDescription.isEmpty {
ical += "DESCRIPTION:\(eventDescription)\n"
}
if !eventLocation.isEmpty {
ical += "LOCATION:\(eventLocation)\n"
}
ical += "DTSTART:\(dateFormatter.string(from: startDate))\n"
ical += "DTEND:\(dateFormatter.string(from: endDate))\n"
ical += "END:VEVENT"
return ical
case .instagram:
return "instagram://user?username=\(socialUsername)"
case .facebook:
// Facebook/ID
let facebookId = extractFacebookId(from: socialUsername)
return "fb://profile/\(facebookId)"
case .spotify:
return "spotify:search:\(socialUsername);\(socialMessage)"
case .twitter:
return "twitter://user?screen_name=\(socialUsername)"
case .whatsapp:
return "whatsapp://send?phone=\(socialUsername)"
case .viber:
return "viber://add?number=\(socialUsername)"
case .snapchat:
return "https://snapchat.com/add/\(socialUsername)"
case .tiktok:
return "https://www.tiktok.com/@\(socialUsername)"
}
}
// MARK: -
private func generateSocialMediaContent() -> String {
switch selectedQRCodeType {
case .instagram:
return "instagram://user?username=\(socialUsername)"
case .facebook:
// Facebook/ID
let facebookId = extractFacebookId(from: socialUsername)
return "fb://profile/\(facebookId)"
case .spotify:
return "spotify:search:\(socialUsername);\(socialMessage)"
case .twitter:
return "twitter://user?screen_name=\(socialUsername)"
case .whatsapp:
return "whatsapp://send?phone=\(socialUsername)"
case .viber:
return "viber://add?number=\(socialUsername)"
case .snapchat:
return "https://snapchat.com/add/\(socialUsername)"
case .tiktok:
return "https://www.tiktok.com/@\(socialUsername)"
default:
return socialUsername
}
}
private func createQRCode() {
let context = coreDataManager.container.viewContext
let historyItem = HistoryItem(context: context)
historyItem.id = UUID()
historyItem.dataType = DataType.qrcode.rawValue
historyItem.dataSource = DataSource.created.rawValue
historyItem.createdAt = Date()
historyItem.isFavorite = false
historyItem.qrCodeType = selectedQRCodeType.rawValue
//
switch selectedQRCodeType {
case .mail:
var mailContent = String(format: NSLocalizedString("email_content_format", comment: "Email content format"), emailAddress, emailSubject, emailBody)
if !emailCc.isEmpty {
mailContent += String(format: NSLocalizedString("email_cc_format", comment: "Email CC format"), emailCc)
}
if !emailBcc.isEmpty {
mailContent += String(format: NSLocalizedString("email_bcc_format", comment: "Email BCC format"), emailBcc)
}
historyItem.content = mailContent
case .wifi:
historyItem.content = String(format: NSLocalizedString("wifi_content_format", comment: "WiFi content format"), wifiSSID, wifiEncryptionType.displayName)
case .vcard, .mecard:
var contactContent = NSLocalizedString("contact_content_prefix", comment: "Contact content prefix")
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
contactContent += "\(contactFirstName) \(contactLastName)"
}
if !contactNickname.isEmpty {
contactContent += String(format: NSLocalizedString("contact_nickname_format", comment: "Contact nickname format"), contactNickname)
}
if !contactPhone.isEmpty {
contactContent += String(format: NSLocalizedString("contact_phone_format", comment: "Contact phone format"), contactPhone)
}
if !contactEmail.isEmpty {
contactContent += String(format: NSLocalizedString("contact_email_format", comment: "Contact email format"), contactEmail)
}
if !contactCompany.isEmpty {
contactContent += String(format: NSLocalizedString("contact_company_format", comment: "Contact company format"), contactCompany)
}
if !contactTitle.isEmpty {
contactContent += String(format: NSLocalizedString("contact_title_format", comment: "Contact title format"), contactTitle)
}
if !contactAddress.isEmpty {
contactContent += String(format: NSLocalizedString("contact_address_format", comment: "Contact address format"), contactAddress)
}
if !contactWebsite.isEmpty {
contactContent += String(format: NSLocalizedString("contact_website_format", comment: "Contact website format"), contactWebsite)
}
if !contactNote.isEmpty {
contactContent += String(format: NSLocalizedString("contact_note_format", comment: "Contact note format"), contactNote)
}
historyItem.content = contactContent
case .location:
historyItem.content = String(format: NSLocalizedString("location_content_format", comment: "Location content format"), locationLatitude, locationLongitude)
case .calendar:
historyItem.content = String(format: NSLocalizedString("calendar_content_format", comment: "Calendar content format"), eventTitle)
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber:
historyItem.content = generateSocialMediaContent()
case .phone, .sms:
historyItem.content = String(format: NSLocalizedString("phone_content_format", comment: "Phone content format"), phoneNumber)
case .url:
historyItem.content = String(format: NSLocalizedString("url_content_format", comment: "URL content format"), urlString)
default:
historyItem.content = content
}
do {
try context.save()
alertMessage = NSLocalizedString("qrcode_created_successfully", comment: "QR code created successfully")
showingAlert = true
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
dismiss()
}
} catch {
alertMessage = String(format: NSLocalizedString("save_failed_error", comment: "Save failed with error"), error.localizedDescription)
showingAlert = true
}
}
// MARK: -
private func generateQRCodeContent() -> String {
switch selectedQRCodeType {
case .mail:
var mailContent = "mailto:\(emailAddress)"
if !emailSubject.isEmpty || !emailBody.isEmpty || !emailCc.isEmpty || !emailBcc.isEmpty {
mailContent += "?"
var params: [String] = []
if !emailSubject.isEmpty {
params.append("subject=\(emailSubject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")")
}
if !emailBody.isEmpty {
params.append("body=\(emailBody.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")")
}
if !emailCc.isEmpty {
params.append("cc=\(emailCc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")")
}
if !emailBcc.isEmpty {
params.append("bcc=\(emailBcc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")")
}
mailContent += params.joined(separator: "&")
}
return mailContent
case .wifi:
var wifiContent = "WIFI:"
wifiContent += "S:\(wifiSSID);"
wifiContent += "T:\(wifiEncryptionType.rawValue.uppercased());"
if !wifiPassword.isEmpty {
wifiContent += "P:\(wifiPassword);"
}
wifiContent += ";"
return wifiContent
case .vcard:
var vcardContent = "BEGIN:VCARD\nVERSION:3.0\n"
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
vcardContent += "N:\(contactLastName);\(contactFirstName);;;\n"
vcardContent += "FN:\(contactFirstName) \(contactLastName)\n"
}
if !contactPhone.isEmpty {
vcardContent += "TEL;TYPE=WORK,PREF:\(contactPhone)\n"
}
if !contactEmail.isEmpty {
vcardContent += "EMAIL:\(contactEmail)\n"
}
if !contactCompany.isEmpty {
vcardContent += "ORG:\(contactCompany)\n"
}
if !contactTitle.isEmpty {
vcardContent += "TITLE:\(contactTitle)\n"
}
if !contactAddress.isEmpty {
vcardContent += "ADR;TYPE=WORK:;;\(contactAddress);;;;\n"
}
if !contactWebsite.isEmpty {
vcardContent += "URL:\(contactWebsite)\n"
}
vcardContent += "END:VCARD"
return vcardContent
case .mecard:
var mecardContent = "MECARD:"
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
mecardContent += "N:\(contactLastName),\(contactFirstName);"
}
if !contactNickname.isEmpty {
mecardContent += "NICKNAME:\(contactNickname);"
}
if !contactPhone.isEmpty {
mecardContent += "TEL:\(contactPhone);"
}
if !contactEmail.isEmpty {
mecardContent += "EMAIL:\(contactEmail);"
}
if !contactCompany.isEmpty {
mecardContent += "ORG:\(contactCompany);"
}
if !contactTitle.isEmpty {
mecardContent += "TITLE:\(contactTitle);"
}
if !contactAddress.isEmpty {
mecardContent += "ADR:,,\(contactAddress);"
}
if !contactWebsite.isEmpty {
mecardContent += "URL:\(contactWebsite);"
}
if !contactNote.isEmpty {
mecardContent += "NOTE:\(contactNote);"
}
mecardContent += ";"
return mecardContent
case .location:
return "geo:\(locationLatitude),\(locationLongitude)"
case .calendar:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
let startDateString = dateFormatter.string(from: startDate)
let endDateString = dateFormatter.string(from: endDate)
var icalContent = "BEGIN:VEVENT\n"
icalContent += "SUMMARY:\(eventTitle)\n"
if !eventDescription.isEmpty {
icalContent += "DESCRIPTION:\(eventDescription)\n"
}
if !eventLocation.isEmpty {
icalContent += "LOCATION:\(eventLocation)\n"
}
icalContent += "DTSTART:\(startDateString)\n"
icalContent += "DTEND:\(endDateString)\n"
icalContent += "END:VEVENT"
return icalContent
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber:
return generateSocialMediaContent()
case .phone:
return "tel:\(phoneNumber)"
case .sms:
return "SMSTO:\(phoneNumber)"
case .url:
return urlString
default:
return content
}
}
// MARK: - Facebook ID
private func extractFacebookId(from input: String) -> String {
// Facebook/ID
if input.hasPrefix("http") {
// Facebook
let patterns = [
"https://www.facebook.com/",
"https://facebook.com/",
"http://www.facebook.com/",
"http://facebook.com/"
]
var cleanedInput = input
for pattern in patterns {
if cleanedInput.hasPrefix(pattern) {
cleanedInput = String(cleanedInput.dropFirst(pattern.count))
break
}
}
//
if let questionMarkIndex = cleanedInput.firstIndex(of: "?") {
cleanedInput = String(cleanedInput[..<questionMarkIndex])
}
if let slashIndex = cleanedInput.firstIndex(of: "/") {
cleanedInput = String(cleanedInput[..<slashIndex])
}
return cleanedInput.isEmpty ? "unknown" : cleanedInput
}
// ID
return input
}
}
#Preview {
NavigationView {
CreateQRCodeView(selectedQRCodeType: .spotify)
}
}