153 lines
4.5 KiB
Swift
153 lines
4.5 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)
|
|
}
|
|
if let lastUpdated = bugStore.lastUpdated {
|
|
Text(formatted(lastUpdated))
|
|
.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 formatter = RelativeDateTimeFormatter()
|
|
formatter.unitsStyle = .abbreviated
|
|
return formatter.localizedString(for: date, relativeTo: Date())
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|