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.

708 lines
25 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 = ""
var body: some View {
ScrollView {
VStack(spacing: 20) {
//
qrCodeStyleSection
//
qrCodeTypeSection
//
parsedInfoSection
//
originalContentSection
//
actionButtonsSection
}
.padding()
}
.navigationTitle("二维码详情")
.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("提示", isPresented: $showingAlert) {
Button("确定") { }
} message: {
Text(alertMessage)
}
}
// 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("扫描此二维码")
.font(.caption)
.foregroundColor(.secondary)
}
}
// MARK: -
private var qrCodeTypeSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "qrcode")
.font(.title2)
.foregroundColor(.blue)
Text("二维码类型")
.font(.headline)
Spacer()
}
if let qrCodeTypeString = historyItem.qrCodeType,
let qrCodeType = QRCodeType(rawValue: qrCodeTypeString) {
HStack {
Image(systemName: qrCodeType.icon)
.font(.title3)
.foregroundColor(.orange)
Text(qrCodeType.displayName)
.font(.title3)
.fontWeight(.medium)
Spacer()
}
.padding()
.background(Color.orange.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
// MARK: -
private var parsedInfoSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "info.circle")
.font(.title2)
.foregroundColor(.green)
Text("解析信息")
.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("原始内容")
.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("自定义样式", 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("标准样式", 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 ? "取消收藏" : "收藏")
.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("复制内容")
.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("打开链接")
.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 ? "已添加到收藏" : "已取消收藏"
alertMessage = message
showingAlert = true
}
// MARK: -
private func copyContent() {
if let content = historyItem.content {
UIPasteboard.general.string = content
alertMessage = "内容已复制到剪贴板"
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:+8613800138000: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 "黑色"
case .white: return "白色"
case .red: return "红色"
case .blue: return "蓝色"
case .green: return "绿色"
case .yellow: return "黄色"
case .purple: return "紫色"
case .orange: return "橙色"
case .pink: return "粉色"
case .cyan: return "青色"
case .magenta: return "洋红色"
case .brown: return "棕色"
case .gray: return "灰色"
case .navy: return "海军蓝"
case .teal: return "蓝绿色"
case .indigo: return "靛蓝色"
case .lime: return "青柠色"
case .maroon: return "栗色"
case .olive: return "橄榄色"
case .silver: return "银色"
}
}
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
}
}