import UIKit import QuickLook class PreviewViewController: UIViewController, QLPreviewingController { // MARK: - Types /// Represents the type of .casera package private enum PackageType { case contractor case residence } // MARK: - UI Elements private let containerView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private let iconImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFit imageView.tintColor = UIColor(red: 7/255, green: 160/255, blue: 195/255, alpha: 1) // App primary color let config = UIImage.SymbolConfiguration(pointSize: 60, weight: .light) imageView.image = UIImage(systemName: "person.crop.rectangle.stack", withConfiguration: config) return imageView }() private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 24, weight: .bold) label.textColor = .label label.textAlignment = .center label.numberOfLines = 2 return label }() private let subtitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 15, weight: .medium) label.textColor = .secondaryLabel label.textAlignment = .center label.text = "Casera Contractor File" return label }() private let dividerView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .separator return view }() private let detailsStackView: UIStackView = { let stack = UIStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.axis = .vertical stack.spacing = 12 stack.alignment = .leading return stack }() private let instructionCard: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = UIColor(red: 7/255, green: 160/255, blue: 195/255, alpha: 0.1) view.layer.cornerRadius = 12 return view }() private let instructionLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 15, weight: .medium) label.textColor = UIColor(red: 7/255, green: 160/255, blue: 195/255, alpha: 1) label.textAlignment = .center label.numberOfLines = 0 label.text = "Tap the share button below, then select \"Casera\" to import this contractor." return label }() private let arrowImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFit imageView.tintColor = .secondaryLabel let config = UIImage.SymbolConfiguration(pointSize: 24, weight: .medium) imageView.image = UIImage(systemName: "arrow.down", withConfiguration: config) return imageView }() // MARK: - Data private var contractorData: ContractorPreviewData? private var residenceData: ResidencePreviewData? private var currentPackageType: PackageType = .contractor // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() print("CaseraQLPreview: viewDidLoad called") setupUI() } // MARK: - Setup private func setupUI() { view.backgroundColor = .systemBackground view.addSubview(containerView) containerView.addSubview(iconImageView) containerView.addSubview(titleLabel) containerView.addSubview(subtitleLabel) containerView.addSubview(dividerView) containerView.addSubview(detailsStackView) containerView.addSubview(instructionCard) instructionCard.addSubview(instructionLabel) containerView.addSubview(arrowImageView) NSLayoutConstraint.activate([ containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -40), containerView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 32), containerView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -32), containerView.widthAnchor.constraint(lessThanOrEqualToConstant: 340), iconImageView.topAnchor.constraint(equalTo: containerView.topAnchor), iconImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), iconImageView.widthAnchor.constraint(equalToConstant: 80), iconImageView.heightAnchor.constraint(equalToConstant: 80), titleLabel.topAnchor.constraint(equalTo: iconImageView.bottomAnchor, constant: 16), titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), subtitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), subtitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), dividerView.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 20), dividerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), dividerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), dividerView.heightAnchor.constraint(equalToConstant: 1), detailsStackView.topAnchor.constraint(equalTo: dividerView.bottomAnchor, constant: 20), detailsStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), detailsStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), instructionCard.topAnchor.constraint(equalTo: detailsStackView.bottomAnchor, constant: 24), instructionCard.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), instructionCard.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), instructionLabel.topAnchor.constraint(equalTo: instructionCard.topAnchor, constant: 16), instructionLabel.leadingAnchor.constraint(equalTo: instructionCard.leadingAnchor, constant: 16), instructionLabel.trailingAnchor.constraint(equalTo: instructionCard.trailingAnchor, constant: -16), instructionLabel.bottomAnchor.constraint(equalTo: instructionCard.bottomAnchor, constant: -16), arrowImageView.topAnchor.constraint(equalTo: instructionCard.bottomAnchor, constant: 16), arrowImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), arrowImageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), ]) } private func addDetailRow(icon: String, text: String) { let rowStack = UIStackView() rowStack.axis = .horizontal rowStack.spacing = 12 rowStack.alignment = .center let iconView = UIImageView() iconView.translatesAutoresizingMaskIntoConstraints = false let config = UIImage.SymbolConfiguration(pointSize: 16, weight: .medium) iconView.image = UIImage(systemName: icon, withConfiguration: config) iconView.tintColor = .secondaryLabel iconView.widthAnchor.constraint(equalToConstant: 24).isActive = true iconView.heightAnchor.constraint(equalToConstant: 24).isActive = true let textLabel = UILabel() textLabel.font = .systemFont(ofSize: 15) textLabel.textColor = .label textLabel.text = text textLabel.numberOfLines = 1 rowStack.addArrangedSubview(iconView) rowStack.addArrangedSubview(textLabel) detailsStackView.addArrangedSubview(rowStack) } // MARK: - QLPreviewingController func preparePreviewOfFile(at url: URL) async throws { print("CaseraQLPreview: preparePreviewOfFile called with URL: \(url)") // Parse the .casera file — single Codable pass to detect type, then decode let data = try Data(contentsOf: url) let decoder = JSONDecoder() let envelope = try? decoder.decode(PackageTypeEnvelope.self, from: data) if envelope?.type == "residence" { currentPackageType = .residence let residence = try decoder.decode(ResidencePreviewData.self, from: data) self.residenceData = residence print("CaseraQLPreview: Parsed residence: \(residence.residenceName)") await MainActor.run { self.updateUIForResidence(with: residence) } } else { currentPackageType = .contractor let contractor = try decoder.decode(ContractorPreviewData.self, from: data) self.contractorData = contractor print("CaseraQLPreview: Parsed contractor: \(contractor.name)") await MainActor.run { self.updateUIForContractor(with: contractor) } } } private func updateUIForContractor(with contractor: ContractorPreviewData) { // Update icon let config = UIImage.SymbolConfiguration(pointSize: 60, weight: .light) iconImageView.image = UIImage(systemName: "person.crop.rectangle.stack", withConfiguration: config) titleLabel.text = contractor.name subtitleLabel.text = "Casera Contractor File" instructionLabel.text = "Tap the share button below, then select \"Casera\" to import this contractor." // Clear existing details detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } // Add details if let company = contractor.company, !company.isEmpty { addDetailRow(icon: "building.2", text: company) } if let phone = contractor.phone, !phone.isEmpty { addDetailRow(icon: "phone", text: phone) } if let email = contractor.email, !email.isEmpty { addDetailRow(icon: "envelope", text: email) } if !contractor.specialtyNames.isEmpty { let specialties = contractor.specialtyNames.joined(separator: ", ") addDetailRow(icon: "wrench.and.screwdriver", text: specialties) } if let exportedBy = contractor.exportedBy, !exportedBy.isEmpty { addDetailRow(icon: "person", text: "Shared by \(exportedBy)") } } private func updateUIForResidence(with residence: ResidencePreviewData) { // Update icon let config = UIImage.SymbolConfiguration(pointSize: 60, weight: .light) iconImageView.image = UIImage(systemName: "house.fill", withConfiguration: config) titleLabel.text = residence.residenceName subtitleLabel.text = "Casera Residence Invite" instructionLabel.text = "Tap the share button below, then select \"Casera\" to join this residence." // Clear existing details detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } // Add details if let sharedBy = residence.sharedBy, !sharedBy.isEmpty { addDetailRow(icon: "person", text: "Shared by \(sharedBy)") } if let expiresAt = residence.expiresAt, !expiresAt.isEmpty { addDetailRow(icon: "clock", text: "Expires: \(expiresAt)") } } } // MARK: - Type Discriminator /// Lightweight struct to detect the package type without a full parse private struct PackageTypeEnvelope: Decodable { let type: String? } // MARK: - Data Model struct ContractorPreviewData: Codable { let version: Int let name: String let company: String? let phone: String? let email: String? let website: String? let notes: String? let streetAddress: String? let city: String? let stateProvince: String? let postalCode: String? let specialtyNames: [String] let rating: Double? let isFavorite: Bool let exportedAt: String? let exportedBy: String? enum CodingKeys: String, CodingKey { case version, name, company, phone, email, website, notes case streetAddress = "street_address" case city case stateProvince = "state_province" case postalCode = "postal_code" case specialtyNames = "specialty_names" case rating case isFavorite = "is_favorite" case exportedAt = "exported_at" case exportedBy = "exported_by" } } struct ResidencePreviewData: Codable { let version: Int let type: String let shareCode: String let residenceName: String let sharedBy: String? let expiresAt: String? let exportedAt: String? let exportedBy: String? enum CodingKeys: String, CodingKey { case version, type case shareCode = "share_code" case residenceName = "residence_name" case sharedBy = "shared_by" case expiresAt = "expires_at" case exportedAt = "exported_at" case exportedBy = "exported_by" } }