1→import SwiftUI 2→ 3→struct RecoveryProgressView: View { 4→ @EnvironmentObject var dataManager: DataManager 5→ 6→ var body: some View { 7→ ScrollView { 8→ LazyVStack(spacing: 24) { 9→ // Sobriety Overview - Days Clean 10→ SobrietyOverviewCard() 11→ 12→ // Level Card 13→ LevelCard() 14→ 15→ // Recovery Journeys Summary 16→ RecoveryJourneySummaryCard() 17→ 18→ // XP Breakdown 19→ XPBreakdownCard() 20→ 21→ // Badges 22→ BadgesCard() 23→ 24→ // Milestones 25→ MilestonesCard() 26→ 27→ // Statistics 28→ StatisticsCard() 29→ } 30→ .padding(24) 31→ } 32→ .navigationTitle("Progresso") 33→ .onAppear { 34→ // Award milestone XP on view appear 35→ awardMilestoneXP() 36→ } 37→ } 38→ 39→ private func awardMilestoneXP() { 40→ guard let journey = dataManager.recoveryJourneys.first, 41→ let progress = dataManager.progress else { return } 42→ 43→ let reachedMilestones = journey.reachedMilestones 44→ 45→ for milestone in reachedMilestones { 46→ if !progress.reachedMilestoneIds.contains(milestone.id) { 47→ _ = progress.unlockMilestone(milestone) 48→ } 49→ } 50→ 51→ // Check badges 52→ _ = progress.checkAndUnlockBadges() 53→ 54→ dataManager.saveContext() 55→ } 56→} 57→ 58→// MARK: - Level Card 59→ 60→struct LevelCard: View { 61→ @EnvironmentObject var dataManager: DataManager 62→ 63→ var body: some View { 64→ let progress = dataManager.progress 65→ let level = progress?.level ?? RecoveryLevel.levels[0] 66→ 67→ VStack(spacing: 16) { 68→ HStack { 69→ VStack(alignment: .leading, spacing: 4) { 70→ Text("NÍVEL \(level.level)") 71→ .font(.caption) 72→ .fontWeight(.semibold) 73→ .foregroundColor(.secondary) 74→ .tracking(2) 75→ 76→ Text(level.titlePT) 77→ .font(.title) 78→ .fontWeight(.bold) 79→ } 80→ 81→ Spacer() 82→ 83→ ZStack { 84→ Circle() 85→ .stroke(Color.gray.opacity(0.2), lineWidth: 8) 86→ .frame(width: 80, height: 80) 87→ 88→ Circle() 89→ .trim(from: 0, to: progress?.levelProgress ?? 0) 90→ .stroke( 91→ LinearGradient( 92→ colors: [.purple, .blue], 93→ startPoint: .topLeading, 94→ endPoint: .bottomTrailing 95→ ), 96→ style: StrokeStyle(lineWidth: 8, lineCap: .round) 97→ ) 98→ .frame(width: 80, height: 80) 99→ .rotationEffect(.degrees(-90)) 100→ 101→ VStack(spacing: 0) { 102→ Text("\(Int((progress?.levelProgress ?? 0) * 100))%") 103→ .font(.headline) 104→ .fontWeight(.bold) 105→ } 106→ } 107→ } 108→ 109→ // XP Bar 110→ VStack(alignment: .leading, spacing: 4) { 111→ HStack { 112→ Text("\(progress?.totalXP ?? 0) XP") 113→ .font(.caption) 114→ .fontWeight(.medium) 115→ 116→ Spacer() 117→ 118→ Text("\(progress?.xpForNextLevel ?? 0) XP para próximo nível") 119→ .font(.caption) 120→ .foregroundColor(.secondary) 121→ } 122→ 123→ GeometryReader { geometry in 124→ ZStack(alignment: .leading) { 125→ Capsule() 126→ .fill(Color.gray.opacity(0.2)) 127→ .frame(height: 8) 128→ 129→ Capsule() 130→ .fill( 131→ LinearGradient( 132→ colors: [.purple, .blue], 133→ startPoint: .leading, 134→ endPoint: .trailing 135→ ) 136→ ) 137→ .frame(width: geometry.size.width * (progress?.levelProgress ?? 0), height: 8) 138→ } 139→ } 140→ .frame(height: 8) 141→ } 142→ } 143→ .padding() 144→ .background( 145→ RoundedRectangle(cornerRadius: 16) 146→ .fill(.ultraThinMaterial) 147→ ) 148→ } 149→} 150→ 151→// MARK: - XP Breakdown Card 152→ 153→struct XPBreakdownCard: View { 154→ @EnvironmentObject var dataManager: DataManager 155→ 156→ var body: some View { 157→ let progress = dataManager.progress 158→ 159→ VStack(alignment: .leading, spacing: 16) { 160→ Text("Origem do XP") 161→ .font(.headline) 162→ 163→ VStack(spacing: 12) { 164→ XPRow(icon: "checkmark.circle.fill", title: "Check-ins", xp: progress?.dailyCheckInXP ?? 0, color: .green) 165→ XPRow(icon: "trophy.fill", title: "Milestones", xp: progress?.milestoneXP ?? 0, color: .yellow) 166→ XPRow(icon: "book.fill", title: "Journaling", xp: progress?.journalXP ?? 0, color: .blue) 167→ XPRow(icon: "shield.fill", title: "Crises Superadas", xp: progress?.crisisSurvivedXP ?? 0, color: .red) 168→ XPRow(icon: "figure.run", title: "Atividades", xp: progress?.activityXP ?? 0, color: .orange) 169→ } 170→ } 171→ .padding() 172→ .background( 173→ RoundedRectangle(cornerRadius: 16) 174→ .fill(.ultraThinMaterial) 175→ ) 176→ } 177→} 178→ 179→struct XPRow: View { 180→ let icon: String 181→ let title: String 182→ let xp: Int 183→ let color: Color 184→ 185→ var body: some View { 186→ HStack { 187→ Image(systemName: icon) 188→ .foregroundColor(color) 189→ .frame(width: 24) 190→ 191→ Text(title) 192→ 193→ Spacer() 194→ 195→ Text("+\(xp) XP") 196→ .fontWeight(.medium) 197→ .foregroundColor(.secondary) 198→ } 199→ } 200→} 201→ 202→// MARK: - Badges Card 203→ 204→struct BadgesCard: View { 205→ @EnvironmentObject var dataManager: DataManager 206→ 207→ var body: some View { 208→ let progress = dataManager.progress 209→ let earnedBadges = progress?.earnedBadges ?? [] 210→ let availableBadges = progress?.availableBadges ?? [] 211→ 212→ VStack(alignment: .leading, spacing: 16) { 213→ HStack { 214→ Text("Conquistas") 215→ .font(.headline) 216→ 217→ Spacer() 218→ 219→ Text("\(earnedBadges.count)/\(Badge.all.count)") 220→ .font(.caption) 221→ .foregroundColor(.secondary) 222→ } 223→ 224→ // Earned badges 225→ if !earnedBadges.isEmpty { 226→ ScrollView(.horizontal, showsIndicators: false) { 227→ HStack(spacing: 16) { 228→ ForEach(earnedBadges) { badge in 229→ BadgeView(badge: badge, isEarned: true) 230→ } 231→ } 232→ } 233→ } 234→ 235→ // Locked badges 236→ if !availableBadges.isEmpty { 237→ Text("Próximas") 238→ .font(.subheadline) 239→ .foregroundColor(.secondary) 240→ 241→ ScrollView(.horizontal, showsIndicators: false) { 242→ HStack(spacing: 16) { 243→ ForEach(availableBadges.prefix(5)) { badge in 244→ BadgeView(badge: badge, isEarned: false) 245→ } 246→ } 247→ } 248→ } 249→ } 250→ .padding() 251→ .background( 252→ RoundedRectangle(cornerRadius: 16) 253→ .fill(.ultraThinMaterial) 254→ ) 255→ } 256→} 257→ 258→struct BadgeView: View { 259→ let badge: Badge 260→ let isEarned: Bool 261→ 262→ var body: some View { 263→ VStack(spacing: 8) { 264→ Image(systemName: badge.icon) 265→ .font(.title) 266→ .foregroundColor(isEarned ? .yellow : .gray) 267→ 268→ Text(badge.namePT) 269→ .font(.caption) 270→ .fontWeight(.medium) 271→ .multilineTextAlignment(.center) 272→ .lineLimit(2) 273→ } 274→ .frame(width: 80) 275→ .padding() 276→ .background( 277→ RoundedRectangle(cornerRadius: 12) 278→ .fill(isEarned ? Color.yellow.opacity(0.1) : Color.gray.opacity(0.1)) 279→ ) 280→ .opacity(isEarned ? 1 : 0.5) 281→ } 282→} 283→ 284→// MARK: - Milestones Card 285→ 286→struct MilestonesCard: View { 287→ @EnvironmentObject var dataManager: DataManager 288→ 289→ var body: some View { 290→ let journey = dataManager.recoveryJourneys.first 291→ let reached = journey?.reachedMilestones ?? [] 292→ let nextMilestone = journey?.nextMilestone 293→ 294→ VStack(alignment: .leading, spacing: 16) { 295→ Text("Milestones") 296→ .font(.headline) 297→ 298→ VStack(spacing: 12) { 299→ ForEach(Milestone.all.prefix(6)) { milestone in 300→ let isReached = reached.contains { $0.id == milestone.id } 301→ let isNext = nextMilestone?.id == milestone.id 302→ 303→ HStack { 304→ Image(systemName: milestone.icon) 305→ .foregroundColor(isReached ? .yellow : (isNext ? .blue : .gray)) 306→ .frame(width: 24) 307→ 308→ VStack(alignment: .leading) { 309→ Text(milestone.titlePT) 310→ .fontWeight(isNext ? .bold : .regular) 311→ 312→ Text("\(milestone.days) dias") 313→ .font(.caption) 314→ .foregroundColor(.secondary) 315→ } 316→ 317→ Spacer() 318→ 319→ if isReached { 320→ Image(systemName: "checkmark.circle.fill") 321→ .foregroundColor(.green) 322→ } else if isNext { 323→ Text("\(journey?.daysToNextMilestone ?? 0)d") 324→ .font(.caption) 325→ .foregroundColor(.blue) 326→ } 327→ } 328→ .padding(.vertical, 4) 329→ .opacity(isReached ? 1 : 0.6) 330→ } 331→ } 332→ } 333→ .padding() 334→ .background( 335→ RoundedRectangle(cornerRadius: 16) 336→ .fill(.ultraThinMaterial) 337→ ) 338→ } 339→} 340→ 341→// MARK: - Statistics Card 342→ 343→struct StatisticsCard: View { 344→ @EnvironmentObject var dataManager: DataManager 345→ 346→ var body: some View { 347→ let progress = dataManager.progress 348→ let daysClean = progress?.daysClean ?? dataManager.getTotalCleanDays() 349→ 350→ VStack(alignment: .leading, spacing: 16) { 351→ Text("Estatísticas") 352→ .font(.headline) 353→ 354→ LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 16) { 355→ StatBox(title: "Dias Limpos", value: "\(daysClean)", icon: "leaf.fill", color: .green) 356→ StatBox(title: "Check-ins", value: "\(progress?.totalCheckIns ?? 0)", icon: "checkmark.circle.fill", color: .blue) 357→ StatBox(title: "Streak Check-in", value: "\(progress?.currentCheckInStreak ?? 0)d", icon: "flame.fill", color: .orange) 358→ StatBox(title: "Crises Vencidas", value: "\(progress?.totalCrisesSurvived ?? 0)", icon: "shield.fill", color: .red) 359→ StatBox(title: "Respirações", value: "\(progress?.totalBreathingExercises ?? 0)", icon: "wind", color: .cyan) 360→ StatBox(title: "XP Ganho", value: formatXP(progress?.totalXP ?? 0), icon: "bolt.fill", color: .purple) 361→ } 362→ 363→ Divider() 364→ 365→ // XP by source breakdown 366→ HStack(spacing: 16) { 367→ XPSourcePill(icon: "checkmark.circle", label: "Check-ins", xp: progress?.dailyCheckInXP ?? 0, color: .green) 368→ XPSourcePill(icon: "trophy", label: "Marcos", xp: progress?.milestoneXP ?? 0, color: .yellow) 369→ XPSourcePill(icon: "shield", label: "Crises", xp: progress?.crisisSurvivedXP ?? 0, color: .red) 370→ XPSourcePill(icon: "figure.run", label: "Atividades", xp: progress?.activityXP ?? 0, color: .orange) 371→ } 372→ } 373→ .padding() 374→ .background( 375→ RoundedRectangle(cornerRadius: 16) 376→ .fill(.ultraThinMaterial) 377→ ) 378→ } 379→ 380→ private func formatXP(_ xp: Int) -> String { 381→ if xp >= 1000 { 382→ return String(format: "%.1fK", Double(xp) / 1000.0) 383→ } 384→ return "\(xp)" 385→ } 386→} 387→ 388→struct XPSourcePill: View { 389→ let icon: String 390→ let label: String 391→ let xp: Int 392→ let color: Color 393→ 394→ var body: some View { 395→ HStack(spacing: 4) { 396→ Image(systemName: icon) 397→ .font(.caption2) 398→ .foregroundColor(color) 399→ 400→ Text("+\(xp)") 401→ .font(.caption2) 402→ .fontWeight(.bold) 403→ } 404→ .padding(.horizontal, 8) 405→ .padding(.vertical, 4) 406→ .background( 407→ Capsule() 408→ .fill(color.opacity(0.1)) 409→ ) 410→ } 411→} 412→ 413→struct StatBox: View { 414→ let title: String 415→ let value: String 416→ let icon: String 417→ let color: Color 418→ 419→ var body: some View { 420→ VStack(spacing: 8) { 421→ Image(systemName: icon) 422→ .foregroundColor(color) 423→ 424→ Text(value) 425→ .font(.title2) 426→ .fontWeight(.bold) 427→ 428→ Text(title) 429→ .font(.caption) 430→ .foregroundColor(.secondary) 431→ } 432→ .frame(maxWidth: .infinity) 433→ .padding() 434→ .background( 435→ RoundedRectangle(cornerRadius: 12) 436→ .fill(color.opacity(0.1)) 437→ ) 438→ } 439→} 440→ 441→// MARK: - Sobriety Overview Card 442→ 443→struct SobrietyOverviewCard: View { 444→ @EnvironmentObject var dataManager: DataManager 445→ 446→ var body: some View { 447→ let daysClean = dataManager.progress?.daysClean ?? dataManager.getTotalCleanDays() 448→ let startDate = dataManager.progress?.sobrietyStartDate ?? Date() 449→ 450→ VStack(spacing: 20) { 451→ // Main days counter 452→ VStack(spacing: 8) { 453→ Text("DIAS EM RECUPERAÇÃO") 454→ .font(.caption) 455→ .fontWeight(.semibold) 456→ .foregroundColor(.secondary) 457→ .tracking(2) 458→ 459→ Text("\(daysClean)") 460→ .font(.system(size: 80, weight: .bold, design: .rounded)) 461→ .foregroundStyle( 462→ LinearGradient( 463→ colors: [.green, .mint], 464→ startPoint: .topLeading, 465→ endPoint: .bottomTrailing 466→ ) 467→ ) 468→ 469→ Text(streakMessage(days: daysClean)) 470→ .font(.headline) 471→ .foregroundColor(.secondary) 472→ } 473→ 474→ Divider() 475→ 476→ // Start date and details 477→ HStack(spacing: 24) { 478→ VStack(spacing: 4) { 479→ Image(systemName: "calendar.badge.clock") 480→ .font(.title2) 481→ .foregroundColor(.blue) 482→ Text("Início") 483→ .font(.caption2) 484→ .foregroundColor(.secondary) 485→ Text(formatDate(startDate)) 486→ .font(.caption) 487→ .fontWeight(.medium) 488→ } 489→ 490→ VStack(spacing: 4) { 491→ Image(systemName: "flame.fill") 492→ .font(.title2) 493→ .foregroundColor(.orange) 494→ Text("Streak") 495→ .font(.caption2) 496→ .foregroundColor(.secondary) 497→ Text("\(daysClean) dias") 498→ .font(.caption) 499→ .fontWeight(.medium) 500→ } 501→ 502→ VStack(spacing: 4) { 503→ Image(systemName: "trophy.fill") 504→ .font(.title2) 505→ .foregroundColor(.yellow) 506→ Text("Recorde") 507→ .font(.caption2) 508→ .foregroundColor(.secondary) 509→ Text("\(dataManager.getLongestStreak()) dias") 510→ .font(.caption) 511→ .fontWeight(.medium) 512→ } 513→ 514→ VStack(spacing: 4) { 515→ Image(systemName: "arrow.counterclockwise") 516→ .font(.title2) 517→ .foregroundColor(.red) 518→ Text("Recaídas") 519→ .font(.caption2) 520→ .foregroundColor(.secondary) 521→ Text("\(totalRelapses())") 522→ .font(.caption) 523→ .fontWeight(.medium) 524→ } 525→ } 526→ } 527→ .padding(24) 528→ .frame(maxWidth: .infinity) 529→ .background( 530→ RoundedRectangle(cornerRadius: 20) 531→ .fill(.ultraThinMaterial) 532→ .overlay( 533→ RoundedRectangle(cornerRadius: 20) 534→ .stroke( 535→ LinearGradient( 536→ colors: [.green.opacity(0.3), .mint.opacity(0.3)], 537→ startPoint: .topLeading, 538→ endPoint: .bottomTrailing 539→ ), 540→ lineWidth: 1 541→ ) 542→ ) 543→ ) 544→ } 545→ 546→ private func formatDate(_ date: Date) -> String { 547→ let formatter = DateFormatter() 548→ formatter.dateFormat = "dd/MM/yyyy" 549→ return formatter.string(from: date) 550→ } 551→ 552→ private func streakMessage(days: Int) -> String { 553→ switch days { 554→ case 0: return "Comece sua jornada hoje!" 555→ case 1: return "Primeiro dia! O mais difícil." 556→ case 2...6: return "Ótimo começo! Continue assim." 557→ case 7...13: return "Uma semana! Você é incrível." 558→ case 14...29: return "Duas semanas de força!" 559→ case 30...59: return "Um mês! Isso é sério." 560→ case 60...89: return "Dois meses de determinação!" 561→ case 90...179: return "90 dias! Você é um guerreiro." 562→ case 180...364: return "Meio ano de liberdade!" 563→ default: return "Mais de um ano! Lendário." 564→ } 565→ } 566→ 567→ private func totalRelapses() -> Int { 568→ dataManager.recoveryJourneys.reduce(0) { $0 + $1.totalRelapses } 569→ } 570→} 571→ 572→// MARK: - Recovery Journey Summary Card 573→ 574→struct RecoveryJourneySummaryCard: View { 575→ @EnvironmentObject var dataManager: DataManager 576→ 577→ var body: some View { 578→ VStack(alignment: .leading, spacing: 16) { 579→ HStack { 580→ Text("Jornadas de Recuperação") 581→ .font(.headline) 582→ 583→ Spacer() 584→ 585→ Text("\(dataManager.recoveryJourneys.filter { $0.currentStreak > 0 }.count)/\(dataManager.recoveryJourneys.count) ativas") 586→ .font(.caption) 587→ .foregroundColor(.secondary) 588→ } 589→ 590→ LazyVGrid(columns: [ 591→ GridItem(.flexible()), 592→ GridItem(.flexible()) 593→ ], spacing: 12) { 594→ ForEach(dataManager.recoveryJourneys) { journey in 595→ JourneyProgressCard(journey: journey) 596→ } 597→ } 598→ } 599→ .padding() 600→ .background( 601→ RoundedRectangle(cornerRadius: 16) 602→ .fill(.ultraThinMaterial) 603→ ) 604→ } 605→} 606→ 607→struct JourneyProgressCard: View { 608→ let journey: RecoveryJourney 609→ 610→ var body: some View { 611→ VStack(alignment: .leading, spacing: 8) { 612→ HStack { 613→ Image(systemName: journey.viceType.icon) 614→ .foregroundColor(journey.viceType.color) 615→ 616→ Text(journey.viceType.displayName) 617→ .font(.subheadline) 618→ .fontWeight(.medium) 619→ 620→ Spacer() 621→ 622→ if journey.currentStreak >= 7 { 623→ Image(systemName: "checkmark.seal.fill") 624→ .foregroundColor(.green) 625→ .font(.caption) 626→ } 627→ } 628→ 629→ HStack(alignment: .bottom) { 630→ Text("\(journey.currentStreak)") 631→ .font(.title) 632→ .fontWeight(.bold) 633→ .foregroundColor(journey.viceType.color) 634→ 635→ Text("dias") 636→ .font(.caption) 637→ .foregroundColor(.secondary) 638→ .padding(.bottom, 4) 639→ 640→ Spacer() 641→ 642→ // Progress to next milestone 643→ if let next = journey.nextMilestone { 644→ VStack(alignment: .trailing, spacing: 2) { 645→ Text("\(journey.daysToNextMilestone)d") 646→ .font(.caption2) 647→ .fontWeight(.bold) 648→ .foregroundColor(.blue) 649→ Text("→ \(next.titlePT)") 650→ .font(.caption2) 651→ .foregroundColor(.secondary) 652→ } 653→ } 654→ } 655→ 656→ // Progress bar 657→ GeometryReader { geo in 658→ let progress = journey.nextMilestone.map { milestone in 659→ Double(journey.currentStreak) / Double(milestone.days) 660→ } ?? 1.0 661→ 662→ ZStack(alignment: .leading) { 663→ Capsule() 664→ .fill(Color.gray.opacity(0.2)) 665→ .frame(height: 4) 666→ 667→ Capsule() 668→ .fill(journey.viceType.color) 669→ .frame(width: geo.size.width * min(progress, 1.0), height: 4) 670→ } 671→ } 672→ .frame(height: 4) 673→ 674→ // Milestones reached 675→ HStack(spacing: 4) { 676→ ForEach(Milestone.all.prefix(5)) { milestone in 677→ let reached = journey.currentStreak >= milestone.days 678→ Circle() 679→ .fill(reached ? journey.viceType.color : Color.gray.opacity(0.3)) 680→ .frame(width: 6, height: 6) 681→ } 682→ Spacer() 683→ Text("\(journey.reachedMilestones.count) marcos") 684→ .font(.caption2) 685→ .foregroundColor(.secondary) 686→ } 687→ } 688→ .padding(12) 689→ .background( 690→ RoundedRectangle(cornerRadius: 12) 691→ .fill(journey.viceType.color.opacity(0.05)) 692→ .overlay( 693→ RoundedRectangle(cornerRadius: 12) 694→ .stroke(journey.viceType.color.opacity(0.2), lineWidth: 1) 695→ ) 696→ ) 697→ } 698→} 699→ 700→#Preview { 701→ RecoveryProgressView() 702→ .environmentObject(DataManager.shared) 703→} 704→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.