diff --git a/DNSecure.xcodeproj/project.pbxproj b/DNSecure.xcodeproj/project.pbxproj index 60b4a62..b092033 100644 --- a/DNSecure.xcodeproj/project.pbxproj +++ b/DNSecure.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 8940025924ACBD2800EBE74B /* DNSecureUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940025824ACBD2800EBE74B /* DNSecureUITests.swift */; }; 8940026924ACBE4900EBE74B /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8940026824ACBE4900EBE74B /* NetworkExtension.framework */; }; 894958AD2548405E009691D5 /* RuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894958AC2548405E009691D5 /* RuleView.swift */; }; + 894F33652C46D2F00060F385 /* RestorationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894F33642C46D2A20060F385 /* RestorationView.swift */; }; 8963FDFB251DF1BC00E3DFE7 /* Bundle+displayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8963FDFA251DF1BC00E3DFE7 /* Bundle+displayName.swift */; }; 8986CDCF251D9B3400D947CD /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8986CDCE251D9B3400D947CD /* Resolver.swift */; }; 8998041628DCDED800C8B421 /* DoTSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8998041528DCDED800C8B421 /* DoTSections.swift */; }; @@ -75,6 +76,7 @@ 8940026624ACBE4900EBE74B /* DNSecure.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DNSecure.entitlements; sourceTree = ""; }; 8940026824ACBE4900EBE74B /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 894958AC2548405E009691D5 /* RuleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleView.swift; sourceTree = ""; }; + 894F33642C46D2A20060F385 /* RestorationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationView.swift; sourceTree = ""; }; 8963FDFA251DF1BC00E3DFE7 /* Bundle+displayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+displayName.swift"; sourceTree = ""; }; 8986CDCE251D9B3400D947CD /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = ""; }; 8998041528DCDED800C8B421 /* DoTSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoTSections.swift; sourceTree = ""; }; @@ -112,6 +114,7 @@ 893AA816258F790D0060B022 /* Views */ = { isa = PBXGroup; children = ( + 894F33642C46D2A20060F385 /* RestorationView.swift */, 8940023D24ACBD2700EBE74B /* ContentView.swift */, 89CB922025209DD100B6983C /* HowToActivateView.swift */, 890B80D4251DC3A20046BAA0 /* DetailView.swift */, @@ -354,6 +357,7 @@ 893AA853258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift in Sources */, 893AA871258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift in Sources */, 893AA85D258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift in Sources */, + 894F33652C46D2F00060F385 /* RestorationView.swift in Sources */, 8940023E24ACBD2700EBE74B /* ContentView.swift in Sources */, 894958AD2548405E009691D5 /* RuleView.swift in Sources */, 8998041828DCDEEF00C8B421 /* DoHSections.swift in Sources */, diff --git a/DNSecure/Views/ContentView.swift b/DNSecure/Views/ContentView.swift index 0db3607..a9e4b01 100644 --- a/DNSecure/Views/ContentView.swift +++ b/DNSecure/Views/ContentView.swift @@ -19,6 +19,7 @@ struct ContentView { @State private var alertTitle = "" @State private var alertMessage = "" @State private var guideIsPresented = false + @State private var isRestoring = false private func addNewDoTServer() { self.servers.append( @@ -40,6 +41,10 @@ struct ContentView { self.selection = self.servers.count - 1 } + private func restoreFromPresets(resolvers: Set) { + self.servers.append(contentsOf: resolvers) + } + private func removeServers(at indexSet: IndexSet) { if let current = self.selection, indexSet.contains(where: { $0 <= current }) { // FIXME: This is a workaround not to crash on deletion. @@ -271,9 +276,15 @@ extension ContentView: View { Menu { Button("DNS-over-TLS", action: self.addNewDoTServer) Button("DNS-over-HTTPS", action: self.addNewDoHServer) + Button("Restore from Presets") { + self.isRestoring = true + } } label: { Image(systemName: "plus") } + .sheet(isPresented: self.$isRestoring) { + RestorationView(onAdd: self.restoreFromPresets) + } } ToolbarItem(placement: .topBarTrailing) { EditButton() diff --git a/DNSecure/Views/RestorationView.swift b/DNSecure/Views/RestorationView.swift new file mode 100644 index 0000000..1eb6b3b --- /dev/null +++ b/DNSecure/Views/RestorationView.swift @@ -0,0 +1,73 @@ +// +// RestorationView.swift +// DNSecure +// +// Created by Kenta Kubo on 7/17/24. +// + +import SwiftUI + +struct RestorationView { + @Environment(\.dismiss) private var dismiss + @State private var selection = Set() + @State private var keyword = "" + let onAdd: (Set) -> () + + private var servers: Resolvers { + guard !self.keyword.isEmpty else { return Presets.servers } + return Presets.servers.filter { $0.name.localizedCaseInsensitiveContains(self.keyword) } + } +} + +extension RestorationView: View { + var body: some View { + NavigationView { + List(self.servers, id: \.self) { resolver in + Button { + if self.selection.contains(resolver) { + self.selection.remove(resolver) + } else { + self.selection.insert(resolver) + } + } label: { + HStack { + VStack(alignment: .leading) { + Text(resolver.name) + Text(resolver.configuration.description) + .foregroundStyle(.secondary) + } + Spacer() + if self.selection.contains(resolver) { + Image(systemName: "checkmark") + } + } + .tint(.primary) + } + } + .navigationTitle("Presets") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Add") { + self.onAdd(self.selection) + self.dismiss() + } + .disabled(self.selection.isEmpty) + } + ToolbarItem(placement: .cancellationAction) { + Button("Cancel", role: .cancel) { + self.dismiss() + } + } + ToolbarItem(placement: .bottomBar) { + Text("\(self.selection.count) Selected") + } + } + } + .navigationViewStyle(.stack) + .searchable(text: self.$keyword, placement: .navigationBarDrawer(displayMode: .always)) + } +} + +#Preview { + RestorationView { _ in } +}