Add Quick Look Preview and Thumbnail extensions for .casera files
- Add CaseraQLPreview extension to show custom preview with contractor details and import instructions when viewing .casera files - Add CaseraQLThumbnail extension to display teal icon in Messages and Files app instead of generic white box - Update UTExportedTypeDeclarations to conform to public.content for better system file type recognition 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
264
iosApp/CaseraQLPreview/PreviewViewController.swift
Normal file
264
iosApp/CaseraQLPreview/PreviewViewController.swift
Normal file
@@ -0,0 +1,264 @@
|
||||
import UIKit
|
||||
import QuickLook
|
||||
|
||||
class PreviewViewController: UIViewController, QLPreviewingController {
|
||||
|
||||
// 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?
|
||||
|
||||
// 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
|
||||
let data = try Data(contentsOf: url)
|
||||
let decoder = JSONDecoder()
|
||||
let contractor = try decoder.decode(ContractorPreviewData.self, from: data)
|
||||
self.contractorData = contractor
|
||||
print("CaseraQLPreview: Parsed contractor: \(contractor.name)")
|
||||
|
||||
await MainActor.run {
|
||||
self.updateUI(with: contractor)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateUI(with contractor: ContractorPreviewData) {
|
||||
titleLabel.text = contractor.name
|
||||
|
||||
// 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user