feat: redesign all share cards, remove unused achievement types, fix sport selector
Redesign trip, progress, and achievement share cards with premium sports-media aesthetic. Remove unused milestone/context achievement card types (only used in debug exporter). Fix gold text unreadable in light mode. Fix sport selector to only show stroke on selected sport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,7 +83,7 @@ struct AchievementsListView: View {
|
||||
let earned = displayAchievements.filter { $0.isEarned }.count
|
||||
let total = displayAchievements.count
|
||||
let progress = total > 0 ? Double(earned) / Double(total) : 0
|
||||
let completedGold = Color(hex: "FFD700")
|
||||
let completedGold = colorScheme == .dark ? Color(hex: "FFD700") : Color(hex: "B8860B")
|
||||
let filterTitle = selectedSport?.displayName ?? "All Sports"
|
||||
let accentColor = selectedSport?.themeColor ?? Theme.warmOrange
|
||||
|
||||
@@ -297,8 +297,10 @@ struct AchievementCard: View {
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
// Gold color for completed achievements
|
||||
private let completedGold = Color(hex: "FFD700")
|
||||
// Gold that's readable in both light and dark mode
|
||||
private var completedGold: Color {
|
||||
colorScheme == .dark ? Color(hex: "FFD700") : Color(hex: "B8860B")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Theme.Spacing.sm) {
|
||||
@@ -460,8 +462,10 @@ struct AchievementDetailSheet: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
// Gold color for completed achievements
|
||||
private let completedGold = Color(hex: "FFD700")
|
||||
// Gold that's readable in both light and dark mode
|
||||
private var completedGold: Color {
|
||||
colorScheme == .dark ? Color(hex: "FFD700") : Color(hex: "B8860B")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -34,8 +34,8 @@ final class DebugShareExporter {
|
||||
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
|
||||
// spotlight = 1 * achievements, collection ~5, progress 12, trips 4, icons 1
|
||||
totalCount = achievementCount + 5 + 12 + 4 + 1
|
||||
|
||||
do {
|
||||
// Step 1: Create export directory
|
||||
@@ -51,13 +51,10 @@ final class DebugShareExporter {
|
||||
let engine = AchievementEngine(modelContext: modelContext)
|
||||
_ = try await engine.recalculateAllAchievements()
|
||||
|
||||
// Step 4: Export achievement spotlight + milestone cards
|
||||
// Step 4: Export achievement spotlight 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(
|
||||
@@ -68,21 +65,12 @@ final class DebugShareExporter {
|
||||
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
|
||||
@@ -139,36 +127,7 @@ final class DebugShareExporter {
|
||||
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
|
||||
// Step 6: Export progress cards
|
||||
currentStep = "Exporting progress cards..."
|
||||
let progressTheme = ShareThemePreferences.theme(for: .stadiumProgress)
|
||||
let progressDir = exportDir.appendingPathComponent("progress")
|
||||
@@ -204,7 +163,7 @@ final class DebugShareExporter {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: Export trip cards
|
||||
// Step 7: Export trip cards
|
||||
currentStep = "Exporting trip cards..."
|
||||
let tripTheme = ShareThemePreferences.theme(for: .tripSummary)
|
||||
let tripDir = exportDir.appendingPathComponent("trips")
|
||||
@@ -220,7 +179,7 @@ final class DebugShareExporter {
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
// Step 9: Export sports icon
|
||||
// Step 8: Export sports icon
|
||||
currentStep = "Exporting sports icon..."
|
||||
let iconDir = exportDir.appendingPathComponent("icons")
|
||||
if let iconData = SportsIconImageGenerator.generatePNGData(),
|
||||
@@ -244,6 +203,184 @@ final class DebugShareExporter {
|
||||
isExporting = false
|
||||
}
|
||||
|
||||
// MARK: - Export Achievement Samples
|
||||
|
||||
func exportAchievementSamples() async {
|
||||
guard !isExporting else { return }
|
||||
isExporting = true
|
||||
error = nil
|
||||
exportPath = nil
|
||||
exportedCount = 0
|
||||
|
||||
do {
|
||||
currentStep = "Creating export directory..."
|
||||
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/samples_\(timestamp)")
|
||||
try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true)
|
||||
|
||||
// Pick a few representative achievements across sports
|
||||
let defs = AchievementRegistry.all
|
||||
let sampleDefs = [
|
||||
defs.first { $0.sport == .mlb } ?? defs[0],
|
||||
defs.first { $0.sport == .nba } ?? defs[1],
|
||||
defs.first { $0.sport == .nhl } ?? defs[2],
|
||||
defs.first { $0.name.lowercased().contains("complete") } ?? defs[3],
|
||||
defs.first { $0.category == .journey } ?? defs[min(4, defs.count - 1)]
|
||||
]
|
||||
|
||||
totalCount = sampleDefs.count
|
||||
|
||||
let spotlightTheme = ShareThemePreferences.theme(for: .achievementSpotlight)
|
||||
|
||||
for def in sampleDefs {
|
||||
let achievement = AchievementProgress(
|
||||
definition: def,
|
||||
currentProgress: totalRequired(for: def),
|
||||
totalRequired: totalRequired(for: def),
|
||||
hasStoredAchievement: true,
|
||||
earnedAt: Date()
|
||||
)
|
||||
|
||||
let safeName = def.id.replacingOccurrences(of: " ", with: "_")
|
||||
|
||||
currentStep = "Spotlight: \(def.name)"
|
||||
let spotlightContent = AchievementSpotlightContent(achievement: achievement)
|
||||
let spotlightImage = try await spotlightContent.render(theme: spotlightTheme)
|
||||
try savePNG(spotlightImage, to: exportDir.appendingPathComponent("spotlight_\(safeName).png"))
|
||||
exportedCount += 1
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
exportPath = exportDir.path
|
||||
currentStep = "Export complete!"
|
||||
print("DEBUG SAMPLES: \(exportDir.path)")
|
||||
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
currentStep = "Export failed"
|
||||
print("DEBUG SAMPLE ERROR: \(error)")
|
||||
}
|
||||
|
||||
isExporting = false
|
||||
}
|
||||
|
||||
// MARK: - Export Progress Samples
|
||||
|
||||
func exportProgressSamples() async {
|
||||
guard !isExporting else { return }
|
||||
isExporting = true
|
||||
error = nil
|
||||
exportPath = nil
|
||||
exportedCount = 0
|
||||
|
||||
do {
|
||||
currentStep = "Creating export directory..."
|
||||
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/progress_samples_\(timestamp)")
|
||||
try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true)
|
||||
|
||||
let allStadiums = AppDataProvider.shared.stadiums
|
||||
let theme = ShareThemePreferences.theme(for: .stadiumProgress)
|
||||
|
||||
// Render real progress cards at different percentages per sport
|
||||
let sports: [Sport] = [.mlb, .nba, .nhl]
|
||||
let percentages = [25, 50, 75, 100]
|
||||
totalCount = sports.count * percentages.count
|
||||
|
||||
for sport in sports {
|
||||
let sportStadiums = allStadiums.filter { $0.sport == sport }
|
||||
let total = sportStadiums.count
|
||||
|
||||
for pct in percentages {
|
||||
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
|
||||
currentStep = "Progress: \(sport.rawValue) \(pct)%"
|
||||
|
||||
let content = ProgressShareContent(
|
||||
progress: leagueProgress,
|
||||
tripCount: tripCount
|
||||
)
|
||||
let image = try await content.render(theme: theme)
|
||||
try savePNG(image, to: exportDir.appendingPathComponent("\(sport.rawValue)_\(pct).png"))
|
||||
exportedCount += 1
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
|
||||
exportPath = exportDir.path
|
||||
currentStep = "Export complete!"
|
||||
print("DEBUG PROGRESS SAMPLES: \(exportDir.path)")
|
||||
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
currentStep = "Export failed"
|
||||
print("DEBUG PROGRESS SAMPLE ERROR: \(error)")
|
||||
}
|
||||
|
||||
isExporting = false
|
||||
}
|
||||
|
||||
// MARK: - Export Trip Samples
|
||||
|
||||
func exportTripSamples() async {
|
||||
guard !isExporting else { return }
|
||||
isExporting = true
|
||||
error = nil
|
||||
exportPath = nil
|
||||
exportedCount = 0
|
||||
do {
|
||||
currentStep = "Creating export directory..."
|
||||
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/trip_samples_\(timestamp)")
|
||||
try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true)
|
||||
|
||||
let theme = ShareThemePreferences.theme(for: .tripSummary)
|
||||
let dummyTrips = Self.buildDummyTrips()
|
||||
totalCount = dummyTrips.count
|
||||
|
||||
for trip in dummyTrips {
|
||||
currentStep = "Trip: \(trip.name)"
|
||||
let content = TripShareContent(trip: trip)
|
||||
let image = try await content.render(theme: theme)
|
||||
let safeName = trip.name.replacingOccurrences(of: " ", with: "_")
|
||||
try savePNG(image, to: exportDir.appendingPathComponent("\(safeName).png"))
|
||||
exportedCount += 1
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
exportPath = exportDir.path
|
||||
currentStep = "Export complete!"
|
||||
print("DEBUG TRIP SAMPLES: \(exportDir.path)")
|
||||
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
currentStep = "Export failed"
|
||||
print("DEBUG TRIP SAMPLE ERROR: \(error)")
|
||||
}
|
||||
|
||||
isExporting = false
|
||||
}
|
||||
|
||||
// MARK: - Add All Stadium Visits
|
||||
|
||||
func addAllStadiumVisits(modelContext: ModelContext) async {
|
||||
@@ -311,9 +448,7 @@ final class DebugShareExporter {
|
||||
|
||||
let subdirs = [
|
||||
"achievements/spotlight",
|
||||
"achievements/milestone",
|
||||
"achievements/collection",
|
||||
"achievements/context",
|
||||
"progress",
|
||||
"trips",
|
||||
"icons"
|
||||
|
||||
@@ -360,6 +360,33 @@ struct SettingsView: View {
|
||||
Label("Export All Shareables", systemImage: "square.and.arrow.up.on.square")
|
||||
}
|
||||
|
||||
Button {
|
||||
showExportProgress = true
|
||||
Task {
|
||||
await exporter.exportAchievementSamples()
|
||||
}
|
||||
} label: {
|
||||
Label("Export Achievement Samples", systemImage: "paintbrush")
|
||||
}
|
||||
|
||||
Button {
|
||||
showExportProgress = true
|
||||
Task {
|
||||
await exporter.exportProgressSamples()
|
||||
}
|
||||
} label: {
|
||||
Label("Export Progress Samples", systemImage: "chart.bar.fill")
|
||||
}
|
||||
|
||||
Button {
|
||||
showExportProgress = true
|
||||
Task {
|
||||
await exporter.exportTripSamples()
|
||||
}
|
||||
} label: {
|
||||
Label("Export Trip Samples", systemImage: "car.fill")
|
||||
}
|
||||
|
||||
Button {
|
||||
Task { await exporter.addAllStadiumVisits(modelContext: modelContext) }
|
||||
} label: {
|
||||
|
||||
Reference in New Issue
Block a user