krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.0: 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}