Migrate to async/await
This commit is contained in:
parent
ea47113615
commit
a0bc5b98cf
13 changed files with 445 additions and 417 deletions
|
@ -31,6 +31,7 @@
|
||||||
28EC1E3A23A441F30088BA26 /* OrDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EC1E3923A441F30088BA26 /* OrDivider.swift */; };
|
28EC1E3A23A441F30088BA26 /* OrDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EC1E3923A441F30088BA26 /* OrDivider.swift */; };
|
||||||
28EC1E3C23A448940088BA26 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EC1E3B23A448940088BA26 /* Styles.swift */; };
|
28EC1E3C23A448940088BA26 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EC1E3B23A448940088BA26 /* Styles.swift */; };
|
||||||
80654D7B24B0E92A007B1E1A /* DisableCustomTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80654D7A24B0E92A007B1E1A /* DisableCustomTimeView.swift */; };
|
80654D7B24B0E92A007B1E1A /* DisableCustomTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80654D7A24B0E92A007B1E1A /* DisableCustomTimeView.swift */; };
|
||||||
|
80A8CF852782190400CCCEC5 /* AsyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A8CF842782190400CCCEC5 /* AsyncData.swift */; };
|
||||||
80E91FA723DE160100B7FB55 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E91FA623DE160100B7FB55 /* AboutView.swift */; };
|
80E91FA723DE160100B7FB55 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E91FA623DE160100B7FB55 /* AboutView.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
@ -88,6 +89,7 @@
|
||||||
28EC1E3B23A448940088BA26 /* Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = "<group>"; };
|
28EC1E3B23A448940088BA26 /* Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = "<group>"; };
|
||||||
80436C6C27555BDE003D48E4 /* Pi-helper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Pi-helper.entitlements"; sourceTree = "<group>"; };
|
80436C6C27555BDE003D48E4 /* Pi-helper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Pi-helper.entitlements"; sourceTree = "<group>"; };
|
||||||
80654D7A24B0E92A007B1E1A /* DisableCustomTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableCustomTimeView.swift; sourceTree = "<group>"; };
|
80654D7A24B0E92A007B1E1A /* DisableCustomTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableCustomTimeView.swift; sourceTree = "<group>"; };
|
||||||
|
80A8CF842782190400CCCEC5 /* AsyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncData.swift; sourceTree = "<group>"; };
|
||||||
80E91FA623DE160100B7FB55 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
80E91FA623DE160100B7FB55 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -141,32 +143,33 @@
|
||||||
2821266E235926E700072D52 /* Pi-helper */ = {
|
2821266E235926E700072D52 /* Pi-helper */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
28D7205B2380C2090038D439 /* resolver.c */,
|
||||||
80436C6C27555BDE003D48E4 /* Pi-helper.entitlements */,
|
80436C6C27555BDE003D48E4 /* Pi-helper.entitlements */,
|
||||||
28D720522380689E0038D439 /* PiHelper-Bridging-Header.h */,
|
28D720522380689E0038D439 /* PiHelper-Bridging-Header.h */,
|
||||||
2821266F235926E700072D52 /* AppDelegate.swift */,
|
28D7205A2380C2090038D439 /* resolver.h */,
|
||||||
28212671235926E700072D52 /* SceneDelegate.swift */,
|
|
||||||
28212673235926E700072D52 /* ContentView.swift */,
|
|
||||||
282126AD235BF21D00072D52 /* AddPiHoleView.swift */,
|
|
||||||
28212675235926EB00072D52 /* Assets.xcassets */,
|
|
||||||
2821267A235926EB00072D52 /* LaunchScreen.storyboard */,
|
|
||||||
2821267D235926EB00072D52 /* Info.plist */,
|
2821267D235926EB00072D52 /* Info.plist */,
|
||||||
28212677235926EB00072D52 /* Preview Content */,
|
80E91FA623DE160100B7FB55 /* AboutView.swift */,
|
||||||
282126A5235BE6EE00072D52 /* PiHoleDataStore.swift */,
|
282126AF235BF52F00072D52 /* ActivityIndicatorView.swift */,
|
||||||
|
282126AD235BF21D00072D52 /* AddPiHoleView.swift */,
|
||||||
|
2821266F235926E700072D52 /* AppDelegate.swift */,
|
||||||
|
28212673235926E700072D52 /* ContentView.swift */,
|
||||||
|
80654D7A24B0E92A007B1E1A /* DisableCustomTimeView.swift */,
|
||||||
|
28D7205823808E8C0038D439 /* Extensions.swift */,
|
||||||
|
280F0D6223DA9B1800D341B7 /* KeyboardHelper.swift */,
|
||||||
|
28EC1E3923A441F30088BA26 /* OrDivider.swift */,
|
||||||
282126A7235BE72800072D52 /* PiHole.swift */,
|
282126A7235BE72800072D52 /* PiHole.swift */,
|
||||||
282126A9235BEE0F00072D52 /* PiHoleApiService.swift */,
|
282126A9235BEE0F00072D52 /* PiHoleApiService.swift */,
|
||||||
|
282126A5235BE6EE00072D52 /* PiHoleDataStore.swift */,
|
||||||
282126AB235BF09800072D52 /* PiHoleDetailsView.swift */,
|
282126AB235BF09800072D52 /* PiHoleDetailsView.swift */,
|
||||||
282126AF235BF52F00072D52 /* ActivityIndicatorView.swift */,
|
|
||||||
282126B6235C0F5400072D52 /* Localizable.strings */,
|
|
||||||
28D7205823808E8C0038D439 /* Extensions.swift */,
|
|
||||||
28D7205A2380C2090038D439 /* resolver.h */,
|
|
||||||
28D7205B2380C2090038D439 /* resolver.c */,
|
|
||||||
280AA3EA2390B302007841F1 /* RetrieveApiKeyView.swift */,
|
280AA3EA2390B302007841F1 /* RetrieveApiKeyView.swift */,
|
||||||
28EC1E3923A441F30088BA26 /* OrDivider.swift */,
|
|
||||||
28EC1E3B23A448940088BA26 /* Styles.swift */,
|
|
||||||
28D2125B23A59057003B33F2 /* ScanningView.swift */,
|
28D2125B23A59057003B33F2 /* ScanningView.swift */,
|
||||||
80654D7A24B0E92A007B1E1A /* DisableCustomTimeView.swift */,
|
28212671235926E700072D52 /* SceneDelegate.swift */,
|
||||||
280F0D6223DA9B1800D341B7 /* KeyboardHelper.swift */,
|
28EC1E3B23A448940088BA26 /* Styles.swift */,
|
||||||
80E91FA623DE160100B7FB55 /* AboutView.swift */,
|
28212675235926EB00072D52 /* Assets.xcassets */,
|
||||||
|
2821267A235926EB00072D52 /* LaunchScreen.storyboard */,
|
||||||
|
282126B6235C0F5400072D52 /* Localizable.strings */,
|
||||||
|
28212677235926EB00072D52 /* Preview Content */,
|
||||||
|
80A8CF842782190400CCCEC5 /* AsyncData.swift */,
|
||||||
);
|
);
|
||||||
path = "Pi-helper";
|
path = "Pi-helper";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -358,6 +361,7 @@
|
||||||
28D7205923808E8C0038D439 /* Extensions.swift in Sources */,
|
28D7205923808E8C0038D439 /* Extensions.swift in Sources */,
|
||||||
282126A8235BE72800072D52 /* PiHole.swift in Sources */,
|
282126A8235BE72800072D52 /* PiHole.swift in Sources */,
|
||||||
80654D7B24B0E92A007B1E1A /* DisableCustomTimeView.swift in Sources */,
|
80654D7B24B0E92A007B1E1A /* DisableCustomTimeView.swift in Sources */,
|
||||||
|
80A8CF852782190400CCCEC5 /* AsyncData.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "28212681235926EB00072D52"
|
BlueprintIdentifier = "28212681235926EB00072D52"
|
||||||
BuildableName = "PihelperTests.xctest"
|
BuildableName = "Pi-helperTests.xctest"
|
||||||
BlueprintName = "Pi-helperTests"
|
BlueprintName = "Pi-helperTests"
|
||||||
ReferencedContainer = "container:Pi-helper.xcodeproj">
|
ReferencedContainer = "container:Pi-helper.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "2821268C235926EB00072D52"
|
BlueprintIdentifier = "2821268C235926EB00072D52"
|
||||||
BuildableName = "PihelperUITests.xctest"
|
BuildableName = "Pi-helperUITests.xctest"
|
||||||
BlueprintName = "Pi-helperUITests"
|
BlueprintName = "Pi-helperUITests"
|
||||||
ReferencedContainer = "container:Pi-helper.xcodeproj">
|
ReferencedContainer = "container:Pi-helper.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
|
|
@ -9,26 +9,22 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ActivityIndicatorView: View {
|
struct ActivityIndicatorView: View {
|
||||||
var isAnimating: Binding<Bool>
|
|
||||||
@State private var rotation = 0.0
|
@State private var rotation = 0.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Image("logo")
|
Image("logo")
|
||||||
.rotationEffect(.degrees(rotation))
|
.resizable()
|
||||||
|
.frame(width: 100, height: 100)
|
||||||
|
.rotationEffect(.degrees(self.rotation))
|
||||||
|
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: self.rotation)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
return withAnimation(self.isAnimating.wrappedValue ? Animation.linear(duration: 1).repeatForever(autoreverses: false) : Animation.linear) {
|
self.rotation = 360
|
||||||
self.rotation = 360.0
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ isAnimating: Binding<Bool>) {
|
|
||||||
self.isAnimating = isAnimating
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ActivityIndicatorView_Previews: PreviewProvider {
|
struct ActivityIndicatorView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ActivityIndicatorView(.constant(true))
|
ActivityIndicatorView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,17 @@ import SwiftUI
|
||||||
|
|
||||||
struct AddPiHoleView: View {
|
struct AddPiHoleView: View {
|
||||||
@State var ipAddress: String = ""
|
@State var ipAddress: String = ""
|
||||||
|
@State var showScanFailed = false
|
||||||
|
@State var showConnectFailed = false
|
||||||
|
var hideNavigationBar: Bool {
|
||||||
|
get {
|
||||||
|
if case .scanning(_) = self.dataStore.pihole {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
@ -17,9 +28,9 @@ struct AddPiHoleView: View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 30) {
|
||||||
Image("logo")
|
Image("logo")
|
||||||
ScanButton(dataStore: self.dataStore)
|
ScanButton(dataStore: self.dataStore)
|
||||||
.alert(isPresented: .constant(self.dataStore.pihole == .failure(.scanFailed)), content: {
|
.alert(isPresented: self.$showScanFailed, content: {
|
||||||
Alert(title: Text("scan_failed"), message: Text("try_direct_connection"), dismissButton: .default(Text("OK"), action: {
|
Alert(title: Text("scan_failed"), message: Text("try_direct_connection"), dismissButton: .default(Text("OK"), action: {
|
||||||
self.dataStore.pihole = .failure(.missingIpAddress)
|
self.dataStore.pihole = .missingIpAddress
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
OrDivider()
|
OrDivider()
|
||||||
|
@ -28,20 +39,27 @@ struct AddPiHoleView: View {
|
||||||
TextField("ip_address", text: $ipAddress)
|
TextField("ip_address", text: $ipAddress)
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
.keyboardType(.default)
|
.keyboardType(.default)
|
||||||
Button(action: {
|
.disableAutocorrection(true)
|
||||||
self.dataStore.connect(self.ipAddress)
|
.autocapitalization(.none)
|
||||||
}, label: { Text("connect") })
|
.onSubmit {
|
||||||
|
Task {
|
||||||
|
await self.dataStore.connect(self.ipAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: { Task {
|
||||||
|
await self.dataStore.connect(self.ipAddress)
|
||||||
|
} }, label: { Text("connect") })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
.alert(isPresented: .constant(self.dataStore.pihole == .failure(.connectionFailed)), content: {
|
.alert(isPresented: self.$showConnectFailed, content: {
|
||||||
Alert(title: Text("connection_failed"), message: Text("verify_ip_address"), dismissButton: .default(Text("OK"), action: {
|
Alert(title: Text("connection_failed"), message: Text("verify_ip_address"), dismissButton: .default(Text("OK"), action: {
|
||||||
self.dataStore.pihole = .failure(.missingIpAddress)
|
self.dataStore.pihole = .missingIpAddress
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.keyboardAwarePadding()
|
.keyboardAwarePadding()
|
||||||
}
|
}
|
||||||
.navigationBarHidden(self.dataStore.pihole == .failure(.missingIpAddress))
|
.navigationBarHidden(self.hideNavigationBar)
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
}
|
}
|
||||||
|
@ -68,17 +86,19 @@ struct ScanButton: View {
|
||||||
Text("scan_for_pihole")
|
Text("scan_for_pihole")
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
Button(action: {
|
Button(action: {
|
||||||
self.dataStore.beginScanning(ipAddress)
|
self.dataStore.scanTask = Task {
|
||||||
|
await self.dataStore.beginScanning(ipAddress)
|
||||||
|
}
|
||||||
}, label: { Text("scan") })
|
}, label: { Text("scan") })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
NavigationLink(
|
NavigationLink(
|
||||||
destination: ScanningView(dataStore: self.dataStore),
|
destination: ScanningView(dataStore: self.dataStore),
|
||||||
isActive: .constant(self.dataStore.pihole == .failure(.scanning(""))),
|
isActive: $dataStore.isScanning,
|
||||||
label: { EmptyView() }
|
label: { EmptyView() }
|
||||||
)
|
)
|
||||||
NavigationLink(
|
NavigationLink(
|
||||||
destination: RetrieveApiKeyView(dataStore: self.dataStore),
|
destination: RetrieveApiKeyView(dataStore: self.dataStore),
|
||||||
isActive: .constant(self.dataStore.pihole == .failure(.missingApiKey) || self.dataStore.pihole == .failure(.invalidCredentials)),
|
isActive: .constant(self.dataStore.pihole == .missingApiKey),
|
||||||
label: { EmptyView() }
|
label: { EmptyView() }
|
||||||
)
|
)
|
||||||
}.toAnyView()
|
}.toAnyView()
|
||||||
|
|
54
Pi-helper/AsyncData.swift
Normal file
54
Pi-helper/AsyncData.swift
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// AsyncData.swift
|
||||||
|
// Pi-helper
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 1/2/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum AsyncData<Data>: Equatable where Data: Equatable {
|
||||||
|
case empty
|
||||||
|
case loading
|
||||||
|
case error(Error)
|
||||||
|
case success(Data)
|
||||||
|
|
||||||
|
var value: Data? {
|
||||||
|
get {
|
||||||
|
if case let .success(data) = self {
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var error: Error? {
|
||||||
|
get {
|
||||||
|
if case let .error(error) = self {
|
||||||
|
return error
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: AsyncData, rhs: AsyncData) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.empty, .empty):
|
||||||
|
return true
|
||||||
|
case (.loading, .loading):
|
||||||
|
return true
|
||||||
|
case (.error(let lError), .error(let rError)):
|
||||||
|
return rError.localizedDescription == lError.localizedDescription
|
||||||
|
case (.success(let lData), .success(let rData)):
|
||||||
|
return lData == rData
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,9 +25,9 @@ struct DisableCustomTimeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(SegmentedPickerStyle())
|
.pickerStyle(SegmentedPickerStyle())
|
||||||
Button(action: {
|
Button(action: { Task {
|
||||||
self.dataStore.disable(Int(self.duration) ?? 0, unit: self.unit)
|
await self.dataStore.disable(Int(self.duration) ?? 0, unit: self.unit)
|
||||||
}, label: { Text(LocalizedStringKey("disable")) })
|
} }, label: { Text(LocalizedStringKey("disable")) })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
|
@ -157,7 +157,7 @@ struct TopItemsResponse: Codable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PiHoleStatus: Equatable {
|
enum Status: Equatable {
|
||||||
case enabled
|
case enabled
|
||||||
case disabled(_ duration: String? = nil)
|
case disabled(_ duration: String? = nil)
|
||||||
case unknown
|
case unknown
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
|
|
||||||
class PiHoleApiService {
|
class PiHoleApiService {
|
||||||
let decoder: JSONDecoder
|
let decoder: JSONDecoder
|
||||||
|
@ -20,70 +19,61 @@ class PiHoleApiService {
|
||||||
self.decoder = decoder
|
self.decoder = decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
func getVersion() -> AnyPublisher<VersionResponse, NetworkError> {
|
func getVersion() async throws -> VersionResponse {
|
||||||
return get(queries: ["version": ""])
|
return try await get(queries: ["version": ""])
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSummary() -> AnyPublisher<PiHole, NetworkError> {
|
func loadSummary() async throws -> PiHole {
|
||||||
return get()
|
return try await get()
|
||||||
}
|
}
|
||||||
|
|
||||||
func enable() -> AnyPublisher<StatusUpdate, NetworkError> {
|
func enable() async throws -> StatusUpdate {
|
||||||
return get(true, queries: ["enable": ""])
|
return try await get(true, queries: ["enable": ""])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTopItems() -> AnyPublisher<TopItemsResponse, NetworkError> {
|
func getTopItems() async throws -> TopItemsResponse {
|
||||||
return get(true, queries: ["topItems": "25"])
|
return try await get(true, queries: ["topItems": "25"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func disable(_ forSeconds: Int? = nil) -> AnyPublisher<StatusUpdate, NetworkError> {
|
func disable(_ forSeconds: Int? = nil) async throws -> StatusUpdate {
|
||||||
var params = [String: String]()
|
var params = [String: String]()
|
||||||
if let timeToDisable = forSeconds {
|
if let timeToDisable = forSeconds {
|
||||||
params["disable"] = String(timeToDisable)
|
params["disable"] = String(timeToDisable)
|
||||||
} else {
|
} else {
|
||||||
params["disable"] = ""
|
params["disable"] = ""
|
||||||
}
|
}
|
||||||
return get(true, queries: params)
|
return try await get(true, queries: params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCustomDisableTimer() -> AnyPublisher<UInt, NetworkError> {
|
func getCustomDisableTimer() async throws -> UInt {
|
||||||
guard let baseUrl = self.baseUrl else {
|
guard let baseUrl = self.baseUrl else {
|
||||||
return Result<UInt, NetworkError>.Publisher(.failure(.invalidUrl))
|
throw NetworkError.invalidUrl
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
guard let url = URL(string: baseUrl + "/custom_disable_timer") else {
|
guard let url = URL(string: baseUrl + "/custom_disable_timer") else {
|
||||||
return Result<UInt, NetworkError>.Publisher(.failure(.invalidUrl))
|
throw NetworkError.invalidUrl
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.timeoutInterval = 0.5
|
request.timeoutInterval = 0.5
|
||||||
let task = URLSession.shared.dataTaskPublisher(for: request)
|
let (data, res) = try await URLSession.shared.data(for: request)
|
||||||
.tryMap { (data, res) -> UInt in
|
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
||||||
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
switch (res as? HTTPURLResponse)?.statusCode {
|
||||||
switch (res as? HTTPURLResponse)?.statusCode {
|
case 400: throw NetworkError.badRequest
|
||||||
case 400: throw NetworkError.badRequest
|
case 401, 403: throw NetworkError.unauthorized
|
||||||
case 401, 403: throw NetworkError.unauthorized
|
case 404: throw NetworkError.notFound
|
||||||
case 404: throw NetworkError.notFound
|
default: throw NetworkError.unknown(nil)
|
||||||
default: throw NetworkError.unknown
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
let dataString = String(data: data, encoding: .utf8) ?? "0"
|
|
||||||
return UInt(dataString) ?? 0
|
|
||||||
}
|
}
|
||||||
.mapError {
|
let dataString = String(data: data, encoding: .utf8) ?? "0"
|
||||||
return $0 as! NetworkError
|
return UInt(dataString) ?? 0
|
||||||
}
|
|
||||||
return task.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func get<ResultType: Codable>(
|
private func get<ResultType: Codable>(
|
||||||
_ requiresAuth: Bool = false,
|
_ requiresAuth: Bool = false,
|
||||||
queries: [String: String]? = nil
|
queries: [String: String]? = nil
|
||||||
) -> AnyPublisher<ResultType, NetworkError> {
|
) async throws -> ResultType {
|
||||||
guard let baseUrl = self.baseUrl else {
|
guard let baseUrl = self.baseUrl else {
|
||||||
return Result<ResultType, NetworkError>.Publisher(.failure(.invalidUrl))
|
throw NetworkError.invalidUrl
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var combinedEndPoint = baseUrl + "/admin/api.php"
|
var combinedEndPoint = baseUrl + "/admin/api.php"
|
||||||
|
@ -101,7 +91,7 @@ class PiHoleApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = URL(string: combinedEndPoint) else {
|
guard let url = URL(string: combinedEndPoint) else {
|
||||||
return Result.Publisher(.failure(.invalidUrl)).eraseToAnyPublisher()
|
throw NetworkError.invalidUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
|
@ -109,23 +99,20 @@ class PiHoleApiService {
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.timeoutInterval = 0.5
|
request.timeoutInterval = 0.5
|
||||||
|
|
||||||
let task = URLSession.shared.dataTaskPublisher(for: request)
|
do {
|
||||||
.tryMap { (data, res) -> Data in
|
let (data, res) = try await URLSession.shared.data(for: request)
|
||||||
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
||||||
switch (res as? HTTPURLResponse)?.statusCode {
|
switch (res as? HTTPURLResponse)?.statusCode {
|
||||||
case 400: throw NetworkError.badRequest
|
case 400: throw NetworkError.badRequest
|
||||||
case 401, 403: throw NetworkError.unauthorized
|
case 401, 403: throw NetworkError.unauthorized
|
||||||
case 404: throw NetworkError.notFound
|
case 404: throw NetworkError.notFound
|
||||||
default: throw NetworkError.unknown
|
default: throw NetworkError.unknown(nil)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return data
|
}
|
||||||
|
return try self.decoder.decode(ResultType.self, from: data)
|
||||||
|
} catch {
|
||||||
|
throw NetworkError.unknown(error)
|
||||||
}
|
}
|
||||||
.decode(type: ResultType.self, decoder: self.decoder)
|
|
||||||
.mapError {
|
|
||||||
return NetworkError.jsonParsingFailed($0)
|
|
||||||
}
|
|
||||||
return task.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,14 +122,12 @@ enum NetworkError: Error, Equatable {
|
||||||
case badRequest
|
case badRequest
|
||||||
case notFound
|
case notFound
|
||||||
case unauthorized
|
case unauthorized
|
||||||
case unknown
|
case unknown(Error?)
|
||||||
case invalidUrl
|
case invalidUrl
|
||||||
case jsonParsingFailed(Error)
|
case jsonParsingFailed(Error)
|
||||||
|
|
||||||
static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
|
static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.loading, .loading):
|
|
||||||
return true
|
|
||||||
case (.cancelled, .cancelled):
|
case (.cancelled, .cancelled):
|
||||||
return true
|
return true
|
||||||
case (.badRequest, .badRequest):
|
case (.badRequest, .badRequest):
|
||||||
|
@ -151,8 +136,8 @@ enum NetworkError: Error, Equatable {
|
||||||
return true
|
return true
|
||||||
case (.unauthorized, .unauthorized):
|
case (.unauthorized, .unauthorized):
|
||||||
return true
|
return true
|
||||||
case (.unknown, .unknown):
|
case (.unknown(let error1), .unknown(let error2)):
|
||||||
return true
|
return error1?.localizedDescription == error2?.localizedDescription
|
||||||
case (.invalidUrl, .invalidUrl):
|
case (.invalidUrl, .invalidUrl):
|
||||||
return true
|
return true
|
||||||
case (.jsonParsingFailed(let error1), .jsonParsingFailed(let error2)):
|
case (.jsonParsingFailed(let error1), .jsonParsingFailed(let error2)):
|
||||||
|
@ -162,5 +147,3 @@ enum NetworkError: Error, Equatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Empty: Codable {}
|
|
||||||
|
|
|
@ -10,12 +10,25 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class PiHoleDataStore: ObservableObject {
|
class PiHoleDataStore: ObservableObject {
|
||||||
private let IP_MIN = 0
|
private let IP_MIN = 0
|
||||||
private let IP_MAX = 255
|
private let IP_MAX = 255
|
||||||
private var currentRequest: AnyCancellable? = nil
|
var isScanning: Bool {
|
||||||
|
get {
|
||||||
|
if case .scanning(_) = self.pihole {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
// No op
|
||||||
|
}
|
||||||
|
}
|
||||||
@Published var showCustomDisableView = false
|
@Published var showCustomDisableView = false
|
||||||
@Published var pihole: Result<PiHoleStatus, PiHoleError>
|
@Published var pihole: PiholeStatus = .empty
|
||||||
|
@Published var error: PiHoleError? = nil
|
||||||
@Published var apiKey: String? = nil {
|
@Published var apiKey: String? = nil {
|
||||||
didSet {
|
didSet {
|
||||||
UserDefaults.standard.set(apiKey, forKey: PiHoleDataStore.API_KEY)
|
UserDefaults.standard.set(apiKey, forKey: PiHoleDataStore.API_KEY)
|
||||||
|
@ -30,9 +43,10 @@ class PiHoleDataStore: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var shouldMonitorStatus = false
|
private var shouldMonitorStatus = false
|
||||||
|
var scanTask: Task<Any, Error>? = nil
|
||||||
|
|
||||||
private func prependScheme(_ ipAddress: String?) -> String? {
|
private func prependScheme(_ ipAddress: String?) -> String? {
|
||||||
guard let host = ipAddress else {
|
guard let host = ipAddress?.lowercased() else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,162 +61,109 @@ class PiHoleDataStore: ObservableObject {
|
||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
func monitorStatus() {
|
func monitorStatus() async {
|
||||||
self.shouldMonitorStatus = true
|
while !Task.isCancelled {
|
||||||
doMonitorStatus()
|
do {
|
||||||
}
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
await loadSummary()
|
||||||
private func doMonitorStatus() {
|
} catch {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
|
break
|
||||||
if !self.shouldMonitorStatus {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
self.loadSummary {
|
|
||||||
self.doMonitorStatus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopMonitoring() {
|
|
||||||
self.shouldMonitorStatus = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func beginScanning(_ ipAddress: String) {
|
|
||||||
var addressParts = ipAddress.split(separator: ".")
|
|
||||||
var chunks = 1
|
|
||||||
var ipAddresses = [String]()
|
|
||||||
ipAddresses.append("pi.hole")
|
|
||||||
while chunks <= IP_MAX {
|
|
||||||
let chunkSize = (IP_MAX - IP_MIN + 1) / chunks
|
|
||||||
if chunkSize == 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for chunk in 0..<chunks {
|
|
||||||
let chunkStart = IP_MIN + (chunk * chunkSize)
|
|
||||||
let chunkEnd = IP_MIN + ((chunk + 1) * chunkSize)
|
|
||||||
addressParts[3] = Substring(String(((chunkEnd - chunkStart) / 2) + chunkStart))
|
|
||||||
ipAddresses.append(addressParts.joined(separator: "."))
|
|
||||||
}
|
|
||||||
chunks *= 2
|
|
||||||
}
|
}
|
||||||
scan(ipAddresses)
|
}
|
||||||
|
|
||||||
|
func beginScanning(_ ipAddress: String) async {
|
||||||
|
var addressParts = ipAddress.split(separator: ".")
|
||||||
|
let lastOctal = Int(addressParts[3])
|
||||||
|
for octal in 0..<IP_MAX {
|
||||||
|
if Task.isCancelled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if octal == lastOctal {
|
||||||
|
// Don't scan the current device
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addressParts[3] = Substring(String(octal))
|
||||||
|
if await scan(addressParts.joined(separator: ".")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.error = .scanFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect(_ rawIpAddress: String) {
|
func cancelScanning() {
|
||||||
|
scanTask?.cancel()
|
||||||
|
scanTask = nil
|
||||||
|
self.pihole = .missingIpAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(_ rawIpAddress: String) async {
|
||||||
guard let formattedIpAddress = prependScheme(rawIpAddress) else {
|
guard let formattedIpAddress = prependScheme(rawIpAddress) else {
|
||||||
self.pihole = .failure(.connectionFailed)
|
self.error = .connectionFailed(rawIpAddress)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.apiService.baseUrl = formattedIpAddress
|
self.apiService.baseUrl = formattedIpAddress
|
||||||
currentRequest = self.apiService.getVersion()
|
do {
|
||||||
.receive(on: DispatchQueue.main)
|
_ = try await self.apiService.getVersion()
|
||||||
.sink(receiveCompletion: { (completion) in
|
self.baseUrl = formattedIpAddress
|
||||||
switch completion {
|
self.pihole = .missingApiKey
|
||||||
case .finished:
|
} catch {
|
||||||
return
|
self.error = .connectionFailed(formattedIpAddress)
|
||||||
case .failure(_):
|
}
|
||||||
self.pihole = .failure(.connectionFailed)
|
|
||||||
}
|
|
||||||
}, receiveValue: { version in
|
|
||||||
self.baseUrl = formattedIpAddress
|
|
||||||
self.pihole = .failure(.missingApiKey)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scan(_ ipAddresses: [String]) {
|
func scan(_ ipAddress: String) async -> Bool {
|
||||||
if ipAddresses.isEmpty {
|
self.pihole = PiholeStatus.scanning(ipAddress)
|
||||||
self.pihole = .failure(.scanFailed)
|
do {
|
||||||
return
|
self.apiService.baseUrl = prependScheme(ipAddress)
|
||||||
|
_ = try await self.apiService.getVersion()
|
||||||
|
self.baseUrl = self.apiService.baseUrl
|
||||||
|
self.pihole = PiholeStatus.missingApiKey
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let ipAddress = prependScheme(ipAddresses[0]) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.apiService.baseUrl = ipAddress
|
|
||||||
self.pihole = .failure(.scanning(ipAddresses[0]))
|
|
||||||
currentRequest = self.apiService.getVersion()
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink(receiveCompletion: { (completion) in
|
|
||||||
switch completion {
|
|
||||||
case .finished:
|
|
||||||
return
|
|
||||||
case .failure(let error):
|
|
||||||
// ignore if timeout, otherwise handle
|
|
||||||
print(error)
|
|
||||||
self.scan(Array(ipAddresses.dropFirst()))
|
|
||||||
}
|
|
||||||
}, receiveValue: { version in
|
|
||||||
// Stop scans, load summary
|
|
||||||
self.baseUrl = ipAddress
|
|
||||||
self.pihole = .failure(.missingApiKey)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func forgetPihole() {
|
func forgetPihole() {
|
||||||
self.baseUrl = nil
|
self.baseUrl = nil
|
||||||
self.apiKey = nil
|
self.apiKey = nil
|
||||||
self.pihole = .failure(.missingIpAddress)
|
self.pihole = PiholeStatus.missingIpAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectWithPassword(_ password: String) {
|
func connectWithPassword(_ password: String) async {
|
||||||
if let hash = password.sha256Hash()?.sha256Hash() {
|
if let hash = password.sha256Hash()?.sha256Hash() {
|
||||||
connectWithApiKey(hash)
|
await connectWithApiKey(hash)
|
||||||
} else {
|
} else {
|
||||||
self.pihole = .failure(.invalidCredentials)
|
self.error = .invalidCredentials
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectWithApiKey(_ apiToken: String) {
|
func connectWithApiKey(_ apiToken: String) async {
|
||||||
self.apiService.apiKey = apiToken
|
self.apiService.apiKey = apiToken
|
||||||
currentRequest = self.apiService.getTopItems()
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink(receiveCompletion: { (completion) in
|
|
||||||
switch completion {
|
|
||||||
case .finished:
|
|
||||||
return
|
|
||||||
case .failure(let error):
|
|
||||||
self.pihole = .failure(.invalidCredentials)
|
|
||||||
print("\(error)")
|
|
||||||
}
|
|
||||||
}, receiveValue: { topItems in
|
|
||||||
self.apiKey = apiToken
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelRequest() {
|
|
||||||
self.currentRequest?.cancel()
|
|
||||||
if (self.pihole != .failure(.missingApiKey)) {
|
|
||||||
// TODO: Find a better way to handle this
|
|
||||||
// The problem is that without this check, the scanning functionality is essentially broken because
|
|
||||||
// it finds the correct IP, navigates to the authentication screen, but then immediately navigates
|
|
||||||
// back to the IP input screen.
|
|
||||||
self.pihole = .failure(.networkError(.cancelled))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadSummary(completionBlock: (() -> Void)? = nil) {
|
|
||||||
var previousStatus: PiHoleStatus? = nil
|
|
||||||
do {
|
do {
|
||||||
previousStatus = try self.pihole.get()
|
_ = try await self.apiService.getTopItems()
|
||||||
} catch _ {
|
self.apiKey = apiToken
|
||||||
|
} catch {
|
||||||
|
self.error = .invalidCredentials
|
||||||
}
|
}
|
||||||
self.pihole = .failure(.loading(previousStatus))
|
}
|
||||||
|
|
||||||
self.currentRequest = apiService.loadSummary()
|
func loadSummary() async {
|
||||||
.receive(on: DispatchQueue.main)
|
var loadingTask: Task<Void, Error>? = nil
|
||||||
.sink(receiveCompletion: { (completion) in
|
if case let .success(previousState) = self.pihole {
|
||||||
switch completion {
|
// Avoid showing the loading spinner immediately
|
||||||
case .finished:
|
loadingTask = Task {
|
||||||
self.currentRequest = nil
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
case .failure(let error):
|
self.pihole = .loading(previousState)
|
||||||
self.pihole = .failure(.networkError(error))
|
}
|
||||||
}
|
} else {
|
||||||
if let completionBlock = completionBlock {
|
self.pihole = .loading()
|
||||||
completionBlock()
|
}
|
||||||
}
|
do {
|
||||||
}, receiveValue: { pihole in
|
let pihole = try await apiService.loadSummary()
|
||||||
|
await MainActor.run {
|
||||||
UIApplication.shared.shortcutItems = [
|
UIApplication.shared.shortcutItems = [
|
||||||
UIApplicationShortcutItem(
|
UIApplicationShortcutItem(
|
||||||
type: ShortcutAction.enable.rawValue,
|
type: ShortcutAction.enable.rawValue,
|
||||||
|
@ -233,64 +194,57 @@ class PiHoleDataStore: ObservableObject {
|
||||||
userInfo: ["forSeconds": 300 as NSSecureCoding]
|
userInfo: ["forSeconds": 300 as NSSecureCoding]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
self.updateStatus(status: pihole.status)
|
}
|
||||||
})
|
await self.updateStatus(pihole.status)
|
||||||
}
|
loadingTask?.cancel()
|
||||||
|
} catch {
|
||||||
func enable() {
|
if let error = error as? NetworkError {
|
||||||
var previousStatus: PiHoleStatus? = nil
|
self.error = .networkError(error)
|
||||||
do {
|
} else {
|
||||||
previousStatus = try self.pihole.get()
|
print("Unhandled error! \(error)")
|
||||||
} catch _ {
|
}
|
||||||
}
|
}
|
||||||
self.pihole = .failure(.loading(previousStatus))
|
|
||||||
self.currentRequest = self.apiService.enable()
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink(receiveCompletion: { (completion) in
|
|
||||||
switch completion {
|
|
||||||
case .finished:
|
|
||||||
self.currentRequest = nil
|
|
||||||
return
|
|
||||||
case .failure(let error):
|
|
||||||
self.pihole = .failure(.networkError(error))
|
|
||||||
}
|
|
||||||
}, receiveValue: { newStatus in
|
|
||||||
self.updateStatus(status: newStatus.status)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func disable(_ forDuration: Int, unit: Int) {
|
func enable() async {
|
||||||
|
var previousStatus: Status? = nil
|
||||||
|
if case let .success(status) = self.pihole {
|
||||||
|
previousStatus = status
|
||||||
|
}
|
||||||
|
self.pihole = .loading(previousStatus)
|
||||||
|
do {
|
||||||
|
let status = try await self.apiService.enable()
|
||||||
|
await self.updateStatus(status.status)
|
||||||
|
} catch {
|
||||||
|
self.error = .networkError(error as! NetworkError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disable(_ forDuration: Int, unit: Int) async {
|
||||||
let multiplier = NSDecimalNumber(decimal: pow(60, unit)).intValue
|
let multiplier = NSDecimalNumber(decimal: pow(60, unit)).intValue
|
||||||
disable(forDuration * multiplier)
|
await disable(forDuration * multiplier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func disable(_ forSeconds: Int? = nil) {
|
func disable(_ forSeconds: Int? = nil) async {
|
||||||
self.showCustomDisableView = false
|
var previousStatus: Status? = nil
|
||||||
var previousStatus: PiHoleStatus? = nil
|
if case let .success(status) = self.pihole {
|
||||||
do {
|
previousStatus = status
|
||||||
previousStatus = try self.pihole.get()
|
}
|
||||||
} catch _ {
|
self.pihole = .loading(previousStatus)
|
||||||
|
do {
|
||||||
|
let status = try await self.apiService.disable(forSeconds)
|
||||||
|
await self.updateStatus(status.status)
|
||||||
|
} catch {
|
||||||
|
if let error = error as? NetworkError {
|
||||||
|
self.error = .networkError(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.pihole = .failure(.loading(previousStatus))
|
|
||||||
self.currentRequest = self.apiService.disable(forSeconds)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink(receiveCompletion: { (completion) in
|
|
||||||
switch completion {
|
|
||||||
case .finished:
|
|
||||||
self.currentRequest = nil
|
|
||||||
return
|
|
||||||
case .failure(let error):
|
|
||||||
self.pihole = .failure(.networkError(error))
|
|
||||||
}
|
|
||||||
}, receiveValue: { newStatus in
|
|
||||||
self.updateStatus(status: newStatus.status)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateStatus(status: String) {
|
private func updateStatus(_ status: String) async {
|
||||||
switch status {
|
switch status {
|
||||||
case "disabled":
|
case "disabled":
|
||||||
self.getDisabledDuration()
|
await self.getDisabledDuration()
|
||||||
default:
|
default:
|
||||||
self.pihole = .success(.enabled)
|
self.pihole = .success(.enabled)
|
||||||
}
|
}
|
||||||
|
@ -298,27 +252,20 @@ class PiHoleDataStore: ObservableObject {
|
||||||
|
|
||||||
private var customDisableTimeRequest: AnyCancellable? = nil
|
private var customDisableTimeRequest: AnyCancellable? = nil
|
||||||
|
|
||||||
private func getDisabledDuration() {
|
private func getDisabledDuration() async {
|
||||||
self.customDisableTimeRequest = self.apiService.getCustomDisableTimer()
|
do {
|
||||||
.receive(on: DispatchQueue.main)
|
let timestamp = try await self.apiService.getCustomDisableTimer()
|
||||||
.sink(receiveCompletion: { (completion) in
|
let disabledUntil = TimeInterval(round(Double(timestamp) / 1000.0))
|
||||||
switch completion {
|
let now = Date().timeIntervalSince1970
|
||||||
case .finished:
|
if now > disabledUntil {
|
||||||
return
|
self.pihole = .success(.disabled())
|
||||||
case .failure(_):
|
} else {
|
||||||
self.pihole = .success(.disabled())
|
self.pihole = .success(.disabled(UInt(disabledUntil - now).toDurationString()))
|
||||||
self.customDisableTimeRequest = nil
|
}
|
||||||
}
|
self.customDisableTimeRequest = nil
|
||||||
}, receiveValue: { timestamp in
|
} catch {
|
||||||
let disabledUntil = TimeInterval(round(Double(timestamp) / 1000.0))
|
self.pihole = .success(.disabled())
|
||||||
let now = Date().timeIntervalSince1970
|
}
|
||||||
if now > disabledUntil {
|
|
||||||
self.pihole = .success(.disabled())
|
|
||||||
} else {
|
|
||||||
self.pihole = .success(.disabled(UInt(disabledUntil - now).toDurationString()))
|
|
||||||
}
|
|
||||||
self.customDisableTimeRequest = nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let apiService = PiHoleApiService()
|
let apiService = PiHoleApiService()
|
||||||
|
@ -329,12 +276,11 @@ class PiHoleDataStore: ObservableObject {
|
||||||
self.baseUrl = baseUrl
|
self.baseUrl = baseUrl
|
||||||
self.apiKey = apiKey
|
self.apiKey = apiKey
|
||||||
if baseUrl == nil {
|
if baseUrl == nil {
|
||||||
self.pihole = .failure(.missingIpAddress)
|
self.pihole = .missingIpAddress
|
||||||
} else if apiKey == nil {
|
} else if apiKey == nil {
|
||||||
self.pihole = .failure(.missingApiKey)
|
self.pihole = .missingApiKey
|
||||||
} else {
|
} else {
|
||||||
self.pihole = .failure(.networkError(.loading))
|
self.pihole = .loading(nil)
|
||||||
self.loadSummary()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,32 +290,49 @@ enum ShortcutAction: String {
|
||||||
case disable = "DisableAction"
|
case disable = "DisableAction"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PiHoleError : Error, Equatable {
|
enum PiholeStatus: Equatable {
|
||||||
case networkError(_ error: NetworkError)
|
case empty
|
||||||
case loading(_ previousStatus: PiHoleStatus? = nil)
|
case loading(_ previousStatus: Status? = nil)
|
||||||
|
case error(_ error: Error)
|
||||||
case scanning(_ ipAddress: String)
|
case scanning(_ ipAddress: String)
|
||||||
case missingIpAddress
|
case missingIpAddress
|
||||||
case missingApiKey
|
case missingApiKey
|
||||||
|
case success(_ status: Status)
|
||||||
|
|
||||||
|
static func == (lhs: PiholeStatus, rhs: PiholeStatus) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.loading(let left), .loading(let right)):
|
||||||
|
return left == right
|
||||||
|
case (.scanning(let left), .scanning(let right)):
|
||||||
|
return left == right
|
||||||
|
case (.missingIpAddress, .missingIpAddress):
|
||||||
|
return true
|
||||||
|
case (.missingApiKey, .missingApiKey):
|
||||||
|
return true
|
||||||
|
case (.success(let left), .success(let right)):
|
||||||
|
return left == right
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PiHoleError : Error, Equatable {
|
||||||
|
case networkError(_ error: NetworkError)
|
||||||
case invalidCredentials
|
case invalidCredentials
|
||||||
case scanFailed
|
case scanFailed
|
||||||
case connectionFailed
|
case connectionFailed(_ host: String)
|
||||||
|
|
||||||
static func == (lhs: PiHoleError, rhs: PiHoleError) -> Bool {
|
static func == (lhs: PiHoleError, rhs: PiHoleError) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.networkError(let error1), .networkError(let error2)):
|
case (.networkError(let error1), .networkError(let error2)):
|
||||||
return error1 == error2
|
return error1 == error2
|
||||||
case (.scanning(_), .scanning(_)):
|
|
||||||
return true
|
|
||||||
case (.missingIpAddress, .missingIpAddress):
|
|
||||||
return true
|
|
||||||
case (.missingApiKey, .missingApiKey):
|
|
||||||
return true
|
|
||||||
case (.invalidCredentials, .invalidCredentials):
|
case (.invalidCredentials, .invalidCredentials):
|
||||||
return true
|
return true
|
||||||
case (.scanFailed, .scanFailed):
|
case (.scanFailed, .scanFailed):
|
||||||
return true
|
return true
|
||||||
case (.connectionFailed, .connectionFailed):
|
case (.connectionFailed(let lIp), .connectionFailed(let rIp)):
|
||||||
return true
|
return lIp == rIp
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,30 +9,32 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PiHoleDetailsView: View {
|
struct PiHoleDetailsView: View {
|
||||||
var stateContent: AnyView {
|
@ViewBuilder
|
||||||
|
func stateContent() -> some View {
|
||||||
switch self.dataStore.pihole {
|
switch self.dataStore.pihole {
|
||||||
case .success(let pihole):
|
case .success(let pihole):
|
||||||
return PiHoleActionsView(dataStore: self.dataStore, status: pihole).toAnyView()
|
PiHoleActionsView(dataStore: self.dataStore, status: pihole)
|
||||||
case .failure(.loading(let previousStatus)):
|
case .loading(let previousStatus):
|
||||||
if let status = previousStatus {
|
if let status = previousStatus {
|
||||||
return PiHoleActionsView(dataStore: self.dataStore, status: status).toAnyView()
|
PiHoleActionsView(dataStore: self.dataStore, status: status)
|
||||||
} else {
|
} else {
|
||||||
return ActivityIndicatorView(.constant(true)).toAnyView()
|
ActivityIndicatorView()
|
||||||
}
|
}
|
||||||
case .failure(.networkError(let error)):
|
case .error(let error):
|
||||||
switch (error) {
|
switch (error) {
|
||||||
default:
|
default:
|
||||||
return PiHoleActionsView(dataStore: self.dataStore, status: .unknown).toAnyView()
|
PiHoleActionsView(dataStore: self.dataStore, status: .unknown)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return ActivityIndicatorView(.constant(true)).toAnyView()
|
ActivityIndicatorView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
stateContent
|
stateContent()
|
||||||
.navigationBarTitle("PiHelper")
|
.animation(.default, value: self.dataStore.pihole)
|
||||||
|
.navigationBarTitle("Pi-helper")
|
||||||
.navigationBarItems(trailing: NavigationLink(destination: AboutView(self.dataStore), label: {
|
.navigationBarItems(trailing: NavigationLink(destination: AboutView(self.dataStore), label: {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
.padding()
|
.padding()
|
||||||
|
@ -40,10 +42,9 @@ struct PiHoleDetailsView: View {
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
.onAppear {
|
.onAppear {
|
||||||
self.dataStore.monitorStatus()
|
Task {
|
||||||
}
|
await self.dataStore.monitorStatus()
|
||||||
.onDisappear {
|
}
|
||||||
self.dataStore.stopMonitoring()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,53 +59,32 @@ struct PiHoleActionsView: View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Text("status")
|
Text("status")
|
||||||
PiHoleStatusView(status)
|
PiholeStatusView(status)
|
||||||
}
|
}
|
||||||
DurationView(status)
|
|
||||||
PiHoleActions(self.dataStore, status: status)
|
PiHoleActions(self.dataStore, status: status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataStore: PiHoleDataStore
|
let dataStore: PiHoleDataStore
|
||||||
let status: PiHoleStatus
|
let status: Status
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PiHoleStatusView: View {
|
struct PiholeStatusView: View {
|
||||||
|
@ViewBuilder
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(status.localizedStringKey).foregroundColor(status.foregroundColor)
|
HStack {
|
||||||
}
|
Text(status.localizedStringKey)
|
||||||
|
.foregroundColor(status.foregroundColor)
|
||||||
let status: PiHoleStatus
|
if case let .disabled(duration) = status, let durationString = duration {
|
||||||
init(_ status: PiHoleStatus) {
|
Text("(\(durationString))")
|
||||||
self.status = status
|
.monospacedDigit()
|
||||||
}
|
.foregroundColor(status.foregroundColor)
|
||||||
}
|
|
||||||
|
|
||||||
struct DurationView: View {
|
|
||||||
var body: some View {
|
|
||||||
stateContent
|
|
||||||
}
|
|
||||||
|
|
||||||
var stateContent: AnyView {
|
|
||||||
switch status {
|
|
||||||
case .disabled(let duration):
|
|
||||||
if let durationString = duration {
|
|
||||||
return HStack {
|
|
||||||
Text("time_remaining")
|
|
||||||
Text(durationString)
|
|
||||||
.frame(minWidth: 30)
|
|
||||||
}
|
|
||||||
.toAnyView()
|
|
||||||
} else {
|
|
||||||
return EmptyView().toAnyView()
|
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
return EmptyView().toAnyView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let status: PiHoleStatus
|
let status: Status
|
||||||
init(_ status: PiHoleStatus) {
|
init(_ status: Status) {
|
||||||
self.status = status
|
self.status = status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,34 +97,44 @@ struct PiHoleActions: View {
|
||||||
var stateContent: AnyView {
|
var stateContent: AnyView {
|
||||||
switch status {
|
switch status {
|
||||||
case .disabled:
|
case .disabled:
|
||||||
return Button(action: { self.dataStore.enable() }, label: { Text("enable") })
|
return Button(action: { Task {
|
||||||
|
await self.dataStore.enable()
|
||||||
|
} }, label: { Text("enable") })
|
||||||
.buttonStyle(PiHelperButtonStyle(.green))
|
.buttonStyle(PiHelperButtonStyle(.green))
|
||||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||||
.toAnyView()
|
.toAnyView()
|
||||||
case .enabled:
|
case .enabled:
|
||||||
return VStack {
|
return VStack {
|
||||||
Button(action: { self.dataStore.disable(10) }, label: { Text("disable_10_sec") })
|
Button(action: { Task {
|
||||||
|
await self.dataStore.disable(10)
|
||||||
|
} }, label: { Text("disable_10_sec") })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||||
Button(action: { self.dataStore.disable(30) }, label: { Text("disable_30_sec") })
|
Button(action: { Task{
|
||||||
|
await self.dataStore.disable(30)
|
||||||
|
} }, label: { Text("disable_30_sec") })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||||
Button(action: { self.dataStore.disable(300) }, label: { Text("disable_5_min") })
|
Button(action: { Task {
|
||||||
|
await self.dataStore.disable(300)
|
||||||
|
} }, label: { Text("disable_5_min") })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||||
NavigationLink(
|
|
||||||
destination: DisableCustomTimeView(self.dataStore),
|
|
||||||
isActive: .constant(self.dataStore.showCustomDisableView),
|
|
||||||
label: { EmptyView() }
|
|
||||||
)
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
self.dataStore.showCustomDisableView = true
|
self.dataStore.showCustomDisableView = true
|
||||||
}, label: { Text("disable_custom") })
|
}, label: { Text("disable_custom") })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||||
Button(action: { self.dataStore.disable() }, label: { Text("disable_permanent") })
|
Button(action: { Task {
|
||||||
|
await self.dataStore.disable()
|
||||||
|
} }, label: { Text("disable_permanent") })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||||
|
NavigationLink(
|
||||||
|
destination: DisableCustomTimeView(self.dataStore),
|
||||||
|
isActive: self.$dataStore.showCustomDisableView,
|
||||||
|
label: { EmptyView() }
|
||||||
|
)
|
||||||
}.toAnyView()
|
}.toAnyView()
|
||||||
default:
|
default:
|
||||||
return Text("Unable to load Pi-hole status. Please verify your credentials and ensure the Pi-hole is accessible from your current network.")
|
return Text("Unable to load Pi-hole status. Please verify your credentials and ensure the Pi-hole is accessible from your current network.")
|
||||||
|
@ -153,8 +143,8 @@ struct PiHoleActions: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObservedObject var dataStore: PiHoleDataStore
|
@ObservedObject var dataStore: PiHoleDataStore
|
||||||
let status: PiHoleStatus
|
let status: Status
|
||||||
init(_ dataStore: PiHoleDataStore, status: PiHoleStatus) {
|
init(_ dataStore: PiHoleDataStore, status: Status) {
|
||||||
self.dataStore = dataStore
|
self.dataStore = dataStore
|
||||||
self.status = status
|
self.status = status
|
||||||
}
|
}
|
||||||
|
@ -164,7 +154,7 @@ struct PiHoleDetailsView_Previews: PreviewProvider {
|
||||||
static var dataStore: PiHoleDataStore {
|
static var dataStore: PiHoleDataStore {
|
||||||
get {
|
get {
|
||||||
let _dataStore = PiHoleDataStore()
|
let _dataStore = PiHoleDataStore()
|
||||||
_dataStore.pihole = .success(.disabled("20"))
|
_dataStore.pihole = PiholeStatus.success(Status.disabled("20"))
|
||||||
return _dataStore
|
return _dataStore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,15 @@ import SwiftUI
|
||||||
struct RetrieveApiKeyView: View {
|
struct RetrieveApiKeyView: View {
|
||||||
@State var apiKey: String = ""
|
@State var apiKey: String = ""
|
||||||
@State var password: String = ""
|
@State var password: String = ""
|
||||||
|
var showAlert: Bool {
|
||||||
|
get {
|
||||||
|
if case .invalidCredentials = self.dataStore.error {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -22,32 +31,42 @@ struct RetrieveApiKeyView: View {
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
SecureField("prompt_password", text: self.$password)
|
SecureField("prompt_password", text: self.$password)
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
Button(action: {
|
.onSubmit {
|
||||||
self.dataStore.connectWithPassword(self.password)
|
Task {
|
||||||
}, label: {
|
await self.dataStore.connectWithPassword(self.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: { Task {
|
||||||
|
await self.dataStore.connectWithPassword(self.password)
|
||||||
|
} }, label: {
|
||||||
Text("connect_with_password")
|
Text("connect_with_password")
|
||||||
})
|
})
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
OrDivider()
|
OrDivider()
|
||||||
SecureField("prompt_api_key", text: self.$apiKey)
|
SecureField("prompt_api_key", text: self.$apiKey)
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
Button(action: {
|
.onSubmit {
|
||||||
self.dataStore.connectWithApiKey(self.apiKey)
|
Task {
|
||||||
}, label: {
|
await self.dataStore.connectWithApiKey(self.apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: { Task {
|
||||||
|
await self.dataStore.connectWithApiKey(self.apiKey)
|
||||||
|
} }, label: {
|
||||||
Text("connect_with_api_key")
|
Text("connect_with_api_key")
|
||||||
})
|
})
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.keyboardAwarePadding()
|
.keyboardAwarePadding()
|
||||||
.alert(isPresented: .constant(self.dataStore.pihole == .failure(.invalidCredentials)), content: {
|
.alert(isPresented: .constant(showAlert), content: {
|
||||||
Alert(title: Text("connection_failed"), message: Text("verify_credentials"), dismissButton: .default(Text("OK"), action: {
|
Alert(title: Text("connection_failed"), message: Text("verify_credentials"), dismissButton: .default(Text("OK"), action: {
|
||||||
self.dataStore.pihole = .failure(.missingApiKey)
|
self.dataStore.pihole = .missingApiKey
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
self.dataStore.pihole = .failure(.missingIpAddress)
|
self.dataStore.pihole = .missingIpAddress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,39 +12,34 @@ struct ScanningView: View {
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@ObservedObject var dataStore: PiHoleDataStore
|
@ObservedObject var dataStore: PiHoleDataStore
|
||||||
|
|
||||||
var stateContent: AnyView {
|
@ViewBuilder
|
||||||
|
var body: some View {
|
||||||
switch dataStore.pihole {
|
switch dataStore.pihole {
|
||||||
case .failure(.scanning(let ipAddress)):
|
case .scanning(let ipAddress):
|
||||||
return ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 10) {
|
VStack(spacing: 10) {
|
||||||
ActivityIndicatorView(.constant(true))
|
ActivityIndicatorView()
|
||||||
Text("scanning_ip_address")
|
Text("scanning_ip_address")
|
||||||
Text(verbatim: ipAddress)
|
Text(verbatim: ipAddress)
|
||||||
Button(action: {
|
Button(action: {
|
||||||
self.dataStore.cancelRequest()
|
self.dataStore.cancelScanning()
|
||||||
}, label: { Text("cancel") })
|
}, label: { Text("cancel") })
|
||||||
.buttonStyle(PiHelperButtonStyle())
|
.buttonStyle(PiHelperButtonStyle())
|
||||||
}.padding()
|
}.padding()
|
||||||
}.toAnyView()
|
}
|
||||||
default:
|
default:
|
||||||
self.presentationMode.wrappedValue.dismiss()
|
EmptyView().onAppear {
|
||||||
return EmptyView().toAnyView()
|
self.presentationMode.wrappedValue.dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
stateContent
|
|
||||||
.onDisappear {
|
|
||||||
self.dataStore.cancelRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ScanningView_Previews: PreviewProvider {
|
struct ScanningView_Previews: PreviewProvider {
|
||||||
static var dataStore: PiHoleDataStore {
|
static var dataStore: PiHoleDataStore {
|
||||||
get {
|
get {
|
||||||
let dataStore = PiHoleDataStore()
|
let dataStore = PiHoleDataStore()
|
||||||
dataStore.pihole = .failure(.scanning("127.0.0.1"))
|
dataStore.pihole = .scanning("127.0.0.1")
|
||||||
return dataStore
|
return dataStore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,11 +22,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// If we already have the address of a Pi we've previously connected to, try that
|
// If we already have the address of a Pi we've previously connected to, try that
|
||||||
let apiKey = UserDefaults.standard.string(forKey: PiHoleDataStore.API_KEY)
|
let apiKey = UserDefaults.standard.string(forKey: PiHoleDataStore.API_KEY)
|
||||||
self.dataStore = PiHoleDataStore(baseUrl: host, apiKey: apiKey)
|
self.dataStore = PiHoleDataStore(baseUrl: host, apiKey: apiKey)
|
||||||
//} else if let cString = resolver_get_dns_server_ip() {
|
|
||||||
// Otherwise the Pi is likely the DNS server (though not always true), so we can try connecting to it here
|
|
||||||
// self.dataStore!.scan(String(cString: cString))
|
|
||||||
} else {
|
} else {
|
||||||
self.dataStore = PiHoleDataStore()
|
self.dataStore = PiHoleDataStore()
|
||||||
|
Task {
|
||||||
|
await self.dataStore?.scan("pi.hole")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the SwiftUI view that provides the window contents.
|
// Create the SwiftUI view that provides the window contents.
|
||||||
|
@ -55,10 +55,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// Is there a shortcut item that has not yet been processed?
|
// Is there a shortcut item that has not yet been processed?
|
||||||
if let shortcutItem = (UIApplication.shared.delegate as! AppDelegate).shortcutItemToProcess {
|
if let shortcutItem = (UIApplication.shared.delegate as! AppDelegate).shortcutItemToProcess {
|
||||||
if shortcutItem.type == ShortcutAction.enable.rawValue {
|
if shortcutItem.type == ShortcutAction.enable.rawValue {
|
||||||
self.dataStore?.enable()
|
Task {
|
||||||
|
await self.dataStore?.enable()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let amount = shortcutItem.userInfo?["forSeconds"] as? Int
|
let amount = shortcutItem.userInfo?["forSeconds"] as? Int
|
||||||
self.dataStore?.disable(amount)
|
Task {
|
||||||
|
await self.dataStore?.disable(amount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the shorcut item so it's never processed twice.
|
// Reset the shorcut item so it's never processed twice.
|
||||||
|
|
Loading…
Reference in a new issue