summaryrefslogtreecommitdiff
path: root/DomainDig/WatchlistView.swift
diff options
context:
space:
mode:
Diffstat (limited to 'DomainDig/WatchlistView.swift')
-rw-r--r--DomainDig/WatchlistView.swift252
1 files changed, 119 insertions, 133 deletions
diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift
index 7e42b53..0874956 100644
--- a/DomainDig/WatchlistView.swift
+++ b/DomainDig/WatchlistView.swift
@@ -1,9 +1,18 @@
import SwiftUI
struct WatchlistView: View {
+ @Environment(\.appDensity) private var appDensity
@Bindable var viewModel: DomainViewModel
@Environment(\.dismiss) private var dismiss
+ private var pinnedDomains: [TrackedDomain] {
+ viewModel.filteredTrackedDomains.filter(\.isPinned)
+ }
+
+ private var otherDomains: [TrackedDomain] {
+ viewModel.filteredTrackedDomains.filter { !$0.isPinned }
+ }
+
var body: some View {
List {
if viewModel.batchLookupSource == .watchlistRefresh, (!viewModel.batchResults.isEmpty || viewModel.batchLookupRunning) {
@@ -13,7 +22,7 @@ struct WatchlistView: View {
.tint(.cyan)
HStack {
Text(viewModel.batchProgressLabel)
- .font(.system(.caption, design: .monospaced))
+ .font(appDensity.font(.caption))
.foregroundStyle(.secondary)
Spacer()
if viewModel.batchLookupRunning {
@@ -21,7 +30,7 @@ struct WatchlistView: View {
viewModel.cancelBatchLookup()
}
.buttonStyle(.bordered)
- .font(.system(.caption2, design: .monospaced))
+ .font(appDensity.font(.caption2))
}
}
@@ -36,87 +45,34 @@ struct WatchlistView: View {
if viewModel.filteredTrackedDomains.isEmpty {
Section {
- VStack(alignment: .leading, spacing: 8) {
- Text("No tracked domains yet")
- .font(.system(.callout, design: .monospaced))
- .foregroundStyle(.primary)
- Text("Tracked domains appear here. Tracking is local and manual for now.")
- .font(.system(.caption, design: .monospaced))
- .foregroundStyle(.secondary)
- }
- .padding(.vertical, 8)
+ EmptyStateCardView(
+ title: "No Tracked Domains",
+ message: "Track important domains locally so you can refresh them quickly and see status changes at a glance.",
+ suggestion: "Run an inspection and use the Track action on a domain you care about.",
+ systemImage: "eye"
+ )
}
.listRowBackground(Color(.systemGray6).opacity(0.5))
} else {
if let limitMessage = PremiumAccessService.trackedDomainLimitMessage(currentCount: viewModel.trackedDomains.count) {
Section {
Text(limitMessage)
- .font(.system(.caption, design: .monospaced))
+ .font(appDensity.font(.caption))
.foregroundStyle(.secondary)
}
.listRowBackground(Color(.systemGray6).opacity(0.5))
}
- Section {
- ForEach(viewModel.filteredTrackedDomains) { trackedDomain in
- NavigationLink {
- TrackedDomainDetailView(viewModel: viewModel, trackedDomain: trackedDomain)
- } label: {
- WatchlistRowView(
- trackedDomain: trackedDomain,
- isRefreshing: viewModel.refreshingTrackedDomainID == trackedDomain.id
- )
- }
- .buttonStyle(.plain)
- .swipeActions(edge: .leading, allowsFullSwipe: false) {
- Button {
- viewModel.togglePinned(for: trackedDomain)
- } label: {
- Label(trackedDomain.isPinned ? "Unpin" : "Pin", systemImage: trackedDomain.isPinned ? "pin.slash" : "pin")
- }
- .tint(.yellow)
- }
- .swipeActions(edge: .trailing, allowsFullSwipe: false) {
- Button(role: .destructive) {
- viewModel.deleteTrackedDomain(trackedDomain)
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- .contextMenu {
- Button {
- viewModel.refreshTrackedDomain(trackedDomain)
- } label: {
- Label("Refresh", systemImage: "arrow.clockwise")
- }
-
- Button {
- dismiss()
- viewModel.rerunInspection(for: trackedDomain)
- } label: {
- Label("Open Inspection", systemImage: "magnifyingglass")
- }
-
- Button {
- viewModel.togglePinned(for: trackedDomain)
- } label: {
- Label(trackedDomain.isPinned ? "Unpin" : "Pin", systemImage: trackedDomain.isPinned ? "pin.slash" : "pin")
- }
+ if !pinnedDomains.isEmpty {
+ trackedSection(title: "Pinned", domains: pinnedDomains)
+ }
- Button(role: .destructive) {
- viewModel.deleteTrackedDomain(trackedDomain)
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- .listRowBackground(Color(.systemGray6).opacity(0.5))
- }
- .onDelete(perform: deleteFilteredTrackedDomains)
- } header: {
- Text("Tracked Domains")
+ if !otherDomains.isEmpty {
+ trackedSection(title: pinnedDomains.isEmpty ? "Tracked Domains" : "Others", domains: otherDomains)
}
}
}
+ .animation(.easeInOut(duration: 0.2), value: viewModel.filteredTrackedDomains.map(\.id))
.scrollContentBackground(.hidden)
.background(Color.black)
.navigationTitle("Watchlist")
@@ -138,6 +94,7 @@ struct WatchlistView: View {
}
Button(viewModel.batchLookupRunning ? "Check All Running" : "Check All") {
+ AppHaptics.refresh()
viewModel.refreshAllTrackedDomains()
}
.disabled(viewModel.batchLookupRunning)
@@ -170,6 +127,86 @@ struct WatchlistView: View {
.preferredColorScheme(.dark)
}
+ @ViewBuilder
+ private func trackedSection(title: String, domains: [TrackedDomain]) -> some View {
+ Section(title) {
+ ForEach(domains) { trackedDomain in
+ trackedDomainRow(trackedDomain)
+ }
+ }
+ }
+
+ private func trackedDomainRow(_ trackedDomain: TrackedDomain) -> some View {
+ NavigationLink {
+ TrackedDomainDetailView(viewModel: viewModel, trackedDomain: trackedDomain)
+ } label: {
+ WatchlistRowView(
+ trackedDomain: trackedDomain,
+ isRefreshing: viewModel.refreshingTrackedDomainID == trackedDomain.id
+ )
+ }
+ .buttonStyle(.plain)
+ .swipeActions(edge: .leading, allowsFullSwipe: false) {
+ Button {
+ AppHaptics.refresh()
+ viewModel.refreshTrackedDomain(trackedDomain)
+ } label: {
+ Label("Refresh", systemImage: "arrow.clockwise")
+ }
+ .tint(.cyan)
+
+ Button {
+ viewModel.togglePinned(for: trackedDomain)
+ } label: {
+ Label(trackedDomain.isPinned ? "Unpin" : "Pin", systemImage: trackedDomain.isPinned ? "pin.slash" : "pin")
+ }
+ .tint(.yellow)
+ }
+ .swipeActions(edge: .trailing, allowsFullSwipe: false) {
+ Button {
+ AppHaptics.refresh()
+ viewModel.refreshTrackedDomain(trackedDomain)
+ } label: {
+ Label("Refresh", systemImage: "arrow.clockwise")
+ }
+ .tint(.cyan)
+
+ Button(role: .destructive) {
+ viewModel.deleteTrackedDomain(trackedDomain)
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ .contextMenu {
+ Button {
+ AppHaptics.refresh()
+ viewModel.refreshTrackedDomain(trackedDomain)
+ } label: {
+ Label("Refresh", systemImage: "arrow.clockwise")
+ }
+
+ Button {
+ dismiss()
+ viewModel.rerunInspection(for: trackedDomain)
+ } label: {
+ Label("Open Inspection", systemImage: "magnifyingglass")
+ }
+
+ Button {
+ viewModel.togglePinned(for: trackedDomain)
+ } label: {
+ Label(trackedDomain.isPinned ? "Unpin" : "Pin", systemImage: trackedDomain.isPinned ? "pin.slash" : "pin")
+ }
+
+ Button(role: .destructive) {
+ viewModel.deleteTrackedDomain(trackedDomain)
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ .listRowBackground(Color(.systemGray6).opacity(0.5))
+ }
+
private var batchSummaryBinding: Binding<BatchSweepSummary?> {
Binding(
get: { viewModel.latestBatchSweepSummary },
@@ -203,11 +240,12 @@ struct WatchlistView: View {
}
struct WatchlistRowView: View {
+ @Environment(\.appDensity) private var appDensity
let trackedDomain: TrackedDomain
let isRefreshing: Bool
var body: some View {
- VStack(alignment: .leading, spacing: 6) {
+ VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing + 1) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
if trackedDomain.isPinned {
Image(systemName: "pin.fill")
@@ -215,7 +253,7 @@ struct WatchlistRowView: View {
.foregroundStyle(.yellow)
}
Text(trackedDomain.domain)
- .font(.system(.callout, design: .monospaced))
+ .font(appDensity.font(.callout))
.foregroundStyle(.primary)
.lineLimit(2)
.multilineTextAlignment(.leading)
@@ -224,19 +262,19 @@ struct WatchlistRowView: View {
}
Text("Updated \(trackedDomain.updatedAt.formatted(date: .abbreviated, time: .shortened))")
- .font(.system(.caption2, design: .monospaced))
+ .font(appDensity.font(.caption2))
.foregroundStyle(.secondary)
indicatorRow
if let note = trackedDomain.note?.trimmingCharacters(in: .whitespacesAndNewlines), !note.isEmpty {
Text(note)
- .font(.system(.caption, design: .monospaced))
+ .font(appDensity.font(.caption))
.foregroundStyle(.secondary)
.lineLimit(2)
} else if let summary = trackedDomain.lastChangeSummary {
Text(summary.message)
- .font(.system(.caption, design: .monospaced))
+ .font(appDensity.font(.caption))
.foregroundStyle(.secondary)
.lineLimit(2)
}
@@ -259,84 +297,32 @@ struct WatchlistRowView: View {
@ViewBuilder
private var statusBadge: some View {
if isRefreshing {
- HStack(spacing: 6) {
- ProgressView()
- .controlSize(.small)
- Text("Refreshing")
- .font(.system(.caption2, design: .monospaced))
- .foregroundStyle(.secondary)
- }
- .padding(.horizontal, 8)
- .padding(.vertical, 4)
- .background(Color(.systemGray5).opacity(0.6))
- .clipShape(Capsule())
+ AppStatusBadgeView(model: .init(title: "Refreshing", systemImage: "arrow.clockwise", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.6)))
} else {
- Text(availabilityLabel(trackedDomain.lastKnownAvailability))
- .font(.system(.caption2, design: .monospaced))
- .foregroundStyle(badgeColor)
- .padding(.horizontal, 8)
- .padding(.vertical, 4)
- .background(badgeBackground)
- .clipShape(Capsule())
- }
- }
-
- private var badgeColor: Color {
- switch trackedDomain.lastKnownAvailability {
- case .available:
- return .green
- case .registered:
- return .yellow
- case .unknown, .none:
- return .secondary
- }
- }
-
- private var badgeBackground: Color {
- switch trackedDomain.lastKnownAvailability {
- case .available:
- return .green.opacity(0.16)
- case .registered:
- return .yellow.opacity(0.16)
- case .unknown, .none:
- return Color(.systemGray5).opacity(0.6)
+ AppStatusBadgeView(model: AppStatusFactory.availability(trackedDomain.lastKnownAvailability))
}
}
@ViewBuilder
private var indicatorRow: some View {
HStack(spacing: 8) {
- if let severity = trackedDomain.lastChangeSeverity, severity >= .medium {
- Label(severity.title, systemImage: severity == .high ? "exclamationmark.circle.fill" : "circle.fill")
- .font(.system(.caption2, design: .monospaced))
- .foregroundStyle(severity == .high ? .red : .yellow)
- } else {
- Label("Stable", systemImage: "circle.fill")
- .font(.system(.caption2, design: .monospaced))
- .foregroundStyle(.secondary)
- }
+ AppStatusBadgeView(model: AppStatusFactory.change(trackedDomain.lastChangeSummary))
if trackedDomain.certificateWarningLevel != .none {
- Text(certificateLabel)
- .font(.system(.caption2, design: .monospaced))
- .foregroundStyle(trackedDomain.certificateWarningLevel == .critical ? .red : .yellow)
- .padding(.horizontal, 8)
- .padding(.vertical, 4)
- .background((trackedDomain.certificateWarningLevel == .critical ? Color.red : Color.yellow).opacity(0.16))
- .clipShape(Capsule())
+ AppStatusBadgeView(model: certificateBadge)
}
}
}
- private var certificateLabel: String {
+ private var certificateBadge: AppStatusBadgeModel {
let days = trackedDomain.certificateDaysRemaining.map { "\($0)d" } ?? "Soon"
switch trackedDomain.certificateWarningLevel {
case .critical:
- return "Cert \(days)"
+ return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16))
case .warning:
- return "Warn \(days)"
+ return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16))
case .none:
- return ""
+ return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16))
}
}
}