Adds debug-only DebugShareExporter that bulk-exports ~298 shareable image variations (achievement cards, progress cards, trip cards, icons) to Documents/DebugExport/ for visual QA. Also adds a button to populate all stadium visits for testing the fully-unlocked app state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
623 lines
26 KiB
Swift
623 lines
26 KiB
Swift
//
|
|
// DebugShareExporter.swift
|
|
// SportsTime
|
|
//
|
|
// Debug-only bulk export of all shareable image variations for visual QA.
|
|
//
|
|
|
|
#if DEBUG
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
import CoreLocation
|
|
|
|
// MARK: - Debug Share Exporter
|
|
|
|
@MainActor @Observable
|
|
final class DebugShareExporter {
|
|
|
|
var isExporting = false
|
|
var currentStep = ""
|
|
var progress: Double = 0
|
|
var exportedCount = 0
|
|
var totalCount = 0
|
|
var exportPath: String?
|
|
var error: String?
|
|
|
|
// MARK: - Export All
|
|
|
|
func exportAll(modelContext: ModelContext) async {
|
|
guard !isExporting else { return }
|
|
isExporting = true
|
|
error = nil
|
|
exportPath = nil
|
|
exportedCount = 0
|
|
|
|
let achievementCount = AchievementRegistry.all.count
|
|
// spotlight + milestone + context = 3 * achievements, collection ~5, progress 12, trips 4, icons 1
|
|
totalCount = (achievementCount * 3) + 5 + 12 + 4 + 1
|
|
|
|
do {
|
|
// Step 1: Create export directory
|
|
currentStep = "Creating export directory..."
|
|
let exportDir = try createExportDirectory()
|
|
|
|
// Step 2: Add stadium visits
|
|
currentStep = "Adding stadium visits..."
|
|
await addAllStadiumVisitsInternal(modelContext: modelContext)
|
|
|
|
// Step 3: Recalculate achievements
|
|
currentStep = "Recalculating achievements..."
|
|
let engine = AchievementEngine(modelContext: modelContext)
|
|
_ = try await engine.recalculateAllAchievements()
|
|
|
|
// Step 4: Export achievement spotlight + milestone cards
|
|
currentStep = "Exporting spotlight cards..."
|
|
let spotlightTheme = ShareThemePreferences.theme(for: .achievementSpotlight)
|
|
let milestoneTheme = ShareThemePreferences.theme(for: .achievementMilestone)
|
|
|
|
let spotlightDir = exportDir.appendingPathComponent("achievements/spotlight")
|
|
let milestoneDir = exportDir.appendingPathComponent("achievements/milestone")
|
|
|
|
for definition in AchievementRegistry.all {
|
|
let achievement = AchievementProgress(
|
|
definition: definition,
|
|
currentProgress: totalRequired(for: definition),
|
|
totalRequired: totalRequired(for: definition),
|
|
hasStoredAchievement: true,
|
|
earnedAt: Date()
|
|
)
|
|
|
|
// Spotlight
|
|
currentStep = "Spotlight: \(definition.name)"
|
|
let spotlightContent = AchievementSpotlightContent(achievement: achievement)
|
|
let spotlightImage = try await spotlightContent.render(theme: spotlightTheme)
|
|
try savePNG(spotlightImage, to: spotlightDir.appendingPathComponent("\(definition.id).png"))
|
|
exportedCount += 1
|
|
updateProgress()
|
|
|
|
// Milestone
|
|
currentStep = "Milestone: \(definition.name)"
|
|
let milestoneContent = AchievementMilestoneContent(achievement: achievement)
|
|
let milestoneImage = try await milestoneContent.render(theme: milestoneTheme)
|
|
try savePNG(milestoneImage, to: milestoneDir.appendingPathComponent("\(definition.id).png"))
|
|
exportedCount += 1
|
|
updateProgress()
|
|
}
|
|
|
|
// Step 5: Export achievement collection cards
|
|
currentStep = "Exporting collection cards..."
|
|
let collectionTheme = ShareThemePreferences.theme(for: .achievementCollection)
|
|
let collectionDir = exportDir.appendingPathComponent("achievements/collection")
|
|
|
|
let allAchievements = AchievementRegistry.all.map { def in
|
|
AchievementProgress(
|
|
definition: def,
|
|
currentProgress: totalRequired(for: def),
|
|
totalRequired: totalRequired(for: def),
|
|
hasStoredAchievement: true,
|
|
earnedAt: Date()
|
|
)
|
|
}
|
|
|
|
// Per-sport collections
|
|
for sport in [Sport.mlb, .nba, .nhl] {
|
|
let sportAchievements = allAchievements.filter { $0.definition.sport == sport }
|
|
let collectionContent = AchievementCollectionContent(
|
|
achievements: Array(sportAchievements.prefix(12)),
|
|
year: 2026,
|
|
sports: [sport],
|
|
filterSport: sport
|
|
)
|
|
let image = try await collectionContent.render(theme: collectionTheme)
|
|
try savePNG(image, to: collectionDir.appendingPathComponent("\(sport.rawValue).png"))
|
|
exportedCount += 1
|
|
updateProgress()
|
|
}
|
|
|
|
// Cross-sport collection
|
|
let crossSportAchievements = allAchievements.filter { $0.definition.sport == nil }
|
|
if !crossSportAchievements.isEmpty {
|
|
let content = AchievementCollectionContent(
|
|
achievements: Array(crossSportAchievements.prefix(12)),
|
|
year: 2026
|
|
)
|
|
let image = try await content.render(theme: collectionTheme)
|
|
try savePNG(image, to: collectionDir.appendingPathComponent("cross-sport.png"))
|
|
}
|
|
exportedCount += 1
|
|
updateProgress()
|
|
|
|
// All top-12 collection
|
|
let topAchievements = Array(allAchievements.prefix(12))
|
|
let allContent = AchievementCollectionContent(
|
|
achievements: topAchievements,
|
|
year: 2026
|
|
)
|
|
let allImage = try await allContent.render(theme: collectionTheme)
|
|
try savePNG(allImage, to: collectionDir.appendingPathComponent("all.png"))
|
|
exportedCount += 1
|
|
updateProgress()
|
|
|
|
// Step 6: Export achievement context cards
|
|
currentStep = "Generating map snapshot for context cards..."
|
|
let contextTheme = ShareThemePreferences.theme(for: .achievementContext)
|
|
let contextDir = exportDir.appendingPathComponent("achievements/context")
|
|
|
|
let contextStops = Self.eastCoastStops()
|
|
let mapGenerator = ShareMapSnapshotGenerator()
|
|
let mapSnapshot = await mapGenerator.generateRouteMap(stops: contextStops, theme: contextTheme)
|
|
|
|
for definition in AchievementRegistry.all {
|
|
currentStep = "Context: \(definition.name)"
|
|
let achievement = AchievementProgress(
|
|
definition: definition,
|
|
currentProgress: totalRequired(for: definition),
|
|
totalRequired: totalRequired(for: definition),
|
|
hasStoredAchievement: true,
|
|
earnedAt: Date()
|
|
)
|
|
let contextContent = AchievementContextContent(
|
|
achievement: achievement,
|
|
tripName: "Road Trip 2026",
|
|
mapSnapshot: mapSnapshot
|
|
)
|
|
let image = try await contextContent.render(theme: contextTheme)
|
|
try savePNG(image, to: contextDir.appendingPathComponent("\(definition.id).png"))
|
|
exportedCount += 1
|
|
updateProgress()
|
|
}
|
|
|
|
// Step 7: Export progress cards
|
|
currentStep = "Exporting progress cards..."
|
|
let progressTheme = ShareThemePreferences.theme(for: .stadiumProgress)
|
|
let progressDir = exportDir.appendingPathComponent("progress")
|
|
|
|
let allStadiums = AppDataProvider.shared.stadiums
|
|
for sport in [Sport.mlb, .nba, .nhl] {
|
|
let sportStadiums = allStadiums.filter { $0.sport == sport }
|
|
let total = sportStadiums.count
|
|
|
|
for pct in [25, 50, 75, 100] {
|
|
currentStep = "Progress: \(sport.rawValue) \(pct)%"
|
|
let visitedCount = (total * pct) / 100
|
|
let visited = Array(sportStadiums.prefix(visitedCount))
|
|
let remaining = Array(sportStadiums.dropFirst(visitedCount))
|
|
|
|
let leagueProgress = LeagueProgress(
|
|
sport: sport,
|
|
totalStadiums: total,
|
|
visitedStadiums: visitedCount,
|
|
stadiumsVisited: visited,
|
|
stadiumsRemaining: remaining
|
|
)
|
|
|
|
let tripCount = pct == 100 ? 5 : pct / 25
|
|
let progressContent = ProgressShareContent(
|
|
progress: leagueProgress,
|
|
tripCount: tripCount
|
|
)
|
|
let image = try await progressContent.render(theme: progressTheme)
|
|
try savePNG(image, to: progressDir.appendingPathComponent("\(sport.rawValue)_\(pct).png"))
|
|
exportedCount += 1
|
|
updateProgress()
|
|
}
|
|
}
|
|
|
|
// Step 8: Export trip cards
|
|
currentStep = "Exporting trip cards..."
|
|
let tripTheme = ShareThemePreferences.theme(for: .tripSummary)
|
|
let tripDir = exportDir.appendingPathComponent("trips")
|
|
|
|
let dummyTrips = Self.buildDummyTrips()
|
|
for trip in dummyTrips {
|
|
currentStep = "Trip: \(trip.name)"
|
|
let tripContent = TripShareContent(trip: trip)
|
|
let image = try await tripContent.render(theme: tripTheme)
|
|
let safeName = trip.name.replacingOccurrences(of: " ", with: "_")
|
|
try savePNG(image, to: tripDir.appendingPathComponent("\(safeName).png"))
|
|
exportedCount += 1
|
|
updateProgress()
|
|
}
|
|
|
|
// Step 9: Export sports icon
|
|
currentStep = "Exporting sports icon..."
|
|
let iconDir = exportDir.appendingPathComponent("icons")
|
|
if let iconData = SportsIconImageGenerator.generatePNGData(),
|
|
let iconImage = UIImage(data: iconData) {
|
|
try savePNG(iconImage, to: iconDir.appendingPathComponent("sports_icons.png"))
|
|
}
|
|
exportedCount += 1
|
|
updateProgress()
|
|
|
|
// Done
|
|
exportPath = exportDir.path
|
|
currentStep = "Export complete!"
|
|
print("DEBUG EXPORT: \(exportDir.path)")
|
|
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
currentStep = "Export failed"
|
|
print("DEBUG EXPORT ERROR: \(error)")
|
|
}
|
|
|
|
isExporting = false
|
|
}
|
|
|
|
// MARK: - Add All Stadium Visits
|
|
|
|
func addAllStadiumVisits(modelContext: ModelContext) async {
|
|
guard !isExporting else { return }
|
|
isExporting = true
|
|
currentStep = "Adding stadium visits..."
|
|
error = nil
|
|
|
|
await addAllStadiumVisitsInternal(modelContext: modelContext)
|
|
|
|
// Recalculate achievements
|
|
currentStep = "Recalculating achievements..."
|
|
do {
|
|
let engine = AchievementEngine(modelContext: modelContext)
|
|
let delta = try await engine.recalculateAllAchievements()
|
|
currentStep = "Done! \(delta.newlyEarned.count) achievements earned"
|
|
print("DEBUG: Added all stadium visits. \(delta.newlyEarned.count) new achievements.")
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
currentStep = "Achievement calc failed"
|
|
}
|
|
|
|
isExporting = false
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
private func addAllStadiumVisitsInternal(modelContext: ModelContext) async {
|
|
let stadiums = AppDataProvider.shared.stadiums
|
|
let calendar = Calendar.current
|
|
let today = Date()
|
|
|
|
// Spread visits across 3 days to trigger journey achievements
|
|
for (index, stadium) in stadiums.enumerated() {
|
|
let dayOffset = index % 3
|
|
let visitDate = calendar.date(byAdding: .day, value: -dayOffset, to: today) ?? today
|
|
|
|
let visit = StadiumVisit(
|
|
stadiumId: stadium.id,
|
|
stadiumNameAtVisit: stadium.name,
|
|
visitDate: visitDate,
|
|
sport: stadium.sport,
|
|
visitType: .game,
|
|
dataSource: .fullyManual,
|
|
source: .manual
|
|
)
|
|
modelContext.insert(visit)
|
|
}
|
|
|
|
do {
|
|
try modelContext.save()
|
|
print("DEBUG: Inserted \(stadiums.count) stadium visits")
|
|
} catch {
|
|
print("DEBUG: Failed to save visits: \(error)")
|
|
self.error = "Failed to save visits: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
|
|
private func createExportDirectory() throws -> URL {
|
|
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd_HHmmss"
|
|
let timestamp = formatter.string(from: Date())
|
|
let exportDir = docs.appendingPathComponent("DebugExport/\(timestamp)")
|
|
|
|
let subdirs = [
|
|
"achievements/spotlight",
|
|
"achievements/milestone",
|
|
"achievements/collection",
|
|
"achievements/context",
|
|
"progress",
|
|
"trips",
|
|
"icons"
|
|
]
|
|
|
|
for subdir in subdirs {
|
|
try FileManager.default.createDirectory(
|
|
at: exportDir.appendingPathComponent(subdir),
|
|
withIntermediateDirectories: true
|
|
)
|
|
}
|
|
|
|
return exportDir
|
|
}
|
|
|
|
private func savePNG(_ image: UIImage, to url: URL) throws {
|
|
guard let data = image.pngData() else {
|
|
throw ExportError.renderingFailed
|
|
}
|
|
try data.write(to: url)
|
|
}
|
|
|
|
private func updateProgress() {
|
|
progress = totalCount > 0 ? Double(exportedCount) / Double(totalCount) : 0
|
|
}
|
|
|
|
/// Compute a reasonable totalRequired for an achievement definition.
|
|
/// For division/conference, we use the team count from LeagueStructure since
|
|
/// AppDataProvider doesn't expose per-division stadium queries.
|
|
private func totalRequired(for definition: AchievementDefinition) -> Int {
|
|
switch definition.requirement {
|
|
case .firstVisit:
|
|
return 1
|
|
case .visitCount(let count):
|
|
return count
|
|
case .visitCountForSport(let count, _):
|
|
return count
|
|
case .completeDivision(let divisionId):
|
|
if let division = LeagueStructure.division(byId: divisionId) {
|
|
return max(division.teamCount, 5)
|
|
}
|
|
return 5
|
|
case .completeConference(let conferenceId):
|
|
if let conference = LeagueStructure.conference(byId: conferenceId) {
|
|
return conference.divisionIds.count * 5
|
|
}
|
|
return 15
|
|
case .completeLeague(let sport):
|
|
return AppDataProvider.shared.stadiums.filter { $0.sport == sport }.count
|
|
case .visitsInDays(let visitCount, _):
|
|
return visitCount
|
|
case .multipleLeagues(let count):
|
|
return count
|
|
case .specificStadium:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// MARK: - Dummy Trip Data
|
|
|
|
private static func eastCoastStops() -> [TripStop] {
|
|
let base = Date()
|
|
return [
|
|
TripStop(
|
|
stopNumber: 1, city: "New York", state: "NY",
|
|
coordinate: CLLocationCoordinate2D(latitude: 40.8296, longitude: -73.9262),
|
|
arrivalDate: base, departureDate: base.addingTimeInterval(86400),
|
|
games: ["game1"], stadium: "Yankee Stadium"
|
|
),
|
|
TripStop(
|
|
stopNumber: 2, city: "Boston", state: "MA",
|
|
coordinate: CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972),
|
|
arrivalDate: base.addingTimeInterval(86400), departureDate: base.addingTimeInterval(172800),
|
|
games: ["game2"], stadium: "Fenway Park"
|
|
),
|
|
TripStop(
|
|
stopNumber: 3, city: "Philadelphia", state: "PA",
|
|
coordinate: CLLocationCoordinate2D(latitude: 39.9061, longitude: -75.1665),
|
|
arrivalDate: base.addingTimeInterval(172800), departureDate: base.addingTimeInterval(259200),
|
|
games: ["game3"], stadium: "Citizens Bank Park"
|
|
),
|
|
TripStop(
|
|
stopNumber: 4, city: "Washington", state: "DC",
|
|
coordinate: CLLocationCoordinate2D(latitude: 38.8730, longitude: -77.0074),
|
|
arrivalDate: base.addingTimeInterval(259200), departureDate: base.addingTimeInterval(345600),
|
|
games: ["game4"], stadium: "Nationals Park"
|
|
)
|
|
]
|
|
}
|
|
|
|
private static func westCoastStops() -> [TripStop] {
|
|
let base = Date()
|
|
return [
|
|
TripStop(
|
|
stopNumber: 1, city: "Seattle", state: "WA",
|
|
coordinate: CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3323),
|
|
arrivalDate: base, departureDate: base.addingTimeInterval(86400),
|
|
games: ["game1"], stadium: "T-Mobile Park"
|
|
),
|
|
TripStop(
|
|
stopNumber: 2, city: "San Francisco", state: "CA",
|
|
coordinate: CLLocationCoordinate2D(latitude: 37.7680, longitude: -122.3877),
|
|
arrivalDate: base.addingTimeInterval(86400), departureDate: base.addingTimeInterval(172800),
|
|
games: ["game2"], stadium: "Chase Center"
|
|
),
|
|
TripStop(
|
|
stopNumber: 3, city: "San Francisco", state: "CA",
|
|
coordinate: CLLocationCoordinate2D(latitude: 37.7786, longitude: -122.3893),
|
|
arrivalDate: base.addingTimeInterval(172800), departureDate: base.addingTimeInterval(259200),
|
|
games: ["game3"], stadium: "Oracle Park"
|
|
),
|
|
TripStop(
|
|
stopNumber: 4, city: "Los Angeles", state: "CA",
|
|
coordinate: CLLocationCoordinate2D(latitude: 34.0739, longitude: -118.2400),
|
|
arrivalDate: base.addingTimeInterval(259200), departureDate: base.addingTimeInterval(345600),
|
|
games: ["game4"], stadium: "Dodger Stadium"
|
|
)
|
|
]
|
|
}
|
|
|
|
private static func centralStops() -> [TripStop] {
|
|
let base = Date()
|
|
return [
|
|
TripStop(
|
|
stopNumber: 1, city: "Chicago", state: "IL",
|
|
coordinate: CLLocationCoordinate2D(latitude: 41.8299, longitude: -87.6338),
|
|
arrivalDate: base, departureDate: base.addingTimeInterval(86400),
|
|
games: ["game1"], stadium: "Guaranteed Rate Field"
|
|
),
|
|
TripStop(
|
|
stopNumber: 2, city: "Milwaukee", state: "WI",
|
|
coordinate: CLLocationCoordinate2D(latitude: 43.0280, longitude: -87.9712),
|
|
arrivalDate: base.addingTimeInterval(86400), departureDate: base.addingTimeInterval(172800),
|
|
games: ["game2"], stadium: "American Family Field"
|
|
),
|
|
TripStop(
|
|
stopNumber: 3, city: "Minneapolis", state: "MN",
|
|
coordinate: CLLocationCoordinate2D(latitude: 44.9817, longitude: -93.2776),
|
|
arrivalDate: base.addingTimeInterval(172800), departureDate: base.addingTimeInterval(259200),
|
|
games: ["game3"], stadium: "Target Field"
|
|
),
|
|
TripStop(
|
|
stopNumber: 4, city: "Detroit", state: "MI",
|
|
coordinate: CLLocationCoordinate2D(latitude: 42.3390, longitude: -83.0485),
|
|
arrivalDate: base.addingTimeInterval(259200), departureDate: base.addingTimeInterval(345600),
|
|
games: ["game4"], stadium: "Comerica Park"
|
|
)
|
|
]
|
|
}
|
|
|
|
private static func crossCountryStops() -> [TripStop] {
|
|
let base = Date()
|
|
return [
|
|
TripStop(
|
|
stopNumber: 1, city: "New York", state: "NY",
|
|
coordinate: CLLocationCoordinate2D(latitude: 40.7571, longitude: -73.8458),
|
|
arrivalDate: base, departureDate: base.addingTimeInterval(86400),
|
|
games: ["game1"], stadium: "Citi Field"
|
|
),
|
|
TripStop(
|
|
stopNumber: 2, city: "Chicago", state: "IL",
|
|
coordinate: CLLocationCoordinate2D(latitude: 41.9484, longitude: -87.6553),
|
|
arrivalDate: base.addingTimeInterval(86400), departureDate: base.addingTimeInterval(172800),
|
|
games: ["game2"], stadium: "Wrigley Field"
|
|
),
|
|
TripStop(
|
|
stopNumber: 3, city: "Denver", state: "CO",
|
|
coordinate: CLLocationCoordinate2D(latitude: 39.7559, longitude: -104.9942),
|
|
arrivalDate: base.addingTimeInterval(172800), departureDate: base.addingTimeInterval(259200),
|
|
games: ["game3"], stadium: "Coors Field"
|
|
),
|
|
TripStop(
|
|
stopNumber: 4, city: "Los Angeles", state: "CA",
|
|
coordinate: CLLocationCoordinate2D(latitude: 34.0739, longitude: -118.2400),
|
|
arrivalDate: base.addingTimeInterval(259200), departureDate: base.addingTimeInterval(345600),
|
|
games: ["game4"], stadium: "Dodger Stadium"
|
|
)
|
|
]
|
|
}
|
|
|
|
private static func buildDummyTrips() -> [Trip] {
|
|
let prefs = TripPreferences(sports: [.mlb])
|
|
|
|
func makeTrip(name: String, stops: [TripStop]) -> Trip {
|
|
var segments: [TravelSegment] = []
|
|
for i in 0..<(stops.count - 1) {
|
|
segments.append(TravelSegment(
|
|
fromLocation: LocationInput(name: stops[i].city, coordinate: stops[i].coordinate),
|
|
toLocation: LocationInput(name: stops[i + 1].city, coordinate: stops[i + 1].coordinate),
|
|
travelMode: .drive,
|
|
distanceMeters: 400_000,
|
|
durationSeconds: 14400
|
|
))
|
|
}
|
|
return Trip(
|
|
name: name,
|
|
preferences: prefs,
|
|
stops: stops,
|
|
travelSegments: segments,
|
|
totalGames: stops.count,
|
|
totalDistanceMeters: Double(segments.count) * 400_000,
|
|
totalDrivingSeconds: Double(segments.count) * 14400
|
|
)
|
|
}
|
|
|
|
return [
|
|
makeTrip(name: "East Coast", stops: eastCoastStops()),
|
|
makeTrip(name: "West Coast", stops: westCoastStops()),
|
|
makeTrip(name: "Central", stops: centralStops()),
|
|
makeTrip(name: "Cross Country", stops: crossCountryStops())
|
|
]
|
|
}
|
|
|
|
enum ExportError: LocalizedError {
|
|
case renderingFailed
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .renderingFailed: return "Failed to render image to PNG"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Debug Export Progress View
|
|
|
|
struct DebugExportProgressView: View {
|
|
@Bindable var exporter: DebugShareExporter
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var copiedPath = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 24) {
|
|
Spacer()
|
|
|
|
if exporter.isExporting {
|
|
ProgressView(value: exporter.progress) {
|
|
Text(exporter.currentStep)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.horizontal)
|
|
|
|
Text("\(exporter.exportedCount) of \(exporter.totalCount) exported")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
} else if let error = exporter.error {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(.red)
|
|
Text("Export Failed")
|
|
.font(.headline)
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
} else if let path = exporter.exportPath {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(.green)
|
|
Text("Export Complete!")
|
|
.font(.headline)
|
|
Text("\(exporter.exportedCount) images exported")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text(path)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundStyle(.secondary)
|
|
.textSelection(.enabled)
|
|
.padding(.horizontal)
|
|
|
|
Button {
|
|
UIPasteboard.general.string = path
|
|
copiedPath = true
|
|
} label: {
|
|
Label(
|
|
copiedPath ? "Copied!" : "Copy Path",
|
|
systemImage: copiedPath ? "checkmark" : "doc.on.doc"
|
|
)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
} else {
|
|
Text("Ready to export")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.navigationTitle("Share Export")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("Done") { dismiss() }
|
|
.disabled(exporter.isExporting)
|
|
}
|
|
}
|
|
}
|
|
.presentationDetents([.medium])
|
|
.interactiveDismissDisabled(exporter.isExporting)
|
|
}
|
|
}
|
|
|
|
#endif
|