1→import SwiftUI 2→import ServiceManagement 3→ 4→@main 5→struct BCBDashboardApp: App { 6→ @StateObject private var dashboardViewModel = DashboardViewModel() 7→ @AppStorage("showMenuBarWidget") private var showMenuBarWidget: Bool = true 8→ 9→ var body: some Scene { 10→ WindowGroup { 11→ ContentView() 12→ .environmentObject(dashboardViewModel) 13→ } 14→ .windowStyle(.automatic) 15→ .defaultSize(width: 1200, height: 800) 16→ .commands { 17→ CommandGroup(replacing: .newItem) { } 18→ 19→ CommandMenu("Data") { 20→ Button("Refresh All") { 21→ Task { 22→ await dashboardViewModel.refreshAll() 23→ } 24→ } 25→ .keyboardShortcut("r", modifiers: .command) 26→ 27→ Divider() 28→ 29→ Button("Clear Cache") { 30→ dashboardViewModel.clearCache() 31→ } 32→ .keyboardShortcut("k", modifiers: [.command, .shift]) 33→ } 34→ 35→ CommandMenu("View") { 36→ Toggle("Show Menu Bar Widget", isOn: $showMenuBarWidget) 37→ } 38→ } 39→ 40→ Settings { 41→ SettingsView() 42→ } 43→ 44→ // Menu Bar Widget 45→ MenuBarExtra(isInserted: $showMenuBarWidget) { 46→ MenuBarWidgetView() 47→ .environmentObject(dashboardViewModel) 48→ } label: { 49→ MenuBarLabel(viewModel: dashboardViewModel) 50→ } 51→ .menuBarExtraStyle(.window) 52→ } 53→} 54→ 55→// MARK: - Menu Bar Label 56→ 57→struct MenuBarLabel: View { 58→ @ObservedObject var viewModel: DashboardViewModel 59→ 60→ var body: some View { 61→ HStack(spacing: 8) { 62→ // USD Value 63→ HStack(spacing: 2) { 64→ Text("$") 65→ .font(.system(size: 10, weight: .medium)) 66→ Text(viewModel.dollarQuote?.sellRate.toMenuBarString() ?? "--") 67→ .font(.system(size: 11, weight: .semibold, design: .rounded)) 68→ } 69→ 70→ // Separator 71→ Text("│") 72→ .font(.system(size: 9)) 73→ .foregroundStyle(.secondary) 74→ 75→ // SELIC Value 76→ HStack(spacing: 1) { 77→ Text(String(format: "%.1f", viewModel.selicTarget?.numericValue ?? 0.0)) 78→ .font(.system(size: 11, weight: .semibold, design: .rounded)) 79→ Text("%") 80→ .font(.system(size: 9, weight: .medium)) 81→ } 82→ } 83→ } 84→} 85→ 86→// MARK: - Menu Bar Widget View 87→ 88→struct MenuBarWidgetView: View { 89→ @EnvironmentObject var viewModel: DashboardViewModel 90→ @Environment(\.openWindow) private var openWindow 91→ 92→ var body: some View { 93→ VStack(spacing: 0) { 94→ // Header 95→ HStack { 96→ Image(systemName: "building.columns.fill") 97→ .foregroundStyle(Color.bcbBlue) 98→ Text("BCB Dashboard") 99→ .font(.headline) 100→ Spacer() 101→ if viewModel.isLoading { 102→ ProgressView() 103→ .controlSize(.small) 104→ } 105→ } 106→ .padding() 107→ .background(Color(NSColor.controlBackgroundColor)) 108→ 109→ Divider() 110→ 111→ // Currency Section 112→ VStack(spacing: 12) { 113→ MenuBarRateRow( 114→ icon: "dollarsign.circle.fill", 115→ iconColor: .bcbGreen, 116→ title: "Dólar (USD)", 117→ value: viewModel.dollarQuote?.sellRate.toBRL() ?? "--", 118→ subtitle: viewModel.dollarQuote?.formattedTime ?? "", 119→ change: viewModel.dollarChange 120→ ) 121→ 122→ MenuBarRateRow( 123→ icon: "eurosign.circle.fill", 124→ iconColor: .bcbBlue, 125→ title: "Euro (EUR)", 126→ value: viewModel.euroQuote?.sellRate.toBRL() ?? "--", 127→ subtitle: viewModel.euroQuote?.formattedTime ?? "", 128→ change: nil 129→ ) 130→ 131→ Divider() 132→ .padding(.vertical, 4) 133→ 134→ MenuBarRateRow( 135→ icon: "percent", 136→ iconColor: .bcbPurple, 137→ title: "SELIC Meta", 138→ value: String(format: "%.2f%%", viewModel.selicTarget?.numericValue ?? 0.0), 139→ subtitle: viewModel.selicTarget?.date ?? "", 140→ change: nil 141→ ) 142→ 143→ MenuBarRateRow( 144→ icon: "chart.line.uptrend.xyaxis", 145→ iconColor: .bcbYellow, 146→ title: "IPCA 12m", 147→ value: String(format: "%.2f%%", viewModel.ipca12m?.numericValue ?? 0.0), 148→ subtitle: viewModel.ipca12m?.date ?? "", 149→ change: nil 150→ ) 151→ 152→ if let selicExp = viewModel.focusStats.latestSelic { 153→ Divider() 154→ .padding(.vertical, 4) 155→ 156→ MenuBarRateRow( 157→ icon: "chart.bar.fill", 158→ iconColor: .bcbPink, 159→ title: "SELIC Esperada", 160→ value: String(format: "%.2f%%", selicExp.median), 161→ subtitle: selicExp.formattedMeeting, 162→ change: nil 163→ ) 164→ } 165→ } 166→ .padding() 167→ 168→ Divider() 169→ 170→ // Footer with actions 171→ HStack { 172→ if let lastUpdate = viewModel.lastUpdate { 173→ Text(lastUpdate.toRelativeString()) 174→ .font(.caption2) 175→ .foregroundStyle(.secondary) 176→ } 177→ 178→ Spacer() 179→ 180→ Button { 181→ Task { 182→ await viewModel.refreshAll() 183→ } 184→ } label: { 185→ Image(systemName: "arrow.clockwise") 186→ .font(.caption) 187→ } 188→ .buttonStyle(.borderless) 189→ .disabled(viewModel.isLoading) 190→ 191→ Divider() 192→ .frame(height: 16) 193→ 194→ Button { 195→ NSApplication.shared.activate(ignoringOtherApps: true) 196→ if let window = NSApplication.shared.windows.first(where: { $0.title.contains("BCB") || $0.contentView != nil }) { 197→ window.makeKeyAndOrderFront(nil) 198→ } 199→ } label: { 200→ Image(systemName: "macwindow") 201→ .font(.caption) 202→ } 203→ .buttonStyle(.borderless) 204→ 205→ Divider() 206→ .frame(height: 16) 207→ 208→ Button { 209→ NSApplication.shared.terminate(nil) 210→ } label: { 211→ Image(systemName: "power") 212→ .font(.caption) 213→ } 214→ .buttonStyle(.borderless) 215→ } 216→ .padding(.horizontal) 217→ .padding(.vertical, 8) 218→ .background(Color(NSColor.controlBackgroundColor)) 219→ } 220→ .frame(width: 280) 221→ } 222→} 223→ 224→// MARK: - Menu Bar Rate Row 225→ 226→struct MenuBarRateRow: View { 227→ let icon: String 228→ let iconColor: Color 229→ let title: String 230→ let value: String 231→ let subtitle: String 232→ let change: Double? 233→ 234→ var body: some View { 235→ HStack { 236→ Image(systemName: icon) 237→ .font(.title3) 238→ .foregroundStyle(iconColor) 239→ .frame(width: 24) 240→ 241→ VStack(alignment: .leading, spacing: 2) { 242→ Text(title) 243→ .font(.caption) 244→ .foregroundStyle(.secondary) 245→ Text(subtitle) 246→ .font(.caption2) 247→ .foregroundStyle(.tertiary) 248→ } 249→ 250→ Spacer() 251→ 252→ VStack(alignment: .trailing, spacing: 2) { 253→ Text(value) 254→ .font(.system(.body, design: .rounded)) 255→ .fontWeight(.semibold) 256→ 257→ if let change = change, change != 0 { 258→ HStack(spacing: 2) { 259→ Image(systemName: change >= 0 ? "arrow.up.right" : "arrow.down.right") 260→ .font(.caption2) 261→ Text(String(format: "%.2f%%", abs(change))) 262→ .font(.caption2) 263→ } 264→ .foregroundStyle(change >= 0 ? Color.bcbGreen : Color.bcbRed) 265→ } 266→ } 267→ } 268→ } 269→} 270→ 271→struct SettingsView: View { 272→ @AppStorage("refreshInterval") private var refreshInterval: Int = 5 273→ @AppStorage("autoLoadFocus") private var autoLoadFocus: Bool = true 274→ @AppStorage("autoLoadPIX") private var autoLoadPIX: Bool = true 275→ @AppStorage("autoLoadInstitutions") private var autoLoadInstitutions: Bool = false 276→ @AppStorage("cacheExpirationMinutes") private var cacheExpiration: Int = 5 277→ @AppStorage("showDecimalPlaces") private var decimalPlaces: Int = 2 278→ @AppStorage("compactMode") private var compactMode: Bool = false 279→ @AppStorage("showMenuBarWidget") private var showMenuBarWidget: Bool = true 280→ @StateObject private var notificationService = NotificationService.shared 281→ @State private var launchAtLogin = SMAppService.mainApp.status == .enabled 282→ 283→ var body: some View { 284→ TabView { 285→ // General Settings 286→ Form { 287→ Section("Inicialização") { 288→ Toggle("Abrir ao iniciar sessão", isOn: $launchAtLogin) 289→ .onChange(of: launchAtLogin) { _, newValue in 290→ do { 291→ if newValue { 292→ try SMAppService.mainApp.register() 293→ } else { 294→ try SMAppService.mainApp.unregister() 295→ } 296→ } catch { 297→ print("Failed to update login item: \(error)") 298→ // Revert the toggle if it failed 299→ launchAtLogin = SMAppService.mainApp.status == .enabled 300→ } 301→ } 302→ 303→ Toggle("Mostrar na barra de menus", isOn: $showMenuBarWidget) 304→ 305→ HStack { 306→ Image(systemName: "info.circle") 307→ .foregroundStyle(.secondary) 308→ Text("O app continuará rodando na barra de menus mesmo com a janela fechada") 309→ .font(.caption) 310→ .foregroundStyle(.secondary) 311→ } 312→ } 313→ 314→ Section("Atualização Automática") { 315→ Picker("Intervalo de atualização", selection: $refreshInterval) { 316→ Text("30 segundos").tag(0) // Special case: 0.5 min 317→ Text("1 minuto").tag(1) 318→ Text("2 minutos").tag(2) 319→ Text("5 minutos").tag(5) 320→ Text("10 minutos").tag(10) 321→ Text("15 minutos").tag(15) 322→ Text("30 minutos").tag(30) 323→ Text("1 hora").tag(60) 324→ Text("Desativado").tag(-1) 325→ } 326→ .pickerStyle(.menu) 327→ 328→ if refreshInterval >= 0 { 329→ HStack { 330→ Image(systemName: "info.circle") 331→ .foregroundStyle(.secondary) 332→ Text(refreshInterval == 0 ? "Atualiza a cada 30 segundos" : "Atualiza a cada \(refreshInterval) minuto(s)") 333→ .font(.caption) 334→ .foregroundStyle(.secondary) 335→ } 336→ } 337→ } 338→ 339→ Section("Cache") { 340→ Picker("Expiração do cache", selection: $cacheExpiration) { 341→ Text("1 minuto").tag(1) 342→ Text("5 minutos").tag(5) 343→ Text("15 minutos").tag(15) 344→ Text("30 minutos").tag(30) 345→ Text("1 hora").tag(60) 346→ } 347→ .pickerStyle(.menu) 348→ 349→ HStack { 350→ Image(systemName: "info.circle") 351→ .foregroundStyle(.secondary) 352→ Text("Dados em cache são reutilizados para evitar requisições repetidas") 353→ .font(.caption) 354→ .foregroundStyle(.secondary) 355→ } 356→ } 357→ 358→ Section("Sobre") { 359→ LabeledContent("Versão", value: "1.1.0") 360→ LabeledContent("Fonte de dados", value: "Banco Central do Brasil") 361→ LabeledContent("APIs utilizadas", value: "PTAX, SGS, SPI, Focus") 362→ 363→ Link(destination: URL(string: "https://dadosabertos.bcb.gov.br")!) { 364→ HStack { 365→ Text("Portal de Dados Abertos BCB") 366→ Spacer() 367→ Image(systemName: "arrow.up.right.square") 368→ } 369→ } 370→ } 371→ } 372→ .formStyle(.grouped) 373→ .tabItem { 374→ Label("Geral", systemImage: "gear") 375→ } 376→ 377→ // Data Settings 378→ Form { 379→ Section("Carregamento Automático") { 380→ Toggle("Carregar Focus (Expectativas) no início", isOn: $autoLoadFocus) 381→ Toggle("Carregar PIX no início", isOn: $autoLoadPIX) 382→ Toggle("Carregar Instituições no início", isOn: $autoLoadInstitutions) 383→ 384→ HStack { 385→ Image(systemName: "info.circle") 386→ .foregroundStyle(.secondary) 387→ Text("Desativar carregamento automático economiza dados e acelera o início") 388→ .font(.caption) 389→ .foregroundStyle(.secondary) 390→ } 391→ } 392→ 393→ Section("Exibição") { 394→ Picker("Casas decimais", selection: $decimalPlaces) { 395→ Text("2 casas (R$ 5,37)").tag(2) 396→ Text("3 casas (R$ 5,370)").tag(3) 397→ Text("4 casas (R$ 5,3700)").tag(4) 398→ } 399→ .pickerStyle(.menu) 400→ 401→ Toggle("Modo compacto", isOn: $compactMode) 402→ 403→ HStack { 404→ Image(systemName: "info.circle") 405→ .foregroundStyle(.secondary) 406→ Text("Modo compacto reduz espaçamentos e tamanho de fontes") 407→ .font(.caption) 408→ .foregroundStyle(.secondary) 409→ } 410→ } 411→ 412→ Section("Dados") { 413→ HStack { 414→ VStack(alignment: .leading, spacing: 4) { 415→ Text("APIs do Banco Central") 416→ .font(.subheadline) 417→ Text("Dados atualizados em tempo real") 418→ .font(.caption) 419→ .foregroundStyle(.secondary) 420→ } 421→ 422→ Spacer() 423→ 424→ Circle() 425→ .fill(.green) 426→ .frame(width: 8, height: 8) 427→ Text("Online") 428→ .font(.caption) 429→ .foregroundStyle(.secondary) 430→ } 431→ } 432→ } 433→ .formStyle(.grouped) 434→ .tabItem { 435→ Label("Dados", systemImage: "chart.bar.doc.horizontal") 436→ } 437→ 438→ // Notification Settings 439→ Form { 440→ Section("Notificações") { 441→ Toggle("Ativar notificações", isOn: $notificationService.notificationsEnabled) 442→ .onChange(of: notificationService.notificationsEnabled) { _, _ in 443→ notificationService.saveSettings() 444→ } 445→ 446→ if !notificationService.isAuthorized { 447→ HStack { 448→ Image(systemName: "exclamationmark.triangle.fill") 449→ .foregroundStyle(.yellow) 450→ Text("Permissão de notificações não concedida") 451→ .font(.caption) 452→ .foregroundStyle(.secondary) 453→ } 454→ 455→ Button("Solicitar Permissão") { 456→ Task { 457→ await notificationService.requestAuthorization() 458→ } 459→ } 460→ } 461→ } 462→ 463→ Section("Alertas de Câmbio") { 464→ Toggle("Notificar qualquer mudança", isOn: $notificationService.notifyOnAnyChange) 465→ .onChange(of: notificationService.notifyOnAnyChange) { _, _ in 466→ notificationService.saveSettings() 467→ } 468→ 469→ if !notificationService.notifyOnAnyChange { 470→ HStack { 471→ Text("Variação mínima:") 472→ Slider(value: $notificationService.dollarChangeThreshold, in: 0.1...5.0, step: 0.1) 473→ .onChange(of: notificationService.dollarChangeThreshold) { _, _ in 474→ notificationService.saveSettings() 475→ } 476→ Text(String(format: "%.1f%%", notificationService.dollarChangeThreshold)) 477→ .monospacedDigit() 478→ .frame(width: 50) 479→ } 480→ } 481→ } 482→ 483→ Section("Teste") { 484→ Button("Enviar Notificação de Teste") { 485→ notificationService.sendTestNotification() 486→ } 487→ .disabled(!notificationService.isAuthorized || !notificationService.notificationsEnabled) 488→ 489→ Button("Limpar Todas as Notificações") { 490→ notificationService.clearAllNotifications() 491→ } 492→ } 493→ } 494→ .formStyle(.grouped) 495→ .tabItem { 496→ Label("Notificações", systemImage: "bell") 497→ } 498→ 499→ // Keyboard Shortcuts 500→ Form { 501→ Section("Atalhos de Teclado") { 502→ KeyboardShortcutRow(keys: "⌘ R", description: "Atualizar todos os dados") 503→ KeyboardShortcutRow(keys: "⌘ ⇧ K", description: "Limpar cache") 504→ KeyboardShortcutRow(keys: "⌘ ,", description: "Abrir configurações") 505→ KeyboardShortcutRow(keys: "⌘ 1-6", description: "Navegar entre abas") 506→ } 507→ 508→ Section("Navegação") { 509→ KeyboardShortcutRow(keys: "⌘ [", description: "Voltar") 510→ KeyboardShortcutRow(keys: "⌘ ]", description: "Avançar") 511→ KeyboardShortcutRow(keys: "⌘ W", description: "Fechar janela") 512→ } 513→ } 514→ .formStyle(.grouped) 515→ .tabItem { 516→ Label("Atalhos", systemImage: "keyboard") 517→ } 518→ } 519→ .frame(width: 500, height: 400) 520→ } 521→} 522→ 523→struct KeyboardShortcutRow: View { 524→ let keys: String 525→ let description: String 526→ 527→ var body: some View { 528→ HStack { 529→ Text(keys) 530→ .font(.system(.body, design: .monospaced)) 531→ .padding(.horizontal, 8) 532→ .padding(.vertical, 4) 533→ .background(Color(NSColor.controlBackgroundColor)) 534→ .clipShape(RoundedRectangle(cornerRadius: 6)) 535→ 536→ Text(description) 537→ .foregroundStyle(.secondary) 538→ 539→ Spacer() 540→ } 541→ } 542→} 543→ 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.