krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v5.0.1: DomainDig/RootTabView.swift · raw
1import SwiftUI
2
3private enum RootTab: Hashable, CaseIterable {
4 case dashboard
5 case audit
6 case history
7 case inspect
8 case settings
9
10 var title: String {
11 switch self {
12 case .dashboard: return "Dashboard"
13 case .audit: return "Audit"
14 case .history: return "History"
15 case .inspect: return "Inspect"
16 case .settings: return "Settings"
17 }
18 }
19
20 var systemImage: String {
21 switch self {
22 case .dashboard: return "square.grid.2x2"
23 case .audit: return "checklist"
24 case .history: return "clock.arrow.trianglehead.counterclockwise.rotate.90"
25 case .inspect: return "magnifyingglass"
26 case .settings: return "gearshape"
27 }
28 }
29}
30
31struct RootTabView: View {
32 @Bindable var viewModel: DomainViewModel
33 @Environment(\.horizontalSizeClass) private var horizontalSizeClass
34 @State private var purchaseService = PurchaseService.shared
35 @State private var intentRouter = DomainDigIntentRouter.shared
36 @State private var detailDomain: TrackedDomain?
37 @State private var selectedTab: RootTab = FeatureAccessService.currentTier == .free ? .inspect : .dashboard
38
39 var body: some View {
40 let _ = purchaseService.currentTier
41
42 Group {
43 if horizontalSizeClass == .regular {
44 splitLayout
45 } else {
46 tabLayout
47 }
48 }
49 .sheet(isPresented: Binding(
50 get: { viewModel.isPaywallPresented },
51 set: { viewModel.isPaywallPresented = $0 }
52 )) {
53 PaywallView()
54 }
55 .alert(item: Binding(
56 get: { viewModel.upgradePrompt },
57 set: { viewModel.upgradePrompt = $0 }
58 )) { prompt in
59 Alert(
60 title: Text(prompt.title),
61 message: Text(prompt.message),
62 primaryButton: .default(Text("Open Paywall")) {
63 viewModel.upgradePrompt = nil
64 viewModel.isPaywallPresented = true
65 },
66 secondaryButton: .cancel(Text("Continue")) {
67 viewModel.upgradePrompt = nil
68 }
69 )
70 }
71 .sheet(item: $detailDomain) { trackedDomain in
72 NavigationStack {
73 TrackedDomainDetailView(viewModel: viewModel, trackedDomain: trackedDomain)
74 }
75 }
76 .onChange(of: purchaseService.currentTier) { _, newValue in
77 if newValue != .free, selectedTab == .inspect, viewModel.trackedDomains.isEmpty == false {
78 selectedTab = .dashboard
79 }
80 }
81 .onOpenURL { url in
82 guard let action = DomainDigDeepLink.action(from: url) else { return }
83 perform(action)
84 }
85 .onChange(of: intentRouter.pendingAction) { _, action in
86 consume(action)
87 }
88 .task {
89 consume(intentRouter.pendingAction)
90 }
91 }
92
93 // MARK: Layouts
94
95 /// Compact (iPhone, iPad slide-over): the classic tab bar.
96 private var tabLayout: some View {
97 TabView(selection: $selectedTab) {
98 ForEach(RootTab.allCases, id: \.self) { tab in
99 section(for: tab)
100 .tabItem {
101 Label(tab.title, systemImage: tab.systemImage)
102 }
103 .tag(tab)
104 }
105 }
106 }
107
108 /// Regular width (iPad, large iPhone landscape): two-column split view.
109 private var splitLayout: some View {
110 NavigationSplitView {
111 List(RootTab.allCases, id: \.self, selection: sidebarSelection) { tab in
112 Label(tab.title, systemImage: tab.systemImage)
113 .tag(tab)
114 }
115 .navigationTitle("DomainDig")
116 } detail: {
117 section(for: selectedTab)
118 }
119 }
120
121 private var sidebarSelection: Binding<RootTab?> {
122 Binding(
123 get: { selectedTab },
124 set: { selectedTab = $0 ?? selectedTab }
125 )
126 }
127
128 @ViewBuilder
129 private func section(for tab: RootTab) -> some View {
130 switch tab {
131 case .dashboard:
132 NavigationStack {
133 DashboardView(viewModel: viewModel)
134 }
135 case .audit:
136 NavigationStack {
137 AuditListView(viewModel: viewModel)
138 }
139 case .history:
140 NavigationStack {
141 HistoryView(viewModel: viewModel)
142 }
143 case .inspect:
144 ContentView(viewModel: viewModel)
145 case .settings:
146 NavigationStack {
147 SettingsView(viewModel: viewModel)
148 }
149 }
150 }
151
152 // MARK: Intent / deep-link routing
153
154 private func consume(_ action: DomainDigDeepLink.Action?) {
155 guard let action else { return }
156 intentRouter.pendingAction = nil
157 perform(action)
158 }
159
160 private func perform(_ action: DomainDigDeepLink.Action) {
161 switch action {
162 case let .inspect(domain):
163 viewModel.domain = domain
164 selectedTab = .inspect
165 viewModel.run()
166 case let .watch(domain):
167 // On success show the dashboard (where tracked domains live); on
168 // failure stay put so the upgrade/paywall alert surfaces in place.
169 if viewModel.trackDomain(domain: domain, availabilityStatus: nil) {
170 selectedTab = .dashboard
171 }
172 case let .detail(domain):
173 // Open the tracked domain's detail (e.g. from a widget tap). If it is
174 // no longer tracked, fall back to inspecting it.
175 if let tracked = viewModel.trackedDomains.first(
176 where: { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
177 ) {
178 detailDomain = tracked
179 } else {
180 perform(.inspect(domain))
181 }
182 case .sweep:
183 selectedTab = .dashboard
184 viewModel.refreshAllTrackedDomains()
185 }
186 }
187}