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.

1400 lines
53 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 QRCode
import NetworkExtension
internal import SwiftImageReadWrite
struct QRCodeDetailView: View {
let historyItem: HistoryItem
@EnvironmentObject var coreDataManager: CoreDataManager
@State private var qrCodeImage: UIImage?
@State private var showingShareSheet = false
@State private var showingAlert = false
@State private var alertMessage = ""
@State private var navigateToStyleView = false
var body: some View {
ScrollView {
VStack(spacing: 20) {
//
parsedInfoSection
#if DEBUG
//
originalContentSection
#endif
//
actionButtonsSection
// Decorate code
decorateCodeButton
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
.navigationTitle(getNavigationTitle())
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showingShareSheet = true
}) {
Image(systemName: "square.and.arrow.up")
}
}
}
.onAppear {
generateQRCodeImage()
}
.sheet(isPresented: $showingShareSheet) {
ShareSheet(activityItems: [historyItem.content ?? ""])
}
.alert("tip".localized, isPresented: $showingAlert) {
Button("confirm".localized) { }
} message: {
Text(alertMessage)
}
.background(
NavigationLink(
destination: QRCodeStyleView(
qrCodeContent: historyItem.content ?? "",
qrCodeType: getQRCodeType(),
existingStyleData: getStyleData(),
historyItem: historyItem
),
isActive: $navigateToStyleView
) {
EmptyView()
}
)
}
// MARK: -
private var qrCodeImageView: some View {
VStack(spacing: 16) {
if let qrCodeImage = qrCodeImage {
Image(uiImage: qrCodeImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.cornerRadius(12)
.shadow(radius: 8)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 200, height: 200)
.overlay(
ProgressView()
.scaleEffect(1.5)
)
}
Text("scan_this_qr_code".localized)
.font(.caption)
.foregroundColor(.secondary)
}
}
// MARK: -
private var parsedInfoSection: some View {
VStack(alignment: .leading, spacing: 12) {
if let content = historyItem.content {
let parsedData = QRCodeParser.parseQRCode(content)
HStack {
Image(systemName: parsedData.icon)
.font(.title3)
.foregroundColor(.green)
Text(parsedData.title)
.font(.title3)
.fontWeight(.medium)
Spacer()
}
//
if parsedData.type == .vcard || parsedData.type == .mecard {
//
contactInfoDetailView(parsedData: parsedData)
} else if parsedData.type == .mail {
// Email
emailInfoDetailView(parsedData: parsedData)
} else if parsedData.type == .calendar {
// Calendar
calendarInfoDetailView(parsedData: parsedData)
} else if parsedData.type == .sms {
// SMS
smsInfoDetailView(parsedData: parsedData)
} else {
//
VStack(alignment: .leading, spacing: 8) {
if let subtitle = parsedData.subtitle {
Text(subtitle)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding()
.background(Color.green.opacity(0.1))
.cornerRadius(8)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
// MARK: -
private func contactInfoDetailView(parsedData: ParsedQRData) -> some View {
VStack(alignment: .leading, spacing: 12) {
if let contactInfo = getContactInfoFromParsedData(parsedData) {
//
if !contactInfo.name.isEmpty {
contactInfoRow(icon: "person.fill", title: "name".localized, value: contactInfo.name)
}
//
if !contactInfo.phoneNumber.isEmpty {
contactInfoRow(icon: "phone.fill", title: "phone".localized, value: contactInfo.phoneNumber)
}
//
if !contactInfo.email.isEmpty {
contactInfoRow(icon: "envelope.fill", title: "email".localized, value: contactInfo.email)
}
//
if !contactInfo.organization.isEmpty {
contactInfoRow(icon: "building.2.fill", title: "organization".localized, value: contactInfo.organization)
}
//
if !contactInfo.title.isEmpty {
contactInfoRow(icon: "briefcase.fill", title: "title".localized, value: contactInfo.title)
}
//
if !contactInfo.address.isEmpty {
contactInfoRow(icon: "location.fill", title: "address".localized, value: contactInfo.address)
}
} else {
//
VStack(alignment: .leading, spacing: 8) {
if let subtitle = parsedData.subtitle {
Text(subtitle)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding()
.background(Color.green.opacity(0.1))
.cornerRadius(8)
}
}
}
// MARK: - Email
private func emailInfoDetailView(parsedData: ParsedQRData) -> some View {
VStack(alignment: .leading, spacing: 12) {
if let emailDetails = getEmailDetails(parsedData: parsedData) {
//
if !emailDetails.emailAddress.isEmpty {
contactInfoRow(icon: "envelope.fill", title: "email_address".localized, value: emailDetails.emailAddress)
}
//
if !emailDetails.subject.isEmpty {
contactInfoRow(icon: "text.bubble.fill", title: "email_subject".localized, value: emailDetails.subject)
}
//
if !emailDetails.body.isEmpty {
contactInfoRow(icon: "doc.text.fill", title: "email_body".localized, value: emailDetails.body)
}
} else {
// Email
VStack(alignment: .leading, spacing: 8) {
if let subtitle = parsedData.subtitle {
Text(subtitle)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
}
}
// MARK: - Calendar
private func calendarInfoDetailView(parsedData: ParsedQRData) -> some View {
VStack(alignment: .leading, spacing: 12) {
if let calendarDetails = getCalendarDetails(parsedData: parsedData) {
//
if !calendarDetails.summary.isEmpty {
contactInfoRow(icon: "calendar.badge.plus", title: "calendar_event_title".localized, value: calendarDetails.summary)
}
//
if !calendarDetails.startTime.isEmpty {
let formattedStartTime = formatCalendarTime(calendarDetails.startTime)
contactInfoRow(icon: "clock.fill", title: "calendar_start_time".localized, value: formattedStartTime)
}
//
if !calendarDetails.endTime.isEmpty {
let formattedEndTime = formatCalendarTime(calendarDetails.endTime)
contactInfoRow(icon: "clock.badge.checkmark.fill", title: "calendar_end_time".localized, value: formattedEndTime)
}
//
if !calendarDetails.location.isEmpty {
contactInfoRow(icon: "location.fill", title: "calendar_location".localized, value: calendarDetails.location)
}
//
if !calendarDetails.description.isEmpty {
contactInfoRow(icon: "text.bubble.fill", title: "calendar_description".localized, value: calendarDetails.description)
}
} else {
// Calendar
VStack(alignment: .leading, spacing: 8) {
if let subtitle = parsedData.subtitle {
Text(subtitle)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding()
.background(Color.orange.opacity(0.1))
.cornerRadius(8)
}
}
}
// MARK: - SMS
private func smsInfoDetailView(parsedData: ParsedQRData) -> some View {
VStack(alignment: .leading, spacing: 12) {
if let smsDetails = getSMSDetails(parsedData: parsedData) {
//
if !smsDetails.phoneNumber.isEmpty {
contactInfoRow(icon: "phone.fill", title: "sms_phone_number".localized, value: smsDetails.phoneNumber)
}
//
if !smsDetails.message.isEmpty {
contactInfoRow(icon: "message.fill", title: "sms_message".localized, value: smsDetails.message)
}
} else {
// SMS
VStack(alignment: .leading, spacing: 8) {
if let subtitle = parsedData.subtitle {
Text(subtitle)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding()
.background(Color.purple.opacity(0.1))
.cornerRadius(8)
}
}
}
// MARK: -
private func contactInfoRow(icon: String, title: String, value: String) -> some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: icon)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.blue)
.frame(width: 20, height: 20)
.padding(.top, 2)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.caption)
.foregroundColor(.secondary)
.textCase(.uppercase)
Text(value)
.font(.body)
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
}
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 16)
.background(Color(.systemGray6))
.cornerRadius(8)
}
// MARK: -
private var originalContentSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "doc.text")
.font(.title2)
.foregroundColor(.purple)
Text("original_content".localized)
.font(.headline)
Spacer()
}
if let content = historyItem.content {
ScrollView {
Text(content)
.font(.system(.body, design: .monospaced))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxHeight: 200)
.background(Color.purple.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
// MARK: -
private var qrCodeStyleSection: some View {
VStack(spacing: 0) {
if let styleData = getStyleData() {
// 使
VStack(spacing: 16) {
//
if let previewImage = generateStylePreviewImage(styleData: styleData) {
Image(uiImage: previewImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 200, maxHeight: 200)
.padding()
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 4)
}
//
HStack(spacing: 8) {
Label("custom".localized, systemImage: "paintpalette")
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.purple.opacity(0.2))
.foregroundColor(.purple)
.cornerRadius(6)
}
}
.padding()
.background(Color.purple.opacity(0.05))
.cornerRadius(12)
} else {
//
VStack(spacing: 16) {
if let standardImage = generateStandardQRCodeImage() {
Image(uiImage: standardImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 200, maxHeight: 200)
.padding()
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 4)
}
Label("standard".localized, systemImage: "qrcode")
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.gray.opacity(0.2))
.foregroundColor(.gray)
.cornerRadius(6)
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(12)
}
}
.padding()
}
// MARK: -
private func actionButton(icon: String, title: String, color: Color, action: @escaping () -> Void) -> some View {
Button(action: action) {
VStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(color)
Text(title)
.font(.caption)
.foregroundColor(color)
.lineLimit(2)
.multilineTextAlignment(.center)
}
.frame(width: 70, height: 70)
.background(color.opacity(0.08))
.cornerRadius(16)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(color.opacity(0.2), lineWidth: 1)
)
}
.buttonStyle(PlainButtonStyle())
}
// MARK: -
private var actionButtonsSection: some View {
Group {
if let content = historyItem.content {
let parsedData = QRCodeParser.parseQRCode(content)
// 使
FlowLayoutView(spacing: 20) {
//
actionButton(
icon: historyItem.isFavorite ? "heart.fill" : "heart",
title: "favorite".localized,
color: historyItem.isFavorite ? .red : .gray,
action: toggleFavorite
)
actionButton(
icon: "doc.on.doc",
title: "copy".localized,
color: .blue,
action: copyContent
)
// QR
if parsedData.type == .wifi {
actionButton(
icon: "doc.on.doc.fill",
title: "copy_password".localized,
color: .orange,
action: copyWiFiPassword
)
actionButton(
icon: "link",
title: "connect_wifi".localized,
color: .purple,
action: setupWiFi
)
} else if parsedData.type == .vcard || parsedData.type == .mecard {
let contactInfo = getContactInfoFromParsedData(parsedData)
actionButton(
icon: "person.badge.plus",
title: "add_contact".localized,
color: .blue,
action: addContact
)
if let contactInfo = contactInfo, contactInfo.hasPhoneNumber {
actionButton(
icon: "phone",
title: "call".localized,
color: .green,
action: { makePhoneCall(phoneNumber: contactInfo.phoneNumber) }
)
actionButton(
icon: "message",
title: "send_sms".localized,
color: .purple,
action: { sendSMS(phoneNumber: contactInfo.phoneNumber) }
)
}
} else if parsedData.type == .phone {
// URL
let phoneNumber = content.replacingOccurrences(of: "tel:", with: "", options: .caseInsensitive)
if !phoneNumber.isEmpty {
actionButton(
icon: "phone",
title: "call".localized,
color: .green,
action: { makePhoneCall(phoneNumber: phoneNumber) }
)
actionButton(
icon: "message",
title: "send_sms".localized,
color: .purple,
action: { sendSMS(phoneNumber: phoneNumber) }
)
}
} else if parsedData.type == .mail {
// URL
let emailAddress = content.replacingOccurrences(of: "mailto:", with: "", options: .caseInsensitive)
if !emailAddress.isEmpty {
actionButton(
icon: "envelope",
title: "send_email".localized,
color: .blue,
action: { sendEmail(emailAddress: emailAddress) }
)
}
} else if parsedData.type == .sms {
let smsDetails = getSMSDetails()
if let smsDetails = smsDetails, !smsDetails.phoneNumber.isEmpty {
actionButton(
icon: "message",
title: "send_sms".localized,
color: .purple,
action: { sendSMS(phoneNumber: smsDetails.phoneNumber, message: smsDetails.message) }
)
}
} else if parsedData.type == .calendar {
actionButton(
icon: "calendar.badge.plus",
title: "add_to_calendar".localized,
color: .orange,
action: addEventToCalendar
)
} else if parsedData.type == .instagram {
actionButton(
icon: "camera",
title: "open".localized,
color: .purple,
action: { openSocialApp(content: content, appType: .instagram) }
)
} else if parsedData.type == .facebook {
actionButton(
icon: "person.2",
title: "open".localized,
color: .blue,
action: { openSocialApp(content: content, appType: .facebook) }
)
} else if parsedData.type == .twitter {
actionButton(
icon: "bird",
title: "open".localized,
color: .black,
action: { openSocialApp(content: content, appType: .twitter) }
)
} else if parsedData.type == .whatsapp {
actionButton(
icon: "message.circle",
title: "open".localized,
color: .green,
action: { openSocialApp(content: content, appType: .whatsapp) }
)
} else if parsedData.type == .viber {
actionButton(
icon: "message.circle.fill",
title: "open".localized,
color: .purple,
action: { openSocialApp(content: content, appType: .viber) }
)
} else if parsedData.type == .spotify {
actionButton(
icon: "music.note",
title: "open".localized,
color: .green,
action: { openSocialApp(content: content, appType: .spotify) }
)
} else if canOpenURL(content) {
actionButton(
icon: "arrow.up.right.square",
title: "open_link".localized,
color: .green,
action: { openURL(content) }
)
}
}
} else {
//
FlowLayoutView(spacing: 20) {
actionButton(
icon: historyItem.isFavorite ? "heart.fill" : "heart",
title: "favorite".localized,
color: historyItem.isFavorite ? .red : .gray,
action: toggleFavorite
)
actionButton(
icon: "doc.on.doc",
title: "copy".localized,
color: .blue,
action: copyContent
)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
// MARK: -
private func generateQRCodeImage() {
guard let content = historyItem.content else { return }
do {
let imageData = try QRCode.build
.text(content)
.quietZonePixelCount(3)
.foregroundColor(CGColor(srgbRed: 1, green: 0, blue: 0.6, alpha: 1))
.backgroundColor(CGColor(srgbRed: 0, green: 0, blue: 0.2, alpha: 1))
.background.cornerRadius(3)
.onPixels.shape(QRCode.PixelShape.CurvePixel())
.eye.shape(QRCode.EyeShape.Teardrop())
.generate.image(dimension: 600, representation: .png())
self.qrCodeImage = UIImage(data: imageData)
} catch {
print("生成二维码失败: \(error)")
}
}
// MARK: -
private func toggleFavorite() {
historyItem.isFavorite.toggle()
coreDataManager.save()
let message = historyItem.isFavorite ? "added_to_favorites".localized : "removed_from_favorites".localized
alertMessage = message
showingAlert = true
}
// MARK: -
private func copyContent() {
if let content = historyItem.content {
UIPasteboard.general.string = content
alertMessage = "content_copied_to_clipboard".localized
showingAlert = true
}
}
// MARK: - URL
private func canOpenURL(_ string: String) -> Bool {
guard let url = URL(string: string) else { return false }
return UIApplication.shared.canOpenURL(url)
}
// MARK: - URL
private func openURL(_ string: String) {
guard let url = URL(string: string) else { return }
UIApplication.shared.open(url)
}
// MARK: - WiFi
private func getWiFiDetails() -> WiFiDetails? {
guard let content = historyItem.content else { return nil }
let parsedData = QRCodeParser.parseQRCode(content)
guard parsedData.type == .wifi,
let extraData = parsedData.extraData else { return nil }
return try? JSONDecoder().decode(WiFiDetails.self, from: extraData)
}
// MARK: - SMS
private func getSMSDetails() -> SMSDetails? {
guard let content = historyItem.content else { return nil }
let parsedData = QRCodeParser.parseQRCode(content)
guard parsedData.type == .sms,
let extraData = parsedData.extraData else { return nil }
return try? JSONDecoder().decode(SMSDetails.self, from: extraData)
}
// MARK: -
private func getCalendarDetails() -> CalendarDetails? {
guard let content = historyItem.content else { return nil }
let parsedData = QRCodeParser.parseQRCode(content)
guard parsedData.type == .calendar,
let extraData = parsedData.extraData else { return nil }
return try? JSONDecoder().decode(CalendarDetails.self, from: extraData)
}
// MARK: - WiFi
private func copyWiFiPassword() {
guard let wifiDetails = getWiFiDetails() else { return }
UIPasteboard.general.string = wifiDetails.password
alertMessage = "wifi_password_copied".localized
showingAlert = true
}
// MARK: - WiFi
private func setupWiFi() {
guard let wifiDetails = getWiFiDetails() else { return }
// 使WiFi
WiFiConnectionManager.shared.connectWithFallback(ssid: wifiDetails.ssid, password: wifiDetails.password) { success, error in
DispatchQueue.main.async {
if success {
self.alertMessage = "wifi_connected_successfully".localized
} else {
self.alertMessage = error ?? "wifi_connection_failed".localized
}
self.showingAlert = true
}
}
}
// MARK: -
private func addContact() {
guard let content = historyItem.content else { return }
ContactManager.shared.addContactToAddressBook(vcardContent: content) { success, error in
DispatchQueue.main.async {
if success {
self.alertMessage = "contact_added_successfully".localized
} else {
self.alertMessage = error ?? "contact_add_failed".localized
}
self.showingAlert = true
}
}
}
// MARK: -
private func makePhoneCall(phoneNumber: String) {
ContactManager.shared.makePhoneCall(phoneNumber: phoneNumber) { success, error in
DispatchQueue.main.async {
if !success {
self.alertMessage = error ?? "phone_call_failed".localized
self.showingAlert = true
}
}
}
}
// MARK: -
private func sendSMS(phoneNumber: String, message: String = "") {
ContactManager.shared.sendSMS(phoneNumber: phoneNumber, message: message) { success, error in
DispatchQueue.main.async {
if !success {
self.alertMessage = error ?? "sms_app_failed".localized
self.showingAlert = true
}
}
}
}
// MARK: -
private func sendEmail(emailAddress: String) {
let mailtoURL = "mailto:\(emailAddress)"
if let url = URL(string: mailtoURL), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
} else {
alertMessage = "email_app_failed".localized
showingAlert = true
}
}
// MARK: -
private func addEventToCalendar() {
guard let calendarDetails = getCalendarDetails() else { return }
CalendarManager.shared.addEventToCalendar(calendarDetails: calendarDetails) { success, error in
DispatchQueue.main.async {
if success {
self.alertMessage = "calendar_event_added_successfully".localized
} else {
self.alertMessage = error ?? "calendar_event_add_failed".localized
}
self.showingAlert = true
}
}
}
// MARK: -
private func getContactInfoFromParsedData(_ parsedData: ParsedQRData) -> ContactInfo? {
guard let extraData = parsedData.extraData else { return nil }
do {
let contactInfo = try JSONDecoder().decode(ContactInfo.self, from: extraData)
return contactInfo
} catch {
print("解析联系人信息失败: \(error)")
return nil
}
}
// MARK: - Email
private func getEmailDetails(parsedData: ParsedQRData) -> EmailDetails? {
guard let extraData = parsedData.extraData else { return nil }
do {
let emailDetails = try JSONDecoder().decode(EmailDetails.self, from: extraData)
return emailDetails
} catch {
print("解析Email信息失败: \(error)")
return nil
}
}
// MARK: - Calendar
private func getCalendarDetails(parsedData: ParsedQRData) -> CalendarDetails? {
guard let extraData = parsedData.extraData else { return nil }
do {
let calendarDetails = try JSONDecoder().decode(CalendarDetails.self, from: extraData)
return calendarDetails
} catch {
print("解析Calendar信息失败: \(error)")
return nil
}
}
// MARK: - SMS
private func getSMSDetails(parsedData: ParsedQRData) -> SMSDetails? {
guard let extraData = parsedData.extraData else { return nil }
do {
let smsDetails = try JSONDecoder().decode(SMSDetails.self, from: extraData)
return smsDetails
} catch {
print("解析SMS信息失败: \(error)")
return nil
}
}
// MARK: -
private func formatCalendarTime(_ timeString: String) -> String {
guard timeString.count >= 15 else { return timeString }
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss"
if let date = dateFormatter.date(from: timeString) {
let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .medium
displayFormatter.timeStyle = .short
return displayFormatter.string(from: date)
}
return timeString
}
}
// MARK: -
struct ShareSheet: UIViewControllerRepresentable {
let activityItems: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
#Preview("WiFi") {
let ctx = PreviewData.context
let item = PreviewData.wifiSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("URL") {
let ctx = PreviewData.context
let item = PreviewData.urlSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("SMS") {
let ctx = PreviewData.context
let item = PreviewData.smsSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("vCard") {
let ctx = PreviewData.context
let item = PreviewData.vcardSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("Instagram") {
let ctx = PreviewData.context
let item = PreviewData.instagramSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("WhatsApp") {
let ctx = PreviewData.context
let item = PreviewData.whatsappSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("Viber") {
let ctx = PreviewData.context
let item = PreviewData.viberSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("Text") {
let ctx = PreviewData.context
let item = PreviewData.textSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("MeCard") {
let ctx = PreviewData.context
let item = PreviewData.mecardSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("Calendar") {
let ctx = PreviewData.context
let item = PreviewData.calendarSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
#Preview("Email") {
let ctx = PreviewData.context
let item = PreviewData.emailSample(in: ctx)
NavigationView { QRCodeDetailView(historyItem: item) }
}
// MARK: - Preview Data
private enum PreviewData {
static let context: NSManagedObjectContext = {
let container = NSPersistentContainer(name: "MyQrCode")
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, _ in }
return container.viewContext
}()
private static func makeBaseItem(in context: NSManagedObjectContext, content: String, qrType: QRCodeType, favorite: Bool = false) -> HistoryItem {
let item = HistoryItem(context: context)
item.id = UUID()
item.content = content
item.dataType = DataType.qrcode.rawValue
item.dataSource = DataSource.created.rawValue
item.createdAt = Date()
item.isFavorite = favorite
item.qrCodeType = qrType.rawValue
return item
}
static func wifiSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "WIFI:T:WPA;S:MyNetwork;P:MyPassword;;"
return makeBaseItem(in: context, content: content, qrType: .wifi, favorite: true)
}
static func urlSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "https://www.example.com"
return makeBaseItem(in: context, content: content, qrType: .url)
}
static func smsSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "SMSTO:+1 (555) 123-4567:Hello"
return makeBaseItem(in: context, content: content, qrType: .sms)
}
static func vcardSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = """
BEGIN:VCARD
VERSION:3.0
N:Doe;John;;;
FN:John Doe
TEL;TYPE=WORK,CELL:(123) 456-7890
EMAIL;TYPE=PREF,INTERNET:john.doe@example.com
ORG:Example Company
TITLE:Software Engineer
ADR;TYPE=WORK:;;123 Main St;Anytown;CA;12345;USA
URL:https://example.com
END:VCARD
""".trimmingCharacters(in: .whitespacesAndNewlines)
return makeBaseItem(in: context, content: content, qrType: .vcard)
}
static func instagramSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "instagram://user?username=example_user"
return makeBaseItem(in: context, content: content, qrType: .instagram)
}
static func whatsappSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "whatsapp://send?phone=+1234567890"
return makeBaseItem(in: context, content: content, qrType: .whatsapp)
}
static func textSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "Hello, this is a text message!"
return makeBaseItem(in: context, content: content, qrType: .text)
}
static func viberSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "viber://add?number=+1234567890"
return makeBaseItem(in: context, content: content, qrType: .viber)
}
static func mecardSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "MECARD:N:Doe,John;NICKNAME:Johnny;TEL:+1234567890;EMAIL:john.doe@example.com;ORG:Example Company;TITLE:Software Engineer;ADR:123 Main St,Anytown,CA,12345,USA;URL:https://example.com;NOTE:This is a note;"
return makeBaseItem(in: context, content: content, qrType: .mecard)
}
static func calendarSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = """
BEGIN:VEVENT
SUMMARY:团队会议
DTSTART:20241201T140000
DTEND:20241201T150000
LOCATION:会议室A
DESCRIPTION:讨论项目进度和下一步计划
END:VEVENT
""".trimmingCharacters(in: .whitespacesAndNewlines)
return makeBaseItem(in: context, content: content, qrType: .calendar)
}
static func emailSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "mailto:example@email.com?subject=Hello&body=This is a test email message with some content to demonstrate the email QR code functionality."
return makeBaseItem(in: context, content: content, qrType: .mail)
}
}
// MARK: -
extension QRCodeDetailView {
// MARK: -
private func generateStylePreviewImage(styleData: QRCodeStyleData) -> UIImage? {
guard let content = historyItem.content else { return nil }
do {
var qrCodeBuilder = try QRCode.build
.text(content)
.quietZonePixelCount(0)
//
if let foregroundColor = getColorFromString(styleData.foregroundColor) {
qrCodeBuilder = qrCodeBuilder.foregroundColor(foregroundColor)
}
//
if let backgroundColor = getColorFromString(styleData.backgroundColor) {
qrCodeBuilder = qrCodeBuilder.backgroundColor(backgroundColor)
}
//
qrCodeBuilder = qrCodeBuilder.background.cornerRadius(3)
//
if let dotType = getDotTypeFromString(styleData.dotType) {
qrCodeBuilder = qrCodeBuilder.onPixels.shape(dotType)
}
//
if let eyeType = getEyeTypeFromString(styleData.eyeType) {
qrCodeBuilder = qrCodeBuilder.eye.shape(eyeType)
}
// Logo
if let logo = styleData.logo, !logo.isEmpty {
if let logoTemplate = getLogoFromString(logo) {
qrCodeBuilder = qrCodeBuilder.logo(logoTemplate)
}
}
//
let imageData = try qrCodeBuilder.generate.image(dimension: 300, representation: .png())
return UIImage(data: imageData)
} catch {
print("生成样式预览图片失败: \(error)")
return nil
}
}
// MARK: -
private func generateStandardQRCodeImage() -> UIImage? {
guard let content = historyItem.content else { return nil }
do {
let imageData = try QRCode.build
.text(content)
.quietZonePixelCount(0)
.foregroundColor(CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1))
.backgroundColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1))
.generate.image(dimension: 300, representation: .png())
return UIImage(data: imageData)
} catch {
print("生成标准二维码图片失败: \(error)")
return nil
}
}
// MARK: -
private func getColorFromString(_ colorString: String) -> CGColor? {
if let color = QRCodeColor(rawValue: colorString) {
return color.cgColor
}
return nil
}
// MARK: -
private func getDotTypeFromString(_ dotTypeString: String) -> QRCodePixelShapeGenerator? {
if let dotType = QRCodeDotType(rawValue: dotTypeString) {
return dotType.pixelShape
}
return nil
}
// MARK: -
private func getEyeTypeFromString(_ eyeTypeString: String) -> QRCodeEyeShapeGenerator? {
if let eyeType = QRCodeEyeType(rawValue: eyeTypeString) {
return eyeType.eyeShape
}
return nil
}
// MARK: - Logo
private func getLogoFromString(_ logoString: String) -> QRCode.LogoTemplate? {
// Logo
if logoString.hasPrefix("custom_") {
// Logo
if let styleData = getStyleData(),
let customImage = styleData.customLogoImage,
let cgImage = customImage.cgImage {
return QRCode.LogoTemplate.CircleCenter(image: cgImage, inset: 0)
}
} else {
// Logo
if let logo = QRCodeLogo(rawValue: logoString),
let image = logo.image,
let cgImage = image.cgImage {
return QRCode.LogoTemplate.CircleCenter(image: cgImage)
}
}
return nil
}
// MARK: - JSON
private func getStyleData() -> QRCodeStyleData? {
guard let jsonString = historyItem.qrCodeStyleData,
let jsonData = jsonString.data(using: .utf8) else {
return nil
}
do {
let styleData = try JSONDecoder().decode(QRCodeStyleData.self, from: jsonData)
return styleData
} catch {
print("❌ 样式数据JSON解码失败\(error)")
return nil
}
}
// MARK: -
private func getColorDisplayName(_ colorString: String) -> String {
if let color = QRCodeColor(rawValue: colorString) {
switch color {
case .black: return "black".localized
case .white: return "white".localized
case .red: return "red".localized
case .blue: return "blue".localized
case .green: return "green".localized
case .yellow: return "yellow".localized
case .purple: return "purple".localized
case .orange: return "orange".localized
case .pink: return "pink".localized
case .cyan: return "cyan".localized
case .magenta: return "magenta".localized
case .brown: return "brown".localized
case .gray: return "gray".localized
case .navy: return "navy".localized
case .teal: return "teal".localized
case .indigo: return "indigo".localized
case .lime: return "lime".localized
case .maroon: return "maroon".localized
case .olive: return "olive".localized
case .silver: return "silver".localized
}
}
return colorString
}
private func getDotTypeDisplayName(_ dotTypeString: String) -> String {
if let dotType = QRCodeDotType(rawValue: dotTypeString) {
return dotType.displayName
}
return dotTypeString
}
private func getEyeTypeDisplayName(_ eyeTypeString: String) -> String {
if let eyeType = QRCodeEyeType(rawValue: eyeTypeString) {
return eyeType.displayName
}
return eyeTypeString
}
private func getLogoDisplayName(_ logoString: String) -> String {
if let logo = QRCodeLogo(rawValue: logoString) {
return logo.displayName
}
return logoString
}
// MARK: -
private func getQRCodeType() -> QRCodeType {
if let qrCodeTypeString = historyItem.qrCodeType,
let qrCodeType = QRCodeType(rawValue: qrCodeTypeString) {
return qrCodeType
}
return .text // text
}
// MARK: -
private func getNavigationTitle() -> String {
if let qrCodeTypeString = historyItem.qrCodeType,
let qrCodeType = QRCodeType(rawValue: qrCodeTypeString) {
// vCardMCard使Contact
if qrCodeType == .vcard || qrCodeType == .mecard {
return "contact".localized
}
return qrCodeType.displayName
}
return "qr_code_detail".localized
}
// MARK: - Decorate code
private var decorateCodeButton: some View {
VStack(spacing: 16) {
Button(action: {
navigateToCustomStyle()
}) {
HStack(spacing: 12) {
Image(systemName: "paintpalette.fill")
.font(.title2)
.foregroundColor(.white)
Text("decorate_code".localized)
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(.white)
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white.opacity(0.8))
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
.background(
LinearGradient(
gradient: Gradient(colors: [Color.purple, Color.blue]),
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(12)
.shadow(color: .purple.opacity(0.3), radius: 8, x: 0, y: 4)
}
.buttonStyle(PlainButtonStyle())
//
if getStyleData() != nil {
HStack {
Image(systemName: "info.circle.fill")
.font(.caption)
.foregroundColor(.orange)
Text("qr_code_has_style".localized)
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
.padding(.horizontal, 4)
}
}
.padding()
.cornerRadius(12)
.shadow(radius: 2)
}
// MARK: -
private enum SocialAppType {
case instagram
case facebook
case twitter
case whatsapp
case viber
case spotify
}
// MARK: -
private func openSocialApp(content: String, appType: SocialAppType) {
// 使
if let url = URL(string: content), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url) { success in
if !success {
//
DispatchQueue.main.async {
self.alertMessage = "app_open_failed".localized
self.showingAlert = true
}
}
}
} else {
// URL
alertMessage = "app_open_failed".localized
showingAlert = true
}
}
// MARK: -
private func navigateToCustomStyle() {
navigateToStyleView = true
}
}
// MARK: - FlowLayout
struct FlowLayoutView<Content: View>: View {
let spacing: CGFloat
let content: Content
init(spacing: CGFloat = 20, @ViewBuilder content: () -> Content) {
self.spacing = spacing
self.content = content()
}
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: spacing), count: 4), spacing: spacing) {
content
}
}
}