import AppKit import SwiftUI struct BugListPopover: View { @State private var bugStore = BugStore.shared @State private var tokenManager = TokenManager.shared @Environment(\.openSettings) private var openSettings @Environment(\.openWindow) private var openWindow var body: some View { VStack(spacing: 0) { header Divider() content Divider() footer } .frame(minWidth: 340, idealWidth: 360, maxWidth: 400, minHeight: 280) } private var header: some View { HStack { Text("My Bugs") .font(.headline) Spacer() if bugStore.isLoading { ProgressView() .scaleEffect(0.7) .frame(width: 16, height: 16) } else { Image(systemName: "arrow.triangle.2.circlepath") .font(.caption2) .foregroundStyle(.secondary) if let lastUpdated = bugStore.lastUpdated { TimelineView(.periodic(from: lastUpdated, by: 30)) { _ in Text(formatted(lastUpdated)) .font(.caption) .foregroundStyle(.secondary) } } else { Text("Never") .font(.caption) .foregroundStyle(.secondary) } } Button("Mark all seen") { bugStore.markAllSeen() AppDelegate.shared?.updateBadge(count: bugStore.activeBugs.count) } .font(.caption) Button { openWindow(id: "main") DispatchQueue.main.async { NSRunningApplication.current.activate(options: [.activateAllWindows]) } AppDelegate.shared?.closePopover() } label: { Image(systemName: "macwindow") .font(.caption) } .buttonStyle(.plain) .foregroundStyle(.secondary) .help("Open main window") } .padding(.horizontal) .padding(.vertical, 8) } @ViewBuilder private var content: some View { if !tokenManager.isAuthenticated { OAuthSetupView() } else if let error = bugStore.error { ErrorStateView(error: error) } else if bugStore.bugsSortedByPriority.isEmpty && !bugStore.isLoading { EmptyStateView() } else { ScrollView { LazyVStack(spacing: 0) { ForEach(bugStore.bugsSortedByPriority) { bug in BugRow( bug: bug, isUnseen: bugStore.unseenBugs.contains(bug.id) ) .onTapGesture { openDetails(bug) } .contextMenu { Button("Open Details") { openDetails(bug) } Button("Open in App (Web)") { openInAppWeb(bug) } Button("Open in Feishu") { openInFeishu(bug) } } Divider().padding(.leading, 44) } } } } } private var footer: some View { HStack { Button(action: openFeishuTable) { Label("Open in Feishu", systemImage: "arrow.up.forward.app") } Spacer() Button("Refresh") { Task { await PollerService.shared.fetchNow() } } Button { openSettings() // Bugger runs as an accessory (no Dock icon), so the Settings // window would otherwise open behind whatever is frontmost. // Activate on the next runloop pass, once the window exists, // to bring it on top. DispatchQueue.main.async { NSRunningApplication.current.activate(options: [.activateAllWindows]) } } label: { Label("Settings", systemImage: "gear") } Button(action: { NSApp.terminate(nil) }) { Label("Quit", systemImage: "power") } } .padding(.horizontal) .padding(.vertical, 6) .buttonStyle(.plain) .font(.caption) } private func openDetails(_ bug: Bug) { MainWindowState.shared.selectedBugID = bug.id openWindow(id: "main") // Accessory app: activate on the next runloop pass so the new // window comes to the front instead of opening behind other apps. DispatchQueue.main.async { NSRunningApplication.current.activate(options: [.activateAllWindows]) } bugStore.markSeen(bug.id) AppDelegate.shared?.updateBadge(count: bugStore.activeBugs.count) AppDelegate.shared?.closePopover() } private func openInAppWeb(_ bug: Bug) { guard let url = bug.feishuURL else { return } AppDelegate.shared?.openWebWindow(url: url, title: bug.title) bugStore.markSeen(bug.id) AppDelegate.shared?.updateBadge(count: bugStore.activeBugs.count) AppDelegate.shared?.closePopover() } private func openInFeishu(_ bug: Bug) { guard let url = bug.feishuURL else { return } NSWorkspace.shared.open(url) bugStore.markSeen(bug.id) AppDelegate.shared?.updateBadge(count: bugStore.activeBugs.count) AppDelegate.shared?.closePopover() } private func openFeishuTable() { guard let config = AppStateService.shared.config, let url = FeishuURLBuilder.tableURL( baseDomain: config.feishuBaseDomain, appToken: config.appToken ) else { return } NSWorkspace.shared.open(url) } private func formatted(_ date: Date) -> String { let seconds = Int(Date().timeIntervalSince(date)) switch seconds { case ..<60: return "Just now" case ..<3600: let m = seconds / 60 return m == 1 ? "1m ago" : "\(m)m ago" case ..<86400: let h = seconds / 3600 return h == 1 ? "1h ago" : "\(h)h ago" default: let d = seconds / 86400 return d == 1 ? "1d ago" : "\(d)d ago" } } } struct EmptyStateView: View { var body: some View { VStack(spacing: 8) { Image(systemName: "checkmark.circle") .font(.largeTitle) .foregroundStyle(.green) Text("No active bugs") .font(.headline) Text("You're all clear!") .font(.subheadline) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } } struct ErrorStateView: View { let error: Error var body: some View { VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundStyle(.orange) Text("Failed to load bugs") .font(.headline) Text(error.localizedDescription) .font(.caption) .foregroundStyle(.secondary) .multilineTextAlignment(.center) Button("Retry") { Task { await PollerService.shared.fetchNow() } } .buttonStyle(.bordered) .padding(.top, 4) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } }