import AppKit import SwiftUI struct BugListPopover: View { @State private var bugStore = BugStore.shared @State private var tokenManager = TokenManager.shared @Environment(\.openSettings) private var openSettings 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) } .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 { openInFeishu(bug) bugStore.markSeen(bug.id) AppDelegate.shared?.updateBadge(count: bugStore.activeBugs.count) } 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 openInFeishu(_ bug: Bug) { guard let url = bug.feishuURL else { return } NSWorkspace.shared.open(url) 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() } }