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 */; };
|
||||
28EC1E3C23A448940088BA26 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EC1E3B23A448940088BA26 /* Styles.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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -88,6 +89,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -141,32 +143,33 @@
|
|||
2821266E235926E700072D52 /* Pi-helper */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
28D7205B2380C2090038D439 /* resolver.c */,
|
||||
80436C6C27555BDE003D48E4 /* Pi-helper.entitlements */,
|
||||
28D720522380689E0038D439 /* PiHelper-Bridging-Header.h */,
|
||||
2821266F235926E700072D52 /* AppDelegate.swift */,
|
||||
28212671235926E700072D52 /* SceneDelegate.swift */,
|
||||
28212673235926E700072D52 /* ContentView.swift */,
|
||||
282126AD235BF21D00072D52 /* AddPiHoleView.swift */,
|
||||
28212675235926EB00072D52 /* Assets.xcassets */,
|
||||
2821267A235926EB00072D52 /* LaunchScreen.storyboard */,
|
||||
28D7205A2380C2090038D439 /* resolver.h */,
|
||||
2821267D235926EB00072D52 /* Info.plist */,
|
||||
28212677235926EB00072D52 /* Preview Content */,
|
||||
282126A5235BE6EE00072D52 /* PiHoleDataStore.swift */,
|
||||
80E91FA623DE160100B7FB55 /* AboutView.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 */,
|
||||
282126A9235BEE0F00072D52 /* PiHoleApiService.swift */,
|
||||
282126A5235BE6EE00072D52 /* PiHoleDataStore.swift */,
|
||||
282126AB235BF09800072D52 /* PiHoleDetailsView.swift */,
|
||||
282126AF235BF52F00072D52 /* ActivityIndicatorView.swift */,
|
||||
282126B6235C0F5400072D52 /* Localizable.strings */,
|
||||
28D7205823808E8C0038D439 /* Extensions.swift */,
|
||||
28D7205A2380C2090038D439 /* resolver.h */,
|
||||
28D7205B2380C2090038D439 /* resolver.c */,
|
||||
280AA3EA2390B302007841F1 /* RetrieveApiKeyView.swift */,
|
||||
28EC1E3923A441F30088BA26 /* OrDivider.swift */,
|
||||
28EC1E3B23A448940088BA26 /* Styles.swift */,
|
||||
28D2125B23A59057003B33F2 /* ScanningView.swift */,
|
||||
80654D7A24B0E92A007B1E1A /* DisableCustomTimeView.swift */,
|
||||
280F0D6223DA9B1800D341B7 /* KeyboardHelper.swift */,
|
||||
80E91FA623DE160100B7FB55 /* AboutView.swift */,
|
||||
28212671235926E700072D52 /* SceneDelegate.swift */,
|
||||
28EC1E3B23A448940088BA26 /* Styles.swift */,
|
||||
28212675235926EB00072D52 /* Assets.xcassets */,
|
||||
2821267A235926EB00072D52 /* LaunchScreen.storyboard */,
|
||||
282126B6235C0F5400072D52 /* Localizable.strings */,
|
||||
28212677235926EB00072D52 /* Preview Content */,
|
||||
80A8CF842782190400CCCEC5 /* AsyncData.swift */,
|
||||
);
|
||||
path = "Pi-helper";
|
||||
sourceTree = "<group>";
|
||||
|
@ -358,6 +361,7 @@
|
|||
28D7205923808E8C0038D439 /* Extensions.swift in Sources */,
|
||||
282126A8235BE72800072D52 /* PiHole.swift in Sources */,
|
||||
80654D7B24B0E92A007B1E1A /* DisableCustomTimeView.swift in Sources */,
|
||||
80A8CF852782190400CCCEC5 /* AsyncData.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "28212681235926EB00072D52"
|
||||
BuildableName = "PihelperTests.xctest"
|
||||
BuildableName = "Pi-helperTests.xctest"
|
||||
BlueprintName = "Pi-helperTests"
|
||||
ReferencedContainer = "container:Pi-helper.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
@ -43,7 +43,7 @@
|
|||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "2821268C235926EB00072D52"
|
||||
BuildableName = "PihelperUITests.xctest"
|
||||
BuildableName = "Pi-helperUITests.xctest"
|
||||
BlueprintName = "Pi-helperUITests"
|
||||
ReferencedContainer = "container:Pi-helper.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
|
|
@ -9,26 +9,22 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ActivityIndicatorView: View {
|
||||
var isAnimating: Binding<Bool>
|
||||
@State private var rotation = 0.0
|
||||
|
||||
var body: some View {
|
||||
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 {
|
||||
return withAnimation(self.isAnimating.wrappedValue ? Animation.linear(duration: 1).repeatForever(autoreverses: false) : Animation.linear) {
|
||||
self.rotation = 360.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_ isAnimating: Binding<Bool>) {
|
||||
self.isAnimating = isAnimating
|
||||
self.rotation = 360
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ActivityIndicatorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ActivityIndicatorView(.constant(true))
|
||||
ActivityIndicatorView()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,17 @@ import SwiftUI
|
|||
|
||||
struct AddPiHoleView: View {
|
||||
@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 {
|
||||
NavigationView {
|
||||
|
@ -17,9 +28,9 @@ struct AddPiHoleView: View {
|
|||
VStack(spacing: 30) {
|
||||
Image("logo")
|
||||
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: {
|
||||
self.dataStore.pihole = .failure(.missingIpAddress)
|
||||
self.dataStore.pihole = .missingIpAddress
|
||||
}))
|
||||
})
|
||||
OrDivider()
|
||||
|
@ -28,20 +39,27 @@ struct AddPiHoleView: View {
|
|||
TextField("ip_address", text: $ipAddress)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.keyboardType(.default)
|
||||
Button(action: {
|
||||
self.dataStore.connect(self.ipAddress)
|
||||
}, label: { Text("connect") })
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.onSubmit {
|
||||
Task {
|
||||
await self.dataStore.connect(self.ipAddress)
|
||||
}
|
||||
}
|
||||
Button(action: { Task {
|
||||
await self.dataStore.connect(self.ipAddress)
|
||||
} }, label: { Text("connect") })
|
||||
.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: {
|
||||
self.dataStore.pihole = .failure(.missingIpAddress)
|
||||
self.dataStore.pihole = .missingIpAddress
|
||||
}))
|
||||
})
|
||||
}
|
||||
.padding()
|
||||
.keyboardAwarePadding()
|
||||
}
|
||||
.navigationBarHidden(self.dataStore.pihole == .failure(.missingIpAddress))
|
||||
.navigationBarHidden(self.hideNavigationBar)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
@ -68,17 +86,19 @@ struct ScanButton: View {
|
|||
Text("scan_for_pihole")
|
||||
.multilineTextAlignment(.center)
|
||||
Button(action: {
|
||||
self.dataStore.beginScanning(ipAddress)
|
||||
self.dataStore.scanTask = Task {
|
||||
await self.dataStore.beginScanning(ipAddress)
|
||||
}
|
||||
}, label: { Text("scan") })
|
||||
.buttonStyle(PiHelperButtonStyle())
|
||||
NavigationLink(
|
||||
destination: ScanningView(dataStore: self.dataStore),
|
||||
isActive: .constant(self.dataStore.pihole == .failure(.scanning(""))),
|
||||
isActive: $dataStore.isScanning,
|
||||
label: { EmptyView() }
|
||||
)
|
||||
NavigationLink(
|
||||
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() }
|
||||
)
|
||||
}.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())
|
||||
Button(action: {
|
||||
self.dataStore.disable(Int(self.duration) ?? 0, unit: self.unit)
|
||||
}, label: { Text(LocalizedStringKey("disable")) })
|
||||
Button(action: { Task {
|
||||
await self.dataStore.disable(Int(self.duration) ?? 0, unit: self.unit)
|
||||
} }, label: { Text(LocalizedStringKey("disable")) })
|
||||
.buttonStyle(PiHelperButtonStyle())
|
||||
}
|
||||
.padding()
|
||||
|
|
|
@ -157,7 +157,7 @@ struct TopItemsResponse: Codable {
|
|||
}
|
||||
}
|
||||
|
||||
enum PiHoleStatus: Equatable {
|
||||
enum Status: Equatable {
|
||||
case enabled
|
||||
case disabled(_ duration: String? = nil)
|
||||
case unknown
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class PiHoleApiService {
|
||||
let decoder: JSONDecoder
|
||||
|
@ -20,70 +19,61 @@ class PiHoleApiService {
|
|||
self.decoder = decoder
|
||||
}
|
||||
|
||||
func getVersion() -> AnyPublisher<VersionResponse, NetworkError> {
|
||||
return get(queries: ["version": ""])
|
||||
func getVersion() async throws -> VersionResponse {
|
||||
return try await get(queries: ["version": ""])
|
||||
}
|
||||
|
||||
func loadSummary() -> AnyPublisher<PiHole, NetworkError> {
|
||||
return get()
|
||||
func loadSummary() async throws -> PiHole {
|
||||
return try await get()
|
||||
}
|
||||
|
||||
func enable() -> AnyPublisher<StatusUpdate, NetworkError> {
|
||||
return get(true, queries: ["enable": ""])
|
||||
func enable() async throws -> StatusUpdate {
|
||||
return try await get(true, queries: ["enable": ""])
|
||||
}
|
||||
|
||||
func getTopItems() -> AnyPublisher<TopItemsResponse, NetworkError> {
|
||||
return get(true, queries: ["topItems": "25"])
|
||||
func getTopItems() async throws -> TopItemsResponse {
|
||||
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]()
|
||||
if let timeToDisable = forSeconds {
|
||||
params["disable"] = String(timeToDisable)
|
||||
} else {
|
||||
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 {
|
||||
return Result<UInt, NetworkError>.Publisher(.failure(.invalidUrl))
|
||||
.eraseToAnyPublisher()
|
||||
throw NetworkError.invalidUrl
|
||||
}
|
||||
guard let url = URL(string: baseUrl + "/custom_disable_timer") else {
|
||||
return Result<UInt, NetworkError>.Publisher(.failure(.invalidUrl))
|
||||
.eraseToAnyPublisher()
|
||||
throw NetworkError.invalidUrl
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.timeoutInterval = 0.5
|
||||
let task = URLSession.shared.dataTaskPublisher(for: request)
|
||||
.tryMap { (data, res) -> UInt in
|
||||
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
||||
switch (res as? HTTPURLResponse)?.statusCode {
|
||||
case 400: throw NetworkError.badRequest
|
||||
case 401, 403: throw NetworkError.unauthorized
|
||||
case 404: throw NetworkError.notFound
|
||||
default: throw NetworkError.unknown
|
||||
}
|
||||
}
|
||||
let dataString = String(data: data, encoding: .utf8) ?? "0"
|
||||
return UInt(dataString) ?? 0
|
||||
let (data, res) = try await URLSession.shared.data(for: request)
|
||||
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
||||
switch (res as? HTTPURLResponse)?.statusCode {
|
||||
case 400: throw NetworkError.badRequest
|
||||
case 401, 403: throw NetworkError.unauthorized
|
||||
case 404: throw NetworkError.notFound
|
||||
default: throw NetworkError.unknown(nil)
|
||||
}
|
||||
}
|
||||
.mapError {
|
||||
return $0 as! NetworkError
|
||||
}
|
||||
return task.eraseToAnyPublisher()
|
||||
let dataString = String(data: data, encoding: .utf8) ?? "0"
|
||||
return UInt(dataString) ?? 0
|
||||
}
|
||||
|
||||
private func get<ResultType: Codable>(
|
||||
_ requiresAuth: Bool = false,
|
||||
queries: [String: String]? = nil
|
||||
) -> AnyPublisher<ResultType, NetworkError> {
|
||||
) async throws -> ResultType {
|
||||
guard let baseUrl = self.baseUrl else {
|
||||
return Result<ResultType, NetworkError>.Publisher(.failure(.invalidUrl))
|
||||
.eraseToAnyPublisher()
|
||||
throw NetworkError.invalidUrl
|
||||
}
|
||||
|
||||
var combinedEndPoint = baseUrl + "/admin/api.php"
|
||||
|
@ -101,7 +91,7 @@ class PiHoleApiService {
|
|||
}
|
||||
|
||||
guard let url = URL(string: combinedEndPoint) else {
|
||||
return Result.Publisher(.failure(.invalidUrl)).eraseToAnyPublisher()
|
||||
throw NetworkError.invalidUrl
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
@ -109,23 +99,20 @@ class PiHoleApiService {
|
|||
request.httpMethod = "GET"
|
||||
request.timeoutInterval = 0.5
|
||||
|
||||
let task = URLSession.shared.dataTaskPublisher(for: request)
|
||||
.tryMap { (data, res) -> Data in
|
||||
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
||||
switch (res as? HTTPURLResponse)?.statusCode {
|
||||
case 400: throw NetworkError.badRequest
|
||||
case 401, 403: throw NetworkError.unauthorized
|
||||
case 404: throw NetworkError.notFound
|
||||
default: throw NetworkError.unknown
|
||||
}
|
||||
do {
|
||||
let (data, res) = try await URLSession.shared.data(for: request)
|
||||
guard let response = res as? HTTPURLResponse, 200...299 ~= response.statusCode else {
|
||||
switch (res as? HTTPURLResponse)?.statusCode {
|
||||
case 400: throw NetworkError.badRequest
|
||||
case 401, 403: throw NetworkError.unauthorized
|
||||
case 404: throw NetworkError.notFound
|
||||
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 notFound
|
||||
case unauthorized
|
||||
case unknown
|
||||
case unknown(Error?)
|
||||
case invalidUrl
|
||||
case jsonParsingFailed(Error)
|
||||
|
||||
static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.loading, .loading):
|
||||
return true
|
||||
case (.cancelled, .cancelled):
|
||||
return true
|
||||
case (.badRequest, .badRequest):
|
||||
|
@ -151,8 +136,8 @@ enum NetworkError: Error, Equatable {
|
|||
return true
|
||||
case (.unauthorized, .unauthorized):
|
||||
return true
|
||||
case (.unknown, .unknown):
|
||||
return true
|
||||
case (.unknown(let error1), .unknown(let error2)):
|
||||
return error1?.localizedDescription == error2?.localizedDescription
|
||||
case (.invalidUrl, .invalidUrl):
|
||||
return true
|
||||
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 UIKit
|
||||
|
||||
@MainActor
|
||||
class PiHoleDataStore: ObservableObject {
|
||||
private let IP_MIN = 0
|
||||
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 pihole: Result<PiHoleStatus, PiHoleError>
|
||||
@Published var pihole: PiholeStatus = .empty
|
||||
@Published var error: PiHoleError? = nil
|
||||
@Published var apiKey: String? = nil {
|
||||
didSet {
|
||||
UserDefaults.standard.set(apiKey, forKey: PiHoleDataStore.API_KEY)
|
||||
|
@ -30,9 +43,10 @@ class PiHoleDataStore: ObservableObject {
|
|||
}
|
||||
}
|
||||
private var shouldMonitorStatus = false
|
||||
var scanTask: Task<Any, Error>? = nil
|
||||
|
||||
private func prependScheme(_ ipAddress: String?) -> String? {
|
||||
guard let host = ipAddress else {
|
||||
guard let host = ipAddress?.lowercased() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -47,162 +61,109 @@ class PiHoleDataStore: ObservableObject {
|
|||
return host
|
||||
}
|
||||
|
||||
func monitorStatus() {
|
||||
self.shouldMonitorStatus = true
|
||||
doMonitorStatus()
|
||||
}
|
||||
|
||||
private func doMonitorStatus() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
|
||||
if !self.shouldMonitorStatus {
|
||||
return
|
||||
func monitorStatus() async {
|
||||
while !Task.isCancelled {
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
await loadSummary()
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
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 {
|
||||
self.pihole = .failure(.connectionFailed)
|
||||
self.error = .connectionFailed(rawIpAddress)
|
||||
return
|
||||
}
|
||||
|
||||
self.apiService.baseUrl = formattedIpAddress
|
||||
currentRequest = self.apiService.getVersion()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (completion) in
|
||||
switch completion {
|
||||
case .finished:
|
||||
return
|
||||
case .failure(_):
|
||||
self.pihole = .failure(.connectionFailed)
|
||||
}
|
||||
}, receiveValue: { version in
|
||||
self.baseUrl = formattedIpAddress
|
||||
self.pihole = .failure(.missingApiKey)
|
||||
})
|
||||
do {
|
||||
_ = try await self.apiService.getVersion()
|
||||
self.baseUrl = formattedIpAddress
|
||||
self.pihole = .missingApiKey
|
||||
} catch {
|
||||
self.error = .connectionFailed(formattedIpAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private func scan(_ ipAddresses: [String]) {
|
||||
if ipAddresses.isEmpty {
|
||||
self.pihole = .failure(.scanFailed)
|
||||
return
|
||||
func scan(_ ipAddress: String) async -> Bool {
|
||||
self.pihole = PiholeStatus.scanning(ipAddress)
|
||||
do {
|
||||
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() {
|
||||
self.baseUrl = 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() {
|
||||
connectWithApiKey(hash)
|
||||
await connectWithApiKey(hash)
|
||||
} else {
|
||||
self.pihole = .failure(.invalidCredentials)
|
||||
self.error = .invalidCredentials
|
||||
}
|
||||
}
|
||||
|
||||
func connectWithApiKey(_ apiToken: String) {
|
||||
func connectWithApiKey(_ apiToken: String) async {
|
||||
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 {
|
||||
previousStatus = try self.pihole.get()
|
||||
} catch _ {
|
||||
_ = try await self.apiService.getTopItems()
|
||||
self.apiKey = apiToken
|
||||
} catch {
|
||||
self.error = .invalidCredentials
|
||||
}
|
||||
self.pihole = .failure(.loading(previousStatus))
|
||||
|
||||
self.currentRequest = apiService.loadSummary()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (completion) in
|
||||
switch completion {
|
||||
case .finished:
|
||||
self.currentRequest = nil
|
||||
case .failure(let error):
|
||||
self.pihole = .failure(.networkError(error))
|
||||
}
|
||||
if let completionBlock = completionBlock {
|
||||
completionBlock()
|
||||
}
|
||||
}, receiveValue: { pihole in
|
||||
}
|
||||
|
||||
func loadSummary() async {
|
||||
var loadingTask: Task<Void, Error>? = nil
|
||||
if case let .success(previousState) = self.pihole {
|
||||
// Avoid showing the loading spinner immediately
|
||||
loadingTask = Task {
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
self.pihole = .loading(previousState)
|
||||
}
|
||||
} else {
|
||||
self.pihole = .loading()
|
||||
}
|
||||
do {
|
||||
let pihole = try await apiService.loadSummary()
|
||||
await MainActor.run {
|
||||
UIApplication.shared.shortcutItems = [
|
||||
UIApplicationShortcutItem(
|
||||
type: ShortcutAction.enable.rawValue,
|
||||
|
@ -233,64 +194,57 @@ class PiHoleDataStore: ObservableObject {
|
|||
userInfo: ["forSeconds": 300 as NSSecureCoding]
|
||||
)
|
||||
]
|
||||
self.updateStatus(status: pihole.status)
|
||||
})
|
||||
}
|
||||
|
||||
func enable() {
|
||||
var previousStatus: PiHoleStatus? = nil
|
||||
do {
|
||||
previousStatus = try self.pihole.get()
|
||||
} catch _ {
|
||||
}
|
||||
await self.updateStatus(pihole.status)
|
||||
loadingTask?.cancel()
|
||||
} catch {
|
||||
if let error = error as? NetworkError {
|
||||
self.error = .networkError(error)
|
||||
} else {
|
||||
print("Unhandled error! \(error)")
|
||||
}
|
||||
}
|
||||
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
|
||||
disable(forDuration * multiplier)
|
||||
await disable(forDuration * multiplier)
|
||||
}
|
||||
|
||||
func disable(_ forSeconds: Int? = nil) {
|
||||
self.showCustomDisableView = false
|
||||
var previousStatus: PiHoleStatus? = nil
|
||||
do {
|
||||
previousStatus = try self.pihole.get()
|
||||
} catch _ {
|
||||
func disable(_ forSeconds: Int? = nil) 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.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 {
|
||||
case "disabled":
|
||||
self.getDisabledDuration()
|
||||
await self.getDisabledDuration()
|
||||
default:
|
||||
self.pihole = .success(.enabled)
|
||||
}
|
||||
|
@ -298,27 +252,20 @@ class PiHoleDataStore: ObservableObject {
|
|||
|
||||
private var customDisableTimeRequest: AnyCancellable? = nil
|
||||
|
||||
private func getDisabledDuration() {
|
||||
self.customDisableTimeRequest = self.apiService.getCustomDisableTimer()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (completion) in
|
||||
switch completion {
|
||||
case .finished:
|
||||
return
|
||||
case .failure(_):
|
||||
self.pihole = .success(.disabled())
|
||||
self.customDisableTimeRequest = nil
|
||||
}
|
||||
}, receiveValue: { timestamp in
|
||||
let disabledUntil = TimeInterval(round(Double(timestamp) / 1000.0))
|
||||
let now = Date().timeIntervalSince1970
|
||||
if now > disabledUntil {
|
||||
self.pihole = .success(.disabled())
|
||||
} else {
|
||||
self.pihole = .success(.disabled(UInt(disabledUntil - now).toDurationString()))
|
||||
}
|
||||
self.customDisableTimeRequest = nil
|
||||
})
|
||||
private func getDisabledDuration() async {
|
||||
do {
|
||||
let timestamp = try await self.apiService.getCustomDisableTimer()
|
||||
let disabledUntil = TimeInterval(round(Double(timestamp) / 1000.0))
|
||||
let now = Date().timeIntervalSince1970
|
||||
if now > disabledUntil {
|
||||
self.pihole = .success(.disabled())
|
||||
} else {
|
||||
self.pihole = .success(.disabled(UInt(disabledUntil - now).toDurationString()))
|
||||
}
|
||||
self.customDisableTimeRequest = nil
|
||||
} catch {
|
||||
self.pihole = .success(.disabled())
|
||||
}
|
||||
}
|
||||
|
||||
let apiService = PiHoleApiService()
|
||||
|
@ -329,12 +276,11 @@ class PiHoleDataStore: ObservableObject {
|
|||
self.baseUrl = baseUrl
|
||||
self.apiKey = apiKey
|
||||
if baseUrl == nil {
|
||||
self.pihole = .failure(.missingIpAddress)
|
||||
self.pihole = .missingIpAddress
|
||||
} else if apiKey == nil {
|
||||
self.pihole = .failure(.missingApiKey)
|
||||
self.pihole = .missingApiKey
|
||||
} else {
|
||||
self.pihole = .failure(.networkError(.loading))
|
||||
self.loadSummary()
|
||||
self.pihole = .loading(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -344,32 +290,49 @@ enum ShortcutAction: String {
|
|||
case disable = "DisableAction"
|
||||
}
|
||||
|
||||
enum PiHoleError : Error, Equatable {
|
||||
case networkError(_ error: NetworkError)
|
||||
case loading(_ previousStatus: PiHoleStatus? = nil)
|
||||
enum PiholeStatus: Equatable {
|
||||
case empty
|
||||
case loading(_ previousStatus: Status? = nil)
|
||||
case error(_ error: Error)
|
||||
case scanning(_ ipAddress: String)
|
||||
case missingIpAddress
|
||||
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 scanFailed
|
||||
case connectionFailed
|
||||
case connectionFailed(_ host: String)
|
||||
|
||||
static func == (lhs: PiHoleError, rhs: PiHoleError) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.networkError(let error1), .networkError(let error2)):
|
||||
return error1 == error2
|
||||
case (.scanning(_), .scanning(_)):
|
||||
return true
|
||||
case (.missingIpAddress, .missingIpAddress):
|
||||
return true
|
||||
case (.missingApiKey, .missingApiKey):
|
||||
return true
|
||||
case (.invalidCredentials, .invalidCredentials):
|
||||
return true
|
||||
case (.scanFailed, .scanFailed):
|
||||
return true
|
||||
case (.connectionFailed, .connectionFailed):
|
||||
return true
|
||||
case (.connectionFailed(let lIp), .connectionFailed(let rIp)):
|
||||
return lIp == rIp
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -9,30 +9,32 @@
|
|||
import SwiftUI
|
||||
|
||||
struct PiHoleDetailsView: View {
|
||||
var stateContent: AnyView {
|
||||
@ViewBuilder
|
||||
func stateContent() -> some View {
|
||||
switch self.dataStore.pihole {
|
||||
case .success(let pihole):
|
||||
return PiHoleActionsView(dataStore: self.dataStore, status: pihole).toAnyView()
|
||||
case .failure(.loading(let previousStatus)):
|
||||
PiHoleActionsView(dataStore: self.dataStore, status: pihole)
|
||||
case .loading(let previousStatus):
|
||||
if let status = previousStatus {
|
||||
return PiHoleActionsView(dataStore: self.dataStore, status: status).toAnyView()
|
||||
PiHoleActionsView(dataStore: self.dataStore, status: status)
|
||||
} else {
|
||||
return ActivityIndicatorView(.constant(true)).toAnyView()
|
||||
ActivityIndicatorView()
|
||||
}
|
||||
case .failure(.networkError(let error)):
|
||||
case .error(let error):
|
||||
switch (error) {
|
||||
default:
|
||||
return PiHoleActionsView(dataStore: self.dataStore, status: .unknown).toAnyView()
|
||||
PiHoleActionsView(dataStore: self.dataStore, status: .unknown)
|
||||
}
|
||||
default:
|
||||
return ActivityIndicatorView(.constant(true)).toAnyView()
|
||||
ActivityIndicatorView()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
stateContent
|
||||
.navigationBarTitle("PiHelper")
|
||||
stateContent()
|
||||
.animation(.default, value: self.dataStore.pihole)
|
||||
.navigationBarTitle("Pi-helper")
|
||||
.navigationBarItems(trailing: NavigationLink(destination: AboutView(self.dataStore), label: {
|
||||
Image(systemName: "info.circle")
|
||||
.padding()
|
||||
|
@ -40,10 +42,9 @@ struct PiHoleDetailsView: View {
|
|||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.onAppear {
|
||||
self.dataStore.monitorStatus()
|
||||
}
|
||||
.onDisappear {
|
||||
self.dataStore.stopMonitoring()
|
||||
Task {
|
||||
await self.dataStore.monitorStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,53 +59,32 @@ struct PiHoleActionsView: View {
|
|||
VStack {
|
||||
HStack {
|
||||
Text("status")
|
||||
PiHoleStatusView(status)
|
||||
PiholeStatusView(status)
|
||||
}
|
||||
DurationView(status)
|
||||
PiHoleActions(self.dataStore, status: status)
|
||||
}
|
||||
}
|
||||
|
||||
let dataStore: PiHoleDataStore
|
||||
let status: PiHoleStatus
|
||||
let status: Status
|
||||
}
|
||||
|
||||
struct PiHoleStatusView: View {
|
||||
struct PiholeStatusView: View {
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
Text(status.localizedStringKey).foregroundColor(status.foregroundColor)
|
||||
}
|
||||
|
||||
let status: PiHoleStatus
|
||||
init(_ status: PiHoleStatus) {
|
||||
self.status = status
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
HStack {
|
||||
Text(status.localizedStringKey)
|
||||
.foregroundColor(status.foregroundColor)
|
||||
if case let .disabled(duration) = status, let durationString = duration {
|
||||
Text("(\(durationString))")
|
||||
.monospacedDigit()
|
||||
.foregroundColor(status.foregroundColor)
|
||||
}
|
||||
default:
|
||||
return EmptyView().toAnyView()
|
||||
}
|
||||
}
|
||||
|
||||
let status: PiHoleStatus
|
||||
init(_ status: PiHoleStatus) {
|
||||
let status: Status
|
||||
init(_ status: Status) {
|
||||
self.status = status
|
||||
}
|
||||
}
|
||||
|
@ -117,34 +97,44 @@ struct PiHoleActions: View {
|
|||
var stateContent: AnyView {
|
||||
switch status {
|
||||
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))
|
||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||
.toAnyView()
|
||||
case .enabled:
|
||||
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())
|
||||
.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())
|
||||
.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())
|
||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||
NavigationLink(
|
||||
destination: DisableCustomTimeView(self.dataStore),
|
||||
isActive: .constant(self.dataStore.showCustomDisableView),
|
||||
label: { EmptyView() }
|
||||
)
|
||||
Button(action: {
|
||||
self.dataStore.showCustomDisableView = true
|
||||
}, label: { Text("disable_custom") })
|
||||
.buttonStyle(PiHelperButtonStyle())
|
||||
.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())
|
||||
.padding(Edge.Set(arrayLiteral: [.bottom, .top]), 5.0)
|
||||
NavigationLink(
|
||||
destination: DisableCustomTimeView(self.dataStore),
|
||||
isActive: self.$dataStore.showCustomDisableView,
|
||||
label: { EmptyView() }
|
||||
)
|
||||
}.toAnyView()
|
||||
default:
|
||||
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
|
||||
let status: PiHoleStatus
|
||||
init(_ dataStore: PiHoleDataStore, status: PiHoleStatus) {
|
||||
let status: Status
|
||||
init(_ dataStore: PiHoleDataStore, status: Status) {
|
||||
self.dataStore = dataStore
|
||||
self.status = status
|
||||
}
|
||||
|
@ -164,7 +154,7 @@ struct PiHoleDetailsView_Previews: PreviewProvider {
|
|||
static var dataStore: PiHoleDataStore {
|
||||
get {
|
||||
let _dataStore = PiHoleDataStore()
|
||||
_dataStore.pihole = .success(.disabled("20"))
|
||||
_dataStore.pihole = PiholeStatus.success(Status.disabled("20"))
|
||||
return _dataStore
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,15 @@ import SwiftUI
|
|||
struct RetrieveApiKeyView: View {
|
||||
@State var apiKey: String = ""
|
||||
@State var password: String = ""
|
||||
var showAlert: Bool {
|
||||
get {
|
||||
if case .invalidCredentials = self.dataStore.error {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
|
@ -22,32 +31,42 @@ struct RetrieveApiKeyView: View {
|
|||
.multilineTextAlignment(.center)
|
||||
SecureField("prompt_password", text: self.$password)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
Button(action: {
|
||||
self.dataStore.connectWithPassword(self.password)
|
||||
}, label: {
|
||||
.onSubmit {
|
||||
Task {
|
||||
await self.dataStore.connectWithPassword(self.password)
|
||||
}
|
||||
}
|
||||
Button(action: { Task {
|
||||
await self.dataStore.connectWithPassword(self.password)
|
||||
} }, label: {
|
||||
Text("connect_with_password")
|
||||
})
|
||||
.buttonStyle(PiHelperButtonStyle())
|
||||
OrDivider()
|
||||
SecureField("prompt_api_key", text: self.$apiKey)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
Button(action: {
|
||||
self.dataStore.connectWithApiKey(self.apiKey)
|
||||
}, label: {
|
||||
.onSubmit {
|
||||
Task {
|
||||
await self.dataStore.connectWithApiKey(self.apiKey)
|
||||
}
|
||||
}
|
||||
Button(action: { Task {
|
||||
await self.dataStore.connectWithApiKey(self.apiKey)
|
||||
} }, label: {
|
||||
Text("connect_with_api_key")
|
||||
})
|
||||
.buttonStyle(PiHelperButtonStyle())
|
||||
}
|
||||
.padding()
|
||||
.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: {
|
||||
self.dataStore.pihole = .failure(.missingApiKey)
|
||||
self.dataStore.pihole = .missingApiKey
|
||||
}))
|
||||
})
|
||||
}
|
||||
.onDisappear {
|
||||
self.dataStore.pihole = .failure(.missingIpAddress)
|
||||
self.dataStore.pihole = .missingIpAddress
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,39 +12,34 @@ struct ScanningView: View {
|
|||
@Environment(\.presentationMode) var presentationMode
|
||||
@ObservedObject var dataStore: PiHoleDataStore
|
||||
|
||||
var stateContent: AnyView {
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
switch dataStore.pihole {
|
||||
case .failure(.scanning(let ipAddress)):
|
||||
return ScrollView {
|
||||
case .scanning(let ipAddress):
|
||||
ScrollView {
|
||||
VStack(spacing: 10) {
|
||||
ActivityIndicatorView(.constant(true))
|
||||
Text("scanning_ip_address")
|
||||
Text(verbatim: ipAddress)
|
||||
Button(action: {
|
||||
self.dataStore.cancelRequest()
|
||||
}, label: { Text("cancel") })
|
||||
.buttonStyle(PiHelperButtonStyle())
|
||||
ActivityIndicatorView()
|
||||
Text("scanning_ip_address")
|
||||
Text(verbatim: ipAddress)
|
||||
Button(action: {
|
||||
self.dataStore.cancelScanning()
|
||||
}, label: { Text("cancel") })
|
||||
.buttonStyle(PiHelperButtonStyle())
|
||||
}.padding()
|
||||
}.toAnyView()
|
||||
}
|
||||
default:
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
return EmptyView().toAnyView()
|
||||
EmptyView().onAppear {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
stateContent
|
||||
.onDisappear {
|
||||
self.dataStore.cancelRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ScanningView_Previews: PreviewProvider {
|
||||
static var dataStore: PiHoleDataStore {
|
||||
get {
|
||||
let dataStore = PiHoleDataStore()
|
||||
dataStore.pihole = .failure(.scanning("127.0.0.1"))
|
||||
dataStore.pihole = .scanning("127.0.0.1")
|
||||
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
|
||||
let apiKey = UserDefaults.standard.string(forKey: PiHoleDataStore.API_KEY)
|
||||
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 {
|
||||
self.dataStore = PiHoleDataStore()
|
||||
Task {
|
||||
await self.dataStore?.scan("pi.hole")
|
||||
}
|
||||
}
|
||||
|
||||
// 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?
|
||||
if let shortcutItem = (UIApplication.shared.delegate as! AppDelegate).shortcutItemToProcess {
|
||||
if shortcutItem.type == ShortcutAction.enable.rawValue {
|
||||
self.dataStore?.enable()
|
||||
Task {
|
||||
await self.dataStore?.enable()
|
||||
}
|
||||
} else {
|
||||
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.
|
||||
|
|
Loading…
Reference in a new issue