feat: initial ui implementation

This commit is contained in:
Iain Learmonth 2026-04-13 14:18:42 +01:00
parent 1612ed099c
commit 13254d63c2
9 changed files with 709 additions and 31 deletions

71
dns/BlockedCount.swift Normal file
View file

@ -0,0 +1,71 @@
import SwiftUI
import Foundation
struct BlockedCount: View {
@State private var txtRecord: String = "..."
var body: some View {
Text(txtRecord)
.onAppear {
fetchTXTRecord()
}
}
func parseResponse(data: Data) -> String? {
// This is a DNS wire format response and we make a lot of assumptions
// It is not critical functionality so just let it fail if it fails
guard data.count > 44 else { return nil }
let startIndex = 44
let subData = data.suffix(from: startIndex)
// Find the first space character (ASCII 32)
var endIndex: Int?
for (offset, byte) in subData.enumerated() {
if byte == 32 { // Space character
endIndex = offset
break
}
}
guard let foundEndIndex = endIndex else { return nil }
// Extract bytes between start and space
let numberData = subData.prefix(foundEndIndex)
return String(decoding: numberData, as: UTF8.self)
}
func fetchTXTRecord() {
let dohURL = URL(string: "https://dns.sr2.uk/dns-query?dns=DoQBAAABAAAAAAAABXN0YXRzB2ludmFsaWQAABAAAQ")!
let request = URLRequest(url: dohURL)
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if error != nil {
txtRecord = "Error"
return
}
guard let data = data else {
txtRecord = "Error"
return
}
guard let count = parseResponse(data: data) else {
txtRecord = "Error"
return
}
txtRecord = count
}
}.resume()
}
}
#Preview {
BlockedCount()
}

41
dns/BlocklistRow.swift Normal file
View file

@ -0,0 +1,41 @@
import SwiftUI
struct BlocklistRow: View {
let option: BlocklistOption
let isSelected: Bool
var body: some View {
HStack(spacing: 16) {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(isSelected ? Color.green.opacity(0.15) : Color.gray.opacity(0.1))
.frame(width: 44, height: 44)
Image(systemName: option.icon)
.font(.title3)
.foregroundStyle(isSelected ? .green : .secondary)
}
VStack(alignment: .leading, spacing: 4) {
Text(option.id + " " + (option.enabled ? "" : "(Coming Soon)"))
.font(.body)
.fontWeight(isSelected ? .semibold : .regular)
Text(option.description)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer()
if isSelected {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
.font(.title3)
.transition(.scale.combined(with: .opacity))
}
}
.padding(.vertical, 8)
}
}

View file

@ -1,24 +0,0 @@
//
// ContentView.swift
// dns
//
// Created by irl on 12/04/2026.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
#Preview {
ContentView()
}

281
dns/HomeView.swift Normal file
View file

@ -0,0 +1,281 @@
import SwiftUI
enum BlocklistOption: String, CaseIterable, Identifiable {
case secure = "Secure"
case securePlusAdblock = "Secure + Adblock"
var id: String { rawValue }
var enabled: Bool {
switch self {
case .secure:
return true
case .securePlusAdblock:
return false
}
}
var description: String {
switch self {
case .secure:
return "Malware and phishing protection"
case .securePlusAdblock:
return "Security plus ad and tracker blocking"
}
}
var icon: String {
switch self {
case .secure:
return "shield"
case .securePlusAdblock:
return "shield.righthalf.filled"
}
}
var server: String {
switch self {
case .secure:
return "dns.sr2.uk"
case .securePlusAdblock:
return "dnsplus.sr2.uk"
}
}
var ipv4: String {
switch self {
case .secure:
return "144.76.160.194"
case .securePlusAdblock:
return "192.0.2.1"
}
}
var ipv6: String {
switch self {
case .secure:
return "2a01:4f8:2210:23ea::4"
case .securePlusAdblock:
return "2001:db8::1"
}
}
}
enum ServiceStatus {
case pending
case operational
case degraded
case outage
var description: String {
switch self {
case .pending:
return "Fetching service status"
case .operational:
return "No issues detected"
case .degraded:
return "Performance degraded"
case .outage:
return "Service disruption"
}
}
var color: Color {
switch self {
case .pending:
return .gray
case .operational:
return .green
case .degraded:
return .orange
case .outage:
return .red
}
}
}
struct HomeView: View {
@State private var isEnabled = false
@State private var selectedBlocklist: BlocklistOption = .secure
@State private var serviceStatus: ServiceStatus = .operational
private let falsePositiveURL = URL(string: "https://www.sr2.uk/contact")!
private let statusURL = URL(string:
"https://status.sr2.uk/")!
private let tosURL = URL(string: "https://www.sr2.uk/terms")!
private let privacyPolicyURL = URL(string: "https://www.sr2.uk/privacy")!
var body: some View {
NavigationStack {
List {
// Main toggle section
Section {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("DNS Protection")
.font(.headline)
Text(isEnabled ? "Active" : "Inactive")
.font(.caption)
.foregroundStyle(isEnabled ? .green : .secondary)
}
Spacer()
Toggle("", isOn: $isEnabled)
.labelsHidden()
.tint(.green)
}
.padding(.vertical, 4)
}
// Blocklist selection
Section {
ForEach(BlocklistOption.allCases) { option in
BlocklistRow(
option: option,
isSelected: selectedBlocklist == option
)
.contentShape(Rectangle())
.onTapGesture {
guard option.enabled else { return }
withAnimation(.spring(duration: 0.3)) {
selectedBlocklist = option
}
}
.opacity(option.enabled ? 1 : 0.6)
}
} header: {
Text("Blocklist")
} footer: {
Text("Select the level of protection for your DNS queries")
}
// Status section
if isEnabled {
Section {
HStack {
Label("Status", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)
Spacer()
Text("Connected")
.foregroundStyle(.secondary)
}
HStack {
Label("Server", systemImage: "server.rack")
Spacer()
Text(selectedBlocklist.server)
.foregroundStyle(.secondary)
.font(.system(.body, design: .monospaced))
}
HStack {
Label("IPv4", systemImage: "globe")
Spacer()
Text(selectedBlocklist.ipv4)
.foregroundStyle(.secondary)
.font(.system(.body, design: .monospaced))
}
HStack {
Label("IPv6", systemImage: "globe")
Spacer()
Text(selectedBlocklist.ipv6)
.foregroundStyle(.secondary)
.font(.system(.body, design: .monospaced))
}
HStack {
Label("Domains in blocklist", systemImage: "xmark.shield.fill")
Spacer()
BlockedCount()
.foregroundStyle(.secondary)
}
} header: {
Text("Connection Details")
}
}
// Support section
Section {
Link(destination: falsePositiveURL) {
HStack {
Label {
Text("Report False Positive")
} icon: {
Image(systemName: "exclamationmark.bubble")
.foregroundStyle(.orange)
}
Spacer()
Image(systemName: "arrow.up.right.square")
.font(.caption)
.foregroundStyle(.secondary)
}
}.foregroundStyle(.primary)
} footer: {
Text("Submit incorrectly blocked domains for review")
}
// Service status section
Section {
Link(destination: statusURL) {
HStack(spacing: 12) {
Circle()
.fill(serviceStatus.color)
.frame(width: 12, height: 12)
VStack(alignment: .leading, spacing: 4) {
Text("Service Status")
.font(.headline)
Text(serviceStatus.description)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "arrow.up.right.square")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}.foregroundStyle(.primary)
Link(destination: tosURL) {
HStack(spacing: 12) {
Image(systemName: "doc.text")
Text("Terms of Service")
Spacer()
Image(systemName: "arrow.up.right.square")
.font(.caption)
.foregroundStyle(.secondary)
}
}.foregroundStyle(.primary)
Link(destination: privacyPolicyURL) {
HStack(spacing: 12) {
Image(systemName: "doc.text")
Text("Privacy Policy")
Spacer()
Image(systemName: "arrow.up.right.square")
.font(.caption)
.foregroundStyle(.secondary)
}
}.foregroundStyle(.primary)
}
}
.navigationTitle("SR2® Cloud DNS")
.animation(.default, value: isEnabled)
}
}
}
#Preview {
HomeView()
}

263
dns/Localizable.xcstrings Normal file
View file

@ -0,0 +1,263 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {
},
"(Coming Soon)" : {
"comment" : "Indicates that this feature is not yet implemented but will be soon",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "(Coming Soon)"
}
}
}
},
"Active" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Active"
}
}
}
},
"Blocklist" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Blocklist"
}
}
}
},
"Connected" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connected"
}
}
}
},
"Connection Details" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connection Details"
}
}
}
},
"DNS Protection" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "DNS Protection"
}
}
}
},
"Domains in blocklist" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Domains in blocklist"
}
}
}
},
"Inactive" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Inactive"
}
}
}
},
"IPv4" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "IPv4"
}
}
}
},
"IPv6" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "IPv6"
}
}
}
},
"Malware and phishing protection" : {
"comment" : "Description of the blocklist contents",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Malware and phishing protection"
}
}
}
},
"No issues detected" : {
"comment" : "No current issues detected with the service",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No issues detected"
}
}
}
},
"Privacy Policy" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Privacy Policy"
}
}
}
},
"Report False Positive" : {
"comment" : "Link to report that a domain name has been incorrectly blocked",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Report False Positive"
}
}
}
},
"Secure" : {
"comment" : "Name of the blocklist that only includes malware and security threats",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secure"
}
}
}
},
"Secure + Adblock" : {
"comment" : "Name of the blocklist that contains “Secure” plus ad blocking",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secure + Adblock"
}
}
}
},
"Security plus ad and tracker blocking" : {
"comment" : "Description of the blocklist contents",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Security plus ad and tracker blocking"
}
}
}
},
"Select the level of protection for your DNS queries" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Select the level of protection for your DNS queries"
}
}
}
},
"Server" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Server"
}
}
}
},
"Service Status" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Service Status"
}
}
}
},
"SR2® Cloud DNS" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "SR2® Cloud DNS"
}
}
}
},
"Status" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Status"
}
}
}
},
"Submit incorrectly blocked domains for review" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Submit incorrectly blocked domains for review"
}
}
}
},
"Terms of Service" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Terms of Service"
}
}
}
}
},
"version" : "1.2"
}

View file

@ -1,9 +1,3 @@
//
// dnsApp.swift
// dns
//
// Created by irl on 12/04/2026.
//
import SwiftUI
@ -11,7 +5,7 @@ import SwiftUI
struct dnsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
HomeView()
}
}
}