diff options
| author | Christian Cleberg <[email protected]> | 2026-04-20 15:48:47 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-20 15:48:47 -0500 |
| commit | 397c7c420c22fc1c235893bd85b2029da9145730 (patch) | |
| tree | d5c12b992c4496eb928f7253252749acb9ac0386 /DomainDig/ContentView.swift | |
| parent | ee2520f2c0c2d806fd284c3d6b8a81a3822f5270 (diff) | |
| download | domain-dig-397c7c420c22fc1c235893bd85b2029da9145730.tar.gz domain-dig-397c7c420c22fc1c235893bd85b2029da9145730.tar.bz2 domain-dig-397c7c420c22fc1c235893bd85b2029da9145730.zip | |
feat(v1.9.0): add domain availability lookup and watchlist foundation
- implement availability detection (available/registered/unknown)
- integrate availability into domain results
- add lightweight domain suggestions
- introduce local watchlist with persistence
- add WatchlistView and toolbar access
- include availability in export
Diffstat (limited to 'DomainDig/ContentView.swift')
| -rw-r--r-- | DomainDig/ContentView.swift | 108 |
1 files changed, 98 insertions, 10 deletions
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index 68f3a6f..f70bc6a 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -16,7 +16,15 @@ struct ContentView: View { actionButtons SummaryView(fields: viewModel.summaryFields) .padding(.top, 8) - DomainSectionView(rows: viewModel.domainRows) + DomainSectionView( + rows: viewModel.domainRows, + suggestions: viewModel.suggestionRows, + showSuggestions: viewModel.availabilityResult?.status == .registered || viewModel.suggestionsLoading, + availabilityLoading: viewModel.availabilityLoading, + suggestionsLoading: viewModel.suggestionsLoading, + isWatched: viewModel.isCurrentDomainWatched, + onToggleWatch: { viewModel.toggleWatchedDomain() } + ) .padding(.top, 16) DNSSectionView( dnssecLabel: viewModel.dnssecLabel, @@ -101,6 +109,12 @@ struct ContentView: View { .foregroundStyle(.secondary) } NavigationLink { + WatchlistView(viewModel: viewModel) + } label: { + Image(systemName: "eye") + .foregroundStyle(.secondary) + } + NavigationLink { SettingsView() } label: { Image(systemName: "gearshape") @@ -280,14 +294,58 @@ struct SummaryView: View { struct DomainSectionView: View { let rows: [InfoRowViewData] + let suggestions: [DomainSuggestionViewData] + let showSuggestions: Bool + let availabilityLoading: Bool + let suggestionsLoading: Bool + let isWatched: Bool + let onToggleWatch: () -> Void var body: some View { VStack(alignment: .leading, spacing: 12) { - SectionTitleView(title: "Domain") + HStack { + SectionTitleView(title: "Domain") + Spacer() + Button(isWatched ? "Watching" : "Watch") { + onToggleWatch() + } + .buttonStyle(.bordered) + .font(.system(.caption, design: .monospaced)) + } CardView { ForEach(rows) { row in LabeledValueRow(row: row) } + if availabilityLoading { + ProgressView("Checking availability…") + .appLoadingStyle() + .padding(.top, 4) + } + if showSuggestions { + Text("Suggestions") + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(.secondary) + .padding(.top, 4) + if suggestionsLoading { + ProgressView("Checking alternatives…") + .appLoadingStyle() + } else if suggestions.isEmpty { + MessageRowView(text: "No suggestions", isError: false) + } else { + ForEach(suggestions) { suggestion in + HStack { + Text(suggestion.domain) + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(.primary) + .textSelection(.enabled) + Spacer() + Text(suggestion.status) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(ResultColors.color(for: suggestion.tone)) + } + } + } + } } } } @@ -384,6 +442,7 @@ struct WebSectionView: View { .foregroundStyle(.cyan) if sslLoading { ProgressView("Checking certificate…") + .appLoadingStyle() } else if let sslError { MessageRowView(text: sslError, isError: true) } else { @@ -410,6 +469,7 @@ struct WebSectionView: View { .foregroundStyle(.cyan) if headersLoading { ProgressView("Fetching headers…") + .appLoadingStyle() } else if let headersError { MessageRowView(text: headersError, isError: true) } else { @@ -441,6 +501,7 @@ struct WebSectionView: View { .foregroundStyle(.cyan) if redirectLoading { ProgressView("Tracing redirects…") + .appLoadingStyle() } else if let redirectError { MessageRowView(text: redirectError, isError: true) } else if redirects.isEmpty { @@ -486,6 +547,7 @@ struct EmailSectionView: View { CardView { if loading { ProgressView("Checking email records…") + .appLoadingStyle() } else if let error { MessageRowView(text: error, isError: true) } else if rows.isEmpty { @@ -549,6 +611,7 @@ struct NetworkSectionView: View { .foregroundStyle(.cyan) if reachabilityLoading { ProgressView("Checking ports…") + .appLoadingStyle() } else if let reachabilityError { MessageRowView(text: reachabilityError, isError: true) } else { @@ -568,13 +631,14 @@ struct NetworkSectionView: View { } } - CardView { + CardView(allowsHorizontalScroll: false) { Text("Location") .font(.system(.subheadline, design: .monospaced)) .fontWeight(.semibold) .foregroundStyle(.cyan) if geolocationLoading { ProgressView("Looking up location…") + .appLoadingStyle() } else if let geolocationError, geolocation == nil { MessageRowView(text: geolocationError, isError: geolocationError != "No A record available") } else if let geolocation { @@ -590,6 +654,7 @@ struct NetworkSectionView: View { Marker(geolocation.ip, coordinate: coordinate) } .mapStyle(.standard) + .frame(maxWidth: .infinity) .frame(height: 180) .cornerRadius(8) } @@ -598,7 +663,7 @@ struct NetworkSectionView: View { } } - CardView { + CardView(allowsHorizontalScroll: false) { Text("Port Scan") .font(.system(.subheadline, design: .monospaced)) .fontWeight(.semibold) @@ -608,10 +673,12 @@ struct NetworkSectionView: View { Text("Domain is behind Cloudflare's proxy. Results reflect the edge, not the origin.") .font(.system(.caption2, design: .monospaced)) .foregroundStyle(.orange) + .fixedSize(horizontal: false, vertical: true) } if portScanLoading { ProgressView("Scanning ports…") + .appLoadingStyle() } else if let portScanError, standardPortRows.isEmpty { MessageRowView(text: portScanError, isError: true) } else { @@ -640,6 +707,7 @@ struct NetworkSectionView: View { if customPortScanLoading { ProgressView("Scanning custom ports…") + .appLoadingStyle() } else if let customPortScanError { MessageRowView(text: customPortScanError, isError: true) } else { @@ -704,25 +772,38 @@ struct SectionTitleView: View { } struct CardView<Content: View>: View { + let allowsHorizontalScroll: Bool let content: Content - init(@ViewBuilder content: () -> Content) { + init(allowsHorizontalScroll: Bool = true, @ViewBuilder content: () -> Content) { + self.allowsHorizontalScroll = allowsHorizontalScroll self.content = content() } var body: some View { - ScrollView(.horizontal) { - VStack(alignment: .leading, spacing: 6) { - content + Group { + if allowsHorizontalScroll { + ScrollView(.horizontal) { + cardContent + .scrollTargetLayout() + } + .scrollBounceBehavior(.basedOnSize, axes: .horizontal) + } else { + cardContent + .frame(maxWidth: .infinity, alignment: .leading) } - .scrollTargetLayout() } - .scrollBounceBehavior(.basedOnSize, axes: .horizontal) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) .background(Color(.systemGray6).opacity(0.5)) .cornerRadius(6) } + + private var cardContent: some View { + VStack(alignment: .leading, spacing: 6) { + content + } + } } struct LoadingCardView: View { @@ -731,6 +812,7 @@ struct LoadingCardView: View { var body: some View { CardView { ProgressView(text) + .appLoadingStyle() .frame(maxWidth: .infinity, alignment: .center) } } @@ -800,6 +882,12 @@ extension DateFormatter { }() } +private extension View { + func appLoadingStyle() -> some View { + font(.system(.caption, design: .monospaced)) + } +} + private struct SettingsView: View { @AppStorage(DNSResolverOption.userDefaultsKey) private var storedResolverURL = DNSResolverOption.defaultURLString |
