137 lines
4.4 KiB
Swift
137 lines
4.4 KiB
Swift
import SwiftUI
|
|
|
|
struct NotificationsPage: View {
|
|
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
|
@EnvironmentObject var tabManager: TabManager
|
|
@Binding var selection: SidebarSelection
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
header
|
|
Divider()
|
|
|
|
if notificationStore.notifications.isEmpty {
|
|
emptyState
|
|
} else {
|
|
ScrollView {
|
|
LazyVStack(spacing: 8) {
|
|
ForEach(notificationStore.notifications) { notification in
|
|
NotificationRow(
|
|
notification: notification,
|
|
tabTitle: tabTitle(for: notification.tabId),
|
|
onOpen: {
|
|
tabManager.focusTab(notification.tabId)
|
|
notificationStore.markRead(id: notification.id)
|
|
selection = .tabs
|
|
},
|
|
onClear: {
|
|
notificationStore.remove(id: notification.id)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
.padding(16)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(Color(nsColor: .windowBackgroundColor))
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack {
|
|
Text("Notifications")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
|
|
Spacer()
|
|
|
|
if !notificationStore.notifications.isEmpty {
|
|
Button("Clear All") {
|
|
notificationStore.clearAll()
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
}
|
|
|
|
private var emptyState: some View {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "bell.slash")
|
|
.font(.system(size: 32))
|
|
.foregroundColor(.secondary)
|
|
Text("No notifications yet")
|
|
.font(.headline)
|
|
Text("Desktop notifications will appear here for quick review.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
|
|
private func tabTitle(for tabId: UUID) -> String? {
|
|
tabManager.tabs.first(where: { $0.id == tabId })?.title
|
|
}
|
|
}
|
|
|
|
private struct NotificationRow: View {
|
|
let notification: TerminalNotification
|
|
let tabTitle: String?
|
|
let onOpen: () -> Void
|
|
let onClear: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
Circle()
|
|
.fill(notification.isRead ? Color.clear : Color.accentColor)
|
|
.frame(width: 8, height: 8)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(Color.accentColor.opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
|
|
)
|
|
.padding(.top, 6)
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack {
|
|
Text(notification.title)
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
Spacer()
|
|
Text(notification.createdAt, style: .time)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
if !notification.body.isEmpty {
|
|
Text(notification.body)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(3)
|
|
}
|
|
|
|
if let tabTitle {
|
|
Text(tabTitle)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
Button(action: onClear) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.padding(12)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(Color(nsColor: .controlBackgroundColor))
|
|
)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture(perform: onOpen)
|
|
}
|
|
}
|