Initial project setup - Phases 1-3 complete
This commit is contained in:
104
UI/Home/HomeView.swift
Normal file
104
UI/Home/HomeView.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import SwiftUI
|
||||
import ProxyCore
|
||||
import GRDB
|
||||
|
||||
struct HomeView: View {
|
||||
@Environment(AppState.self) private var appState
|
||||
@State private var domains: [DomainGroup] = []
|
||||
@State private var searchText = ""
|
||||
@State private var showClearConfirmation = false
|
||||
@State private var observation: AnyDatabaseCancellable?
|
||||
|
||||
private let trafficRepo = TrafficRepository()
|
||||
|
||||
var filteredDomains: [DomainGroup] {
|
||||
if searchText.isEmpty { return domains }
|
||||
return domains.filter { $0.domain.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if !appState.isVPNConnected && domains.isEmpty {
|
||||
ContentUnavailableView {
|
||||
Label("VPN Not Connected", systemImage: "bolt.slash")
|
||||
} description: {
|
||||
Text("Enable the VPN to start capturing network traffic.")
|
||||
} actions: {
|
||||
Button("Enable VPN") {
|
||||
Task { await appState.toggleVPN() }
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
} else if domains.isEmpty {
|
||||
ContentUnavailableView {
|
||||
Label("No Traffic", systemImage: "network.slash")
|
||||
} description: {
|
||||
Text("Waiting for network requests. Open Safari or another app to generate traffic.")
|
||||
}
|
||||
} else {
|
||||
List {
|
||||
ForEach(filteredDomains) { group in
|
||||
NavigationLink(value: group) {
|
||||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(group.domain)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
Text("\(group.requestCount)")
|
||||
.foregroundStyle(.secondary)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText, prompt: "Filter Domains")
|
||||
.navigationTitle("Home")
|
||||
.navigationDestination(for: DomainGroup.self) { group in
|
||||
DomainDetailView(domain: group.domain)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button {
|
||||
showClearConfirmation = true
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
.disabled(domains.isEmpty)
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
Task { await appState.toggleVPN() }
|
||||
} label: {
|
||||
Image(systemName: appState.isVPNConnected ? "bolt.fill" : "bolt.slash")
|
||||
.foregroundStyle(appState.isVPNConnected ? .yellow : .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Clear All Domains", isPresented: $showClearConfirmation) {
|
||||
Button("Clear All", role: .destructive) {
|
||||
try? trafficRepo.deleteAll()
|
||||
}
|
||||
} message: {
|
||||
Text("This will permanently delete all captured traffic.")
|
||||
}
|
||||
.task {
|
||||
startObservation()
|
||||
}
|
||||
}
|
||||
|
||||
private func startObservation() {
|
||||
observation = trafficRepo.observeDomainGroups()
|
||||
.start(in: DatabaseManager.shared.dbPool) { error in
|
||||
print("[HomeView] Observation error: \(error)")
|
||||
} onChange: { newDomains in
|
||||
withAnimation {
|
||||
domains = newDomains
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user