bugger/Sources/Views/MenuBar/BugListPopover.swift

165 lines
4.9 KiB
Swift

import AppKit
import SwiftUI
struct BugListPopover: View {
@State private var bugStore = BugStore.shared
@State private var tokenManager = TokenManager.shared
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)
Text(bugStore.lastUpdated.map(formatted) ?? "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() }
}
SettingsLink {
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) {
NSWorkspace.shared.open(bug.feishuURL)
AppDelegate.shared?.closePopover()
}
private func openFeishuTable() {
guard let config = AppStateService.shared.config else { return }
let url = FeishuURLBuilder.tableURL(
baseDomain: config.feishuBaseDomain,
appToken: config.appToken
)
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)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
}