import SwiftUI import AppKit // MARK: - BroV home: two floors of flat app pills // // Top floor: the Claude and ChatGPT desktop apps (click = bring to front, or launch). // Bottom floor: two empty slots, to be filled later. The bot keeps its spot on the left. struct DesktopAppSlot: Identifiable { let id: String let name: String /// Tried in order: the first installed one wins. let bundleIds: [String] let color: String let downloadURL: String } enum DesktopApps { static let claude = DesktopAppSlot(id: "app_claude", name: "Claude", bundleIds: ["com.anthropic.claudefordesktop"], color: "#D97757", downloadURL: "https://claude.ai/download") static let chatGPT = DesktopAppSlot(id: "app_chatgpt", name: "ChatGPT", // New ChatGPT app first (its id is com.openai.codex), Classic as fallback. bundleIds: ["com.openai.codex", "com.openai.chat"], color: "#10A37F", downloadURL: "https://chatgpt.com/download") } struct AppGridView: View { @ObservedObject var state: AppState var body: some View { ZStack { VStack(spacing: 8) { HStack(spacing: 8) { AppPill(app: DesktopApps.claude) AppPill(app: DesktopApps.chatGPT) } HStack(spacing: 8) { EmptyPill() EmptyPill() } } .padding(.leading, 108) .padding(.trailing, 14) .padding(.vertical, 6) #if !APPSTORE if state.showingPlanDetail { CardBackground(wash: nil) ClaudePlanCardView(usage: state.claudePlanUsage) .transition(.opacity) } #endif } #if !APPSTORE .onChange(of: state.mode) { _, m in if m != .expanded { state.showingPlanDetail = false } } #endif } } // MARK: - App pill struct AppPill: View { let app: DesktopAppSlot @State private var appURL: URL? @State private var isRunning = false @State private var isHovered = false private var status: String { if appURL == nil { return "не установлено" } return isRunning ? "запущено" : "не запущено" } var body: some View { Button(action: open) { HStack(spacing: 10) { icon .frame(width: 26, height: 26) VStack(alignment: .leading, spacing: 1) { Text(app.name) .font(.system(size: 13, weight: .semibold)) .foregroundColor(Color(hex: "#F5F6F8")) HStack(spacing: 5) { Circle() .fill(isRunning ? Color(hex: "#34D399") : Color(hex: "#5F646D")) .frame(width: 5, height: 5) Text(status) .font(.system(size: 10.5)) .foregroundColor(Color(hex: "#8E939C")) } } Spacer(minLength: 4) Image(systemName: "arrow.up.right") .font(.system(size: 9, weight: .semibold)) .foregroundColor(isHovered ? Color(hex: app.color) : Color(hex: "#5F646D")) } .padding(.horizontal, 12) .frame(maxWidth: .infinity, maxHeight: .infinity) .background( RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(isHovered ? Color(hex: app.color).opacity(0.16) : Color(hex: "#0E0F11")) ) .overlay( RoundedRectangle(cornerRadius: 14, style: .continuous) .stroke(Color(hex: app.color).opacity(isHovered ? 0.6 : 0.22), lineWidth: 1) ) .contentShape(Rectangle()) } .buttonStyle(.plain) .scaleEffect(isHovered ? 1.02 : 1) .onHover { h in withAnimation(.spring(response: 0.2, dampingFraction: 0.7)) { isHovered = h } } .onAppear(perform: refresh) .onReceive(NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didLaunchApplicationNotification)) { _ in refresh() } .onReceive(NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didTerminateApplicationNotification)) { _ in refresh() } } @ViewBuilder private var icon: some View { if let appURL { Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path)) .resizable() .interpolation(.high) } else { RoundedRectangle(cornerRadius: 6, style: .continuous) .fill(Color(hex: app.color)) .overlay(Text(String(app.name.prefix(1))) .font(.system(size: 13, weight: .bold)) .foregroundColor(.white)) } } /// The first installed bundle id of the slot. private var bundleId: String? { app.bundleIds.first { NSWorkspace.shared.urlForApplication(withBundleIdentifier: $0) != nil } } private func refresh() { appURL = bundleId.flatMap { NSWorkspace.shared.urlForApplication(withBundleIdentifier: $0) } isRunning = NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == bundleId } } private func open() { SoundEngine.shared.play("blip") if let running = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == bundleId }) { running.activate(options: .activateIgnoringOtherApps) } else if let appURL { NSWorkspace.shared.openApplication(at: appURL, configuration: .init(), completionHandler: nil) } else if let url = URL(string: app.downloadURL) { NSWorkspace.shared.open(url) } } } // MARK: - Empty slot struct EmptyPill: View { var body: some View { RoundedRectangle(cornerRadius: 14, style: .continuous) .strokeBorder(Color.white.opacity(0.08), style: StrokeStyle(lineWidth: 1, dash: [4, 4])) .overlay( Text("пусто") .font(.system(size: 10.5)) .foregroundColor(Color(hex: "#4A4E55")) ) .frame(maxWidth: .infinity, maxHeight: .infinity) } }