165 lines
4.9 KiB
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()
|
|
}
|
|
}
|