Enhance PDF export with maps, images, and progress UI
- Add MapSnapshotService for route maps and city maps using MKMapSnapshotter - Add RemoteImageService for team logos and stadium photos with caching - Add POISearchService for nearby attractions using MKLocalSearch - Add PDFAssetPrefetcher to orchestrate parallel asset fetching - Rewrite PDFGenerator with rich page layouts: cover, route overview, day-by-day itinerary, city spotlights, and summary pages - Add export progress overlay in TripDetailView with animated progress ring 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@ struct TripDetailView: View {
|
||||
@State private var showShareSheet = false
|
||||
@State private var exportURL: URL?
|
||||
@State private var shareURL: URL?
|
||||
@State private var isExporting = false
|
||||
@State private var exportProgress: PDFAssetPrefetcher.PrefetchProgress?
|
||||
@State private var mapCameraPosition: MapCameraPosition = .automatic
|
||||
@State private var isSaved = false
|
||||
@State private var routePolylines: [MKPolyline] = []
|
||||
@@ -93,6 +95,64 @@ struct TripDetailView: View {
|
||||
.onAppear {
|
||||
checkIfSaved()
|
||||
}
|
||||
.overlay {
|
||||
if isExporting {
|
||||
exportProgressOverlay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Export Progress Overlay
|
||||
|
||||
private var exportProgressOverlay: some View {
|
||||
ZStack {
|
||||
// Background dimmer
|
||||
Color.black.opacity(0.6)
|
||||
.ignoresSafeArea()
|
||||
|
||||
// Progress card
|
||||
VStack(spacing: Theme.Spacing.lg) {
|
||||
// Progress ring
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Theme.cardBackgroundElevated(colorScheme), lineWidth: 8)
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: exportProgress?.percentComplete ?? 0)
|
||||
.stroke(Theme.warmOrange, style: StrokeStyle(lineWidth: 8, lineCap: .round))
|
||||
.frame(width: 80, height: 80)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.easeInOut(duration: 0.3), value: exportProgress?.percentComplete)
|
||||
|
||||
Image(systemName: "doc.fill")
|
||||
.font(.system(size: 24))
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
}
|
||||
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
Text("Creating PDF")
|
||||
.font(.system(size: Theme.FontSize.cardTitle, weight: .semibold))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
Text(exportProgress?.currentStep ?? "Preparing...")
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if let progress = exportProgress {
|
||||
Text("\(Int(progress.percentComplete * 100))%")
|
||||
.font(.system(size: Theme.FontSize.micro, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(Theme.textMuted(colorScheme))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(Theme.Spacing.xl)
|
||||
.background(Theme.cardBackground(colorScheme))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
||||
.shadow(color: .black.opacity(0.3), radius: 20, y: 10)
|
||||
}
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
// MARK: - Hero Map Section
|
||||
@@ -432,13 +492,22 @@ struct TripDetailView: View {
|
||||
// MARK: - Actions
|
||||
|
||||
private func exportPDF() async {
|
||||
isExporting = true
|
||||
exportProgress = nil
|
||||
|
||||
do {
|
||||
let url = try await exportService.exportToPDF(trip: trip, games: games)
|
||||
let url = try await exportService.exportToPDF(trip: trip, games: games) { progress in
|
||||
await MainActor.run {
|
||||
self.exportProgress = progress
|
||||
}
|
||||
}
|
||||
exportURL = url
|
||||
showExportSheet = true
|
||||
} catch {
|
||||
print("Failed to export PDF: \(error)")
|
||||
}
|
||||
|
||||
isExporting = false
|
||||
}
|
||||
|
||||
private func shareTrip() async {
|
||||
|
||||
Reference in New Issue
Block a user