2026-04-13 14:18:42 +01:00
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
|
|
struct HomeView: View {
|
2026-04-14 18:20:30 +02:00
|
|
|
|
|
2026-05-29 16:16:20 +02:00
|
|
|
|
@Environment(\.scenePhase)
|
|
|
|
|
|
private var scenePhase
|
|
|
|
|
|
|
2026-04-14 18:20:30 +02:00
|
|
|
|
@EnvironmentObject
|
|
|
|
|
|
private var viewModel: ViewModel
|
|
|
|
|
|
|
2026-04-13 14:18:42 +01:00
|
|
|
|
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 {
|
2026-04-15 12:51:00 +02:00
|
|
|
|
NavigationCompat {
|
2026-04-13 14:18:42 +01:00
|
|
|
|
List {
|
|
|
|
|
|
|
|
|
|
|
|
// Main toggle section
|
|
|
|
|
|
Section {
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("DNS Protection", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.font(.headline)
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(viewModel.isDnsEnabled ? NSLocalizedString("Active", comment: "") : NSLocalizedString("Inactive", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.font(.caption)
|
2026-04-14 18:57:37 +02:00
|
|
|
|
.foregroundStyle(viewModel.isDnsEnabled ? .green : .secondary)
|
2026-04-13 14:18:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
2026-04-14 18:57:37 +02:00
|
|
|
|
Toggle("", isOn: $viewModel.isDnsEnabled)
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.labelsHidden()
|
2026-05-29 15:41:27 +02:00
|
|
|
|
.disabled(true)
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.tint(.green)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(.vertical, 4)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 15:41:27 +02:00
|
|
|
|
if !viewModel.isDnsEnabled {
|
|
|
|
|
|
Section {
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
|
|
|
|
Text(String(format: NSLocalizedString("To enable %@:", comment: ""), ViewModel.title))
|
|
|
|
|
|
Text(String(format: NSLocalizedString("%1$@ Tap \"%2$@\"", comment: ""), "–", NSLocalizedString("Open Settings", comment: "")))
|
|
|
|
|
|
Text(String(format: NSLocalizedString("%1$@ Go to General", comment: ""), "–"))
|
|
|
|
|
|
Text(String(format: NSLocalizedString("%1$@ VPN & Network", comment: ""), "–"))
|
|
|
|
|
|
Text(String(format: NSLocalizedString("%1$@ DNS", comment: ""), "–"))
|
|
|
|
|
|
Text(String(format: NSLocalizedString("%1$@ Select \"%2$@\"", comment: ""), "–", viewModel.blocklist.title))
|
|
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
|
|
|
|
|
UIApplication.shared.open(url)
|
|
|
|
|
|
}
|
|
|
|
|
|
} label: {
|
|
|
|
|
|
Text(NSLocalizedString("Open Settings", comment: ""))
|
|
|
|
|
|
.frame(maxWidth: .infinity, minHeight: 32)
|
|
|
|
|
|
}
|
|
|
|
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 14:18:42 +01:00
|
|
|
|
// Blocklist selection
|
|
|
|
|
|
Section {
|
|
|
|
|
|
ForEach(BlocklistOption.allCases) { option in
|
|
|
|
|
|
BlocklistRow(
|
|
|
|
|
|
option: option,
|
2026-04-14 18:20:30 +02:00
|
|
|
|
isSelected: viewModel.blocklist == option
|
2026-04-13 14:18:42 +01:00
|
|
|
|
)
|
|
|
|
|
|
.contentShape(Rectangle())
|
|
|
|
|
|
.onTapGesture {
|
|
|
|
|
|
guard option.enabled else { return }
|
|
|
|
|
|
withAnimation(.spring(duration: 0.3)) {
|
2026-04-14 18:20:30 +02:00
|
|
|
|
viewModel.blocklist = option
|
2026-04-13 14:18:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.opacity(option.enabled ? 1 : 0.6)
|
|
|
|
|
|
}
|
|
|
|
|
|
} header: {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Blocklist", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
} footer: {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Select the level of protection for your DNS queries", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Status section
|
2026-04-14 18:57:37 +02:00
|
|
|
|
if viewModel.isDnsEnabled {
|
2026-04-13 14:18:42 +01:00
|
|
|
|
Section {
|
|
|
|
|
|
HStack {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Label(NSLocalizedString("Status", comment: ""), systemImage: "checkmark.circle.fill")
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.foregroundStyle(.green)
|
|
|
|
|
|
Spacer()
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Connected", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HStack {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Label(NSLocalizedString("Server", comment: ""), systemImage: "server.rack")
|
2026-04-13 14:18:42 +01:00
|
|
|
|
Spacer()
|
2026-04-14 18:20:30 +02:00
|
|
|
|
Text(viewModel.blocklist.server)
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
.font(.system(.body, design: .monospaced))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
Label("IPv4", systemImage: "globe")
|
|
|
|
|
|
Spacer()
|
2026-04-14 18:20:30 +02:00
|
|
|
|
Text(viewModel.blocklist.ipv4)
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
.font(.system(.body, design: .monospaced))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
Label("IPv6", systemImage: "globe")
|
|
|
|
|
|
Spacer()
|
2026-04-14 18:20:30 +02:00
|
|
|
|
Text(viewModel.blocklist.ipv6)
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
.font(.system(.body, design: .monospaced))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HStack {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Label(NSLocalizedString("Domains in blocklist", comment: ""), systemImage: "xmark.shield.fill")
|
2026-04-13 14:18:42 +01:00
|
|
|
|
Spacer()
|
|
|
|
|
|
BlockedCount()
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
}
|
|
|
|
|
|
} header: {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Connection Details", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Support section
|
|
|
|
|
|
Section {
|
|
|
|
|
|
Link(destination: falsePositiveURL) {
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
Label {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Report False Positive", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
} icon: {
|
|
|
|
|
|
Image(systemName: "exclamationmark.bubble")
|
|
|
|
|
|
.foregroundStyle(.orange)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
|
|
Image(systemName: "arrow.up.right.square")
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
}
|
|
|
|
|
|
}.foregroundStyle(.primary)
|
|
|
|
|
|
} footer: {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Submit incorrectly blocked domains for review", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Service status section
|
|
|
|
|
|
Section {
|
|
|
|
|
|
Link(destination: statusURL) {
|
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
|
Circle()
|
2026-04-15 14:13:47 +02:00
|
|
|
|
.fill(viewModel.summaryStatus.color)
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.frame(width: 12, height: 12)
|
|
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Service Status", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.font(.headline)
|
2026-04-15 14:13:47 +02:00
|
|
|
|
Text(viewModel.summaryStatus.description)
|
2026-04-13 14:18:42 +01:00
|
|
|
|
.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")
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Terms of Service", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
Spacer()
|
|
|
|
|
|
Image(systemName: "arrow.up.right.square")
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}.foregroundStyle(.primary)
|
|
|
|
|
|
|
|
|
|
|
|
Link(destination: privacyPolicyURL) {
|
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
|
Image(systemName: "doc.text")
|
2026-05-29 16:54:21 +02:00
|
|
|
|
Text(NSLocalizedString("Privacy Policy", comment: ""))
|
2026-04-13 14:18:42 +01:00
|
|
|
|
Spacer()
|
|
|
|
|
|
Image(systemName: "arrow.up.right.square")
|
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}.foregroundStyle(.primary)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-29 15:41:27 +02:00
|
|
|
|
.navigationTitle(ViewModel.title)
|
2026-04-14 18:57:37 +02:00
|
|
|
|
.animation(.default, value: viewModel.isDnsEnabled)
|
2026-05-29 16:54:21 +02:00
|
|
|
|
.onChange(of: scenePhase) { newPhase in
|
2026-05-29 16:16:20 +02:00
|
|
|
|
if newPhase == .active {
|
|
|
|
|
|
Task {
|
|
|
|
|
|
await viewModel.refreshConfig()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-13 14:18:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#Preview {
|
|
|
|
|
|
HomeView()
|
2026-04-14 18:20:30 +02:00
|
|
|
|
.environmentObject(ViewModel())
|
2026-04-13 14:18:42 +01:00
|
|
|
|
}
|