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.

767 lines
27 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
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) {
//
qrCodeStyleSection
//
parsedInfoSection
//
originalContentSection
//
actionButtonsSection
// Decorate code
decorateCodeButton
}
.padding()
}
.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) {
HStack {
Image(systemName: "info.circle")
.font(.title2)
.foregroundColor(.green)
Text("parsed_info".localized)
.font(.headline)
Spacer()
}
if let content = historyItem.content {
let parsedData = QRCodeParser.parseQRCode(content)
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: parsedData.icon)
.font(.title3)
.foregroundColor(.green)
Text(parsedData.title)
.font(.title3)
.fontWeight(.medium)
Spacer()
}
if let subtitle = parsedData.subtitle {
Text(subtitle)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
}
}
.padding()
.background(Color.green.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
// 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 var actionButtonsSection: some View {
VStack(spacing: 12) {
//
Button(action: toggleFavorite) {
HStack {
Image(systemName: historyItem.isFavorite ? "heart.fill" : "heart")
.foregroundColor(historyItem.isFavorite ? .red : .gray)
Text(historyItem.isFavorite ? "unfavorite".localized : "favorite".localized)
.fontWeight(.medium)
}
.frame(maxWidth: .infinity)
.padding()
.background(historyItem.isFavorite ? Color.red.opacity(0.1) : Color.gray.opacity(0.1))
.foregroundColor(historyItem.isFavorite ? .red : .gray)
.cornerRadius(10)
}
//
Button(action: copyContent) {
HStack {
Image(systemName: "doc.on.doc")
.foregroundColor(.blue)
Text("copy_content".localized)
.fontWeight(.medium)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(10)
}
// URL
if let content = historyItem.content, canOpenURL(content) {
Button(action: { openURL(content) }) {
HStack {
Image(systemName: "arrow.up.right.square")
.foregroundColor(.green)
Text("open_link".localized)
.fontWeight(.medium)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.green.opacity(0.1))
.foregroundColor(.green)
.cornerRadius(10)
}
}
}
.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: -
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) }
}
// 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)
}
}
// 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) {
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()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
// MARK: -
private func navigateToCustomStyle() {
navigateToStyleView = true
}
}