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
28 KiB

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(NSLocalizedString("tip", comment: "Tip"), isPresented: $showingAlert) {
Button(NSLocalizedString("confirm", comment: "Confirm")) { }
} 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(NSLocalizedString("scan_this_qr_code", comment: "Scan this QR code"))
.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(NSLocalizedString("parsed_info", comment: "Parsed Information"))
.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(NSLocalizedString("original_content", comment: "Original Content"))
.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(NSLocalizedString("custom", comment: "Custom"), 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(NSLocalizedString("standard", comment: "Standard"), 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 ? NSLocalizedString("unfavorite", comment: "Unfavorite") : NSLocalizedString("favorite", comment: "Favorite"))
.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(NSLocalizedString("copy_content", comment: "Copy Content"))
.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(NSLocalizedString("open_link", comment: "Open Link"))
.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 ? NSLocalizedString("added_to_favorites", comment: "Added to favorites") : NSLocalizedString("removed_from_favorites", comment: "Removed from favorites")
alertMessage = message
showingAlert = true
}
// MARK: -
private func copyContent() {
if let content = historyItem.content {
UIPasteboard.general.string = content
alertMessage = NSLocalizedString("content_copied_to_clipboard", comment: "Content copied to clipboard")
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 NSLocalizedString("black", comment: "Black")
case .white: return NSLocalizedString("white", comment: "White")
case .red: return NSLocalizedString("red", comment: "Red")
case .blue: return NSLocalizedString("blue", comment: "Blue")
case .green: return NSLocalizedString("green", comment: "Green")
case .yellow: return NSLocalizedString("yellow", comment: "Yellow")
case .purple: return NSLocalizedString("purple", comment: "Purple")
case .orange: return NSLocalizedString("orange", comment: "Orange")
case .pink: return NSLocalizedString("pink", comment: "Pink")
case .cyan: return NSLocalizedString("cyan", comment: "Cyan")
case .magenta: return NSLocalizedString("magenta", comment: "Magenta")
case .brown: return NSLocalizedString("brown", comment: "Brown")
case .gray: return NSLocalizedString("gray", comment: "Gray")
case .navy: return NSLocalizedString("navy", comment: "Navy")
case .teal: return NSLocalizedString("teal", comment: "Teal")
case .indigo: return NSLocalizedString("indigo", comment: "Indigo")
case .lime: return NSLocalizedString("lime", comment: "Lime")
case .maroon: return NSLocalizedString("maroon", comment: "Maroon")
case .olive: return NSLocalizedString("olive", comment: "Olive")
case .silver: return NSLocalizedString("silver", comment: "Silver")
}
}
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 NSLocalizedString("qr_code_detail", comment: "QR Code Detail")
}
// 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(NSLocalizedString("decorate_code", comment: "Decorate Code"))
.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(NSLocalizedString("qr_code_has_style", comment: "This QR code has custom style, tap to edit"))
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
.padding(.horizontal, 4)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
// MARK: -
private func navigateToCustomStyle() {
navigateToStyleView = true
}
}