Add methods for automatically detecting the Pi-Hole
As soon as the user starts up the app, we'll first try to ping their DNS server, as that should in theory be the Pi-Hole. If that doesn't work, then we offer the user the chance to either scan their network or manually enter in the IP address for their Pi.
This commit is contained in:
parent
1ce7b74329
commit
703ca0717a
15 changed files with 446 additions and 209 deletions
90
.gitignore
vendored
Normal file
90
.gitignore
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# Accio dependency management
|
||||
Dependencies/
|
||||
.accio/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# Code Injection
|
||||
#
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
iOSInjectionProject/
|
|
@ -22,6 +22,9 @@
|
|||
282126AE235BF21D00072D52 /* AddPiHoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126AD235BF21D00072D52 /* AddPiHoleView.swift */; };
|
||||
282126B0235BF52F00072D52 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126AF235BF52F00072D52 /* ActivityIndicatorView.swift */; };
|
||||
282126B4235C0F5400072D52 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 282126B6235C0F5400072D52 /* Localizable.strings */; };
|
||||
28D72051238067F30038D439 /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 28D72050238067EC0038D439 /* libresolv.9.tbd */; };
|
||||
28D7205923808E8C0038D439 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D7205823808E8C0038D439 /* Extensions.swift */; };
|
||||
28D7205C2380C2090038D439 /* resolver.c in Sources */ = {isa = PBXBuildFile; fileRef = 28D7205B2380C2090038D439 /* resolver.c */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -65,6 +68,12 @@
|
|||
282126B3235C0EBF00072D52 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/LaunchScreen.strings"; sourceTree = "<group>"; };
|
||||
282126B5235C0F5400072D52 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
282126B8235C0F8400072D52 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
28D7204E238067B20038D439 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
|
||||
28D72050238067EC0038D439 /* libresolv.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.9.tbd; path = usr/lib/libresolv.9.tbd; sourceTree = SDKROOT; };
|
||||
28D720522380689E0038D439 /* Pi-Helper-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Pi-Helper-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
28D7205823808E8C0038D439 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
28D7205A2380C2090038D439 /* resolver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = resolver.h; sourceTree = "<group>"; };
|
||||
28D7205B2380C2090038D439 /* resolver.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = resolver.c; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -72,6 +81,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
28D72051238067F30038D439 /* libresolv.9.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -99,6 +109,7 @@
|
|||
28212685235926EB00072D52 /* Pi-HelperTests */,
|
||||
28212690235926EB00072D52 /* Pi-HelperUITests */,
|
||||
2821266D235926E700072D52 /* Products */,
|
||||
28D7204D238067B20038D439 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
@ -115,6 +126,7 @@
|
|||
2821266E235926E700072D52 /* Pi-Helper */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
28D720522380689E0038D439 /* Pi-Helper-Bridging-Header.h */,
|
||||
2821266F235926E700072D52 /* AppDelegate.swift */,
|
||||
28212671235926E700072D52 /* SceneDelegate.swift */,
|
||||
28212673235926E700072D52 /* ContentView.swift */,
|
||||
|
@ -129,6 +141,9 @@
|
|||
282126AB235BF09800072D52 /* PiHoleDetailsView.swift */,
|
||||
282126AF235BF52F00072D52 /* ActivityIndicatorView.swift */,
|
||||
282126B6235C0F5400072D52 /* Localizable.strings */,
|
||||
28D7205823808E8C0038D439 /* Extensions.swift */,
|
||||
28D7205A2380C2090038D439 /* resolver.h */,
|
||||
28D7205B2380C2090038D439 /* resolver.c */,
|
||||
);
|
||||
path = "Pi-Helper";
|
||||
sourceTree = "<group>";
|
||||
|
@ -159,6 +174,15 @@
|
|||
path = "Pi-HelperUITests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
28D7204D238067B20038D439 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
28D72050238067EC0038D439 /* libresolv.9.tbd */,
|
||||
28D7204E238067B20038D439 /* libresolv.tbd */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -227,6 +251,7 @@
|
|||
TargetAttributes = {
|
||||
2821266B235926E700072D52 = {
|
||||
CreatedOnToolsVersion = 11.0;
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
28212681235926EB00072D52 = {
|
||||
CreatedOnToolsVersion = 11.0;
|
||||
|
@ -297,9 +322,11 @@
|
|||
282126AA235BEE0F00072D52 /* PiHoleApiService.swift in Sources */,
|
||||
282126AE235BF21D00072D52 /* AddPiHoleView.swift in Sources */,
|
||||
28212670235926E700072D52 /* AppDelegate.swift in Sources */,
|
||||
28D7205C2380C2090038D439 /* resolver.c in Sources */,
|
||||
28212672235926E700072D52 /* SceneDelegate.swift in Sources */,
|
||||
282126B0235BF52F00072D52 /* ActivityIndicatorView.swift in Sources */,
|
||||
28212674235926E700072D52 /* ContentView.swift in Sources */,
|
||||
28D7205923808E8C0038D439 /* Extensions.swift in Sources */,
|
||||
282126A8235BE72800072D52 /* PiHole.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -477,6 +504,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Pi-Helper/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
|
||||
|
@ -488,6 +516,8 @@
|
|||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.wbrawner.Pi-Helper";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Pi-Helper/Pi-Helper-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
@ -497,6 +527,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Pi-Helper/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
|
||||
|
@ -508,6 +539,7 @@
|
|||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.wbrawner.Pi-Helper";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Pi-Helper/Pi-Helper-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "59EB2C60-D493-44EF-8FB1-43FCFB54B3E8"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
<Breakpoints>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "794E3259-7833-47A1-B889-ACE3E7B7B788"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/PiHoleDataStore.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "21"
|
||||
endingLineNumber = "21"
|
||||
landmarkName = "loadSummary(_:apiKey:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "70FF2209-0733-48C4-B198-1F58A9D129D8"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/PiHoleDataStore.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "33"
|
||||
endingLineNumber = "33"
|
||||
landmarkName = "loadSummary(_:apiKey:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "61C30D69-66FB-4844-8C00-1A8C239EB9E9"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/PiHoleDataStore.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "36"
|
||||
endingLineNumber = "36"
|
||||
landmarkName = "loadSummary(_:apiKey:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "1206CF28-BA55-4380-8D4D-6806755092E4"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/ContentView.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "19"
|
||||
endingLineNumber = "19"
|
||||
landmarkName = "stateContent"
|
||||
landmarkType = "24">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "19509035-C3D0-4116-8A99-AAFB00F61136"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/ContentView.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "21"
|
||||
endingLineNumber = "21"
|
||||
landmarkName = "stateContent"
|
||||
landmarkType = "24">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "B75FD518-9E34-4579-89C6-27498913E3DC"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/ContentView.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "23"
|
||||
endingLineNumber = "23"
|
||||
landmarkName = "stateContent"
|
||||
landmarkType = "24">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "F5CC5BA4-EE2D-45D1-8487-3D9C78C6D735"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/PiHoleDataStore.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "104"
|
||||
endingLineNumber = "104"
|
||||
landmarkName = "disable(_:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "6C7C0D86-0A5F-403F-A1B6-36AB78805BD1"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/PiHoleDataStore.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "101"
|
||||
endingLineNumber = "101"
|
||||
landmarkName = "disable(_:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "A60B4D50-A0F1-4565-AB8B-B40648E7A622"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/PiHoleApiService.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "64"
|
||||
endingLineNumber = "64"
|
||||
landmarkName = "get(_:queries:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "918A86D9-9B8C-4C31-8536-B20357C2E017"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Pi-Helper/SceneDelegate.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "79"
|
||||
endingLineNumber = "79"
|
||||
landmarkName = "windowScene(_:performActionFor:completionHandler:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
</Breakpoints>
|
||||
</Bucket>
|
|
@ -9,17 +9,22 @@
|
|||
import SwiftUI
|
||||
|
||||
struct AddPiHoleView: View {
|
||||
@State var ipAddress: String = ""
|
||||
@State var apiKey: String = ""
|
||||
var statefulContent: AnyView {
|
||||
switch self.dataStore.pihole {
|
||||
case .failure(let piholeError):
|
||||
switch piholeError {
|
||||
case .scanning(let ipAddress):
|
||||
return ScanningView(dataStore: self.dataStore, ipAddress: ipAddress).toAnyView()
|
||||
default:
|
||||
return ManuallyAddPiHoleView(dataStore: self.dataStore).toAnyView()
|
||||
}
|
||||
default:
|
||||
return ManuallyAddPiHoleView(dataStore: self.dataStore).toAnyView()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("add_pihole")
|
||||
TextField("ip_address", text: $ipAddress)
|
||||
SecureField("api_key_optional", text: $apiKey)
|
||||
Button(action: { self.dataStore.loadSummary(self.ipAddress, apiKey: self.apiKey) }, label: { Text("connect") })
|
||||
}
|
||||
.padding()
|
||||
statefulContent
|
||||
}
|
||||
|
||||
@ObservedObject var dataStore: PiHoleDataStore
|
||||
|
@ -28,6 +33,62 @@ struct AddPiHoleView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct ManuallyAddPiHoleView: View {
|
||||
@State var ipAddress: String = ""
|
||||
@State var apiKey: String = ""
|
||||
@ObservedObject var dataStore: PiHoleDataStore
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("add_pihole")
|
||||
TextField("ip_address", text: $ipAddress)
|
||||
SecureField("api_key_optional", text: $apiKey)
|
||||
Button(action: {
|
||||
self.dataStore.baseUrl = self.ipAddress
|
||||
self.dataStore.apiKey = self.apiKey
|
||||
self.dataStore.loadSummary()
|
||||
}, label: { Text("connect") })
|
||||
.padding()
|
||||
ScanButton(dataStore: self.dataStore)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct ScanButton: View {
|
||||
@ObservedObject var dataStore: PiHoleDataStore
|
||||
|
||||
var statefulContent: AnyView {
|
||||
if let deviceIpAddress = resolver_get_device_ip() {
|
||||
return Button(action: {
|
||||
self.dataStore.beginScanning(String(cString: deviceIpAddress))
|
||||
}, label: { Text("scan") }).padding().toAnyView()
|
||||
} else {
|
||||
return EmptyView().toAnyView()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View { statefulContent }
|
||||
}
|
||||
|
||||
struct ScanningView: View {
|
||||
let dataStore: PiHoleDataStore
|
||||
let ipAddress: String
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ActivityIndicatorView(.constant(true))
|
||||
Text("scanning_ip_address")
|
||||
Text(verbatim: ipAddress)
|
||||
Button(action: {
|
||||
self.dataStore.cancelRequest()
|
||||
}, label: { Text("cancel") }
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddPiHoleView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddPiHoleView(PiHoleDataStore())
|
||||
|
|
|
@ -16,11 +16,11 @@ struct ContentView: View {
|
|||
var stateContent: AnyView {
|
||||
switch self.dataStore.pihole {
|
||||
case .success(_):
|
||||
return AnyView(PiHoleDetailsView(self.dataStore))
|
||||
case .failure(.loading):
|
||||
return AnyView(ActivityIndicatorView(.constant(true)))
|
||||
return PiHoleDetailsView(self.dataStore).toAnyView()
|
||||
case .failure(.networkError(.loading)):
|
||||
return ActivityIndicatorView(.constant(true)).toAnyView()
|
||||
default:
|
||||
return AnyView(AddPiHoleView(self.dataStore))
|
||||
return AddPiHoleView(self.dataStore).toAnyView()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
16
Pi-Helper/Extensions.swift
Normal file
16
Pi-Helper/Extensions.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Extensions.swift
|
||||
// Pi-Helper
|
||||
//
|
||||
// Created by Billy Brawner on 11/16/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func toAnyView() -> AnyView {
|
||||
return AnyView(self)
|
||||
}
|
||||
}
|
5
Pi-Helper/Pi-Helper-Bridging-Header.h
Normal file
5
Pi-Helper/Pi-Helper-Bridging-Header.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#include "resolver.h"
|
|
@ -110,6 +110,10 @@ struct StatusUpdate: Codable {
|
|||
let status: String
|
||||
}
|
||||
|
||||
struct VersionResponse: Codable {
|
||||
let version: Int
|
||||
}
|
||||
|
||||
enum PiHoleStatus {
|
||||
case enabled
|
||||
case disabled
|
||||
|
|
|
@ -20,6 +20,10 @@ class PiHoleApiService {
|
|||
self.decoder = decoder
|
||||
}
|
||||
|
||||
func getVersion() -> AnyPublisher<VersionResponse, NetworkError> {
|
||||
return get(queries: ["version": nil])
|
||||
}
|
||||
|
||||
func loadSummary() -> AnyPublisher<PiHole, NetworkError> {
|
||||
return get()
|
||||
}
|
||||
|
@ -68,6 +72,7 @@ class PiHoleApiService {
|
|||
var request = URLRequest(url: url)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "GET"
|
||||
request.timeoutInterval = 0.5
|
||||
|
||||
let task = URLSession.shared.dataTaskPublisher(for: request)
|
||||
.tryMap { (data, res) -> Data in
|
||||
|
@ -91,6 +96,7 @@ class PiHoleApiService {
|
|||
|
||||
enum NetworkError: Error {
|
||||
case loading
|
||||
case cancelled
|
||||
case badRequest
|
||||
case notFound
|
||||
case unauthorized
|
||||
|
|
|
@ -11,22 +11,95 @@ import Combine
|
|||
import UIKit
|
||||
|
||||
class PiHoleDataStore: ObservableObject {
|
||||
var pihole: Result<PiHole, NetworkError> = .failure(.notFound) {
|
||||
private let IP_MIN = 0
|
||||
private let IP_MAX = 255
|
||||
private var currentRequest: AnyCancellable? = nil
|
||||
@Published var pihole: Result<PiHole, PiHoleError> = .failure(.notConfigured)
|
||||
var apiKey: String? = nil {
|
||||
didSet {
|
||||
self.objectWillChange.send()
|
||||
UserDefaults.standard.set(apiKey, forKey: PiHoleDataStore.API_KEY)
|
||||
apiService.apiKey = apiKey
|
||||
}
|
||||
}
|
||||
var baseUrl: String? = nil {
|
||||
didSet {
|
||||
let safeHost = prependScheme(baseUrl)
|
||||
UserDefaults.standard.set(safeHost, forKey: PiHoleDataStore.HOST_KEY)
|
||||
apiService.baseUrl = safeHost
|
||||
}
|
||||
}
|
||||
|
||||
func loadSummary(_ host: String, apiKey: String? = nil) {
|
||||
self.pihole = .failure(.loading)
|
||||
|
||||
var safeHost = host
|
||||
if !host.starts(with: "http://") || !host.starts(with: "https://") {
|
||||
safeHost = "http://" + safeHost
|
||||
private func prependScheme(_ ipAddress: String?) -> String? {
|
||||
guard let host = ipAddress else {
|
||||
return nil
|
||||
}
|
||||
apiService.baseUrl = safeHost
|
||||
apiService.apiKey = apiKey
|
||||
_ = apiService.loadSummary()
|
||||
|
||||
if !host.starts(with: "http://") && !host.starts(with: "https://") {
|
||||
return "http://" + host
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
func beginScanning(_ ipAddress: String) {
|
||||
var addressParts = ipAddress.split(separator: ".")
|
||||
var chunks = 1
|
||||
var ipAddresses = [String]()
|
||||
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)
|
||||
}
|
||||
|
||||
private func scan(_ ipAddresses: [String]) {
|
||||
if ipAddresses.isEmpty {
|
||||
self.pihole = .failure(.notConfigured)
|
||||
return
|
||||
}
|
||||
|
||||
guard let ipAddress = prependScheme(ipAddresses[0]) else {
|
||||
return
|
||||
}
|
||||
self.apiService.baseUrl = ipAddress
|
||||
self.pihole = .failure(.scanning(ipAddress))
|
||||
print("Scanning \(ipAddress)")
|
||||
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.loadSummary()
|
||||
})
|
||||
}
|
||||
|
||||
func cancelRequest() {
|
||||
self.currentRequest?.cancel()
|
||||
self.pihole = .failure(.networkError(.cancelled))
|
||||
}
|
||||
|
||||
func loadSummary() {
|
||||
self.pihole = .failure(.networkError(.loading))
|
||||
|
||||
currentRequest = apiService.loadSummary()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (completion) in
|
||||
switch completion {
|
||||
|
@ -34,11 +107,9 @@ class PiHoleDataStore: ObservableObject {
|
|||
// no-op
|
||||
return
|
||||
case .failure(let error):
|
||||
self.pihole = .failure(error)
|
||||
self.pihole = .failure(.networkError(error))
|
||||
}
|
||||
}, receiveValue: { pihole in
|
||||
UserDefaults.standard.set(host, forKey: PiHoleDataStore.HOST_KEY)
|
||||
UserDefaults.standard.set(apiKey, forKey: PiHoleDataStore.API_KEY)
|
||||
UIApplication.shared.shortcutItems = [
|
||||
UIApplicationShortcutItem(
|
||||
type: ShortcutAction.enable.rawValue,
|
||||
|
@ -75,8 +146,8 @@ class PiHoleDataStore: ObservableObject {
|
|||
|
||||
func enable() {
|
||||
let oldPihole = try! pihole.get()
|
||||
self.pihole = .failure(.loading)
|
||||
_ = self.apiService.enable()
|
||||
self.pihole = .failure(.networkError(.loading))
|
||||
currentRequest = self.apiService.enable()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (completion) in
|
||||
switch completion {
|
||||
|
@ -84,7 +155,7 @@ class PiHoleDataStore: ObservableObject {
|
|||
// no-op
|
||||
return
|
||||
case .failure(let error):
|
||||
self.pihole = .failure(error)
|
||||
self.pihole = .failure(.networkError(error))
|
||||
}
|
||||
}, receiveValue: { newStatus in
|
||||
self.pihole = .success(oldPihole.copy(status: newStatus.status))
|
||||
|
@ -93,8 +164,8 @@ class PiHoleDataStore: ObservableObject {
|
|||
|
||||
func disable(_ forSeconds: Int? = nil) {
|
||||
let oldPihole = try! pihole.get()
|
||||
self.pihole = .failure(.loading)
|
||||
_ = self.apiService.disable(forSeconds)
|
||||
self.pihole = .failure(.networkError(.loading))
|
||||
currentRequest = self.apiService.disable(forSeconds)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (completion) in
|
||||
switch completion {
|
||||
|
@ -102,26 +173,25 @@ class PiHoleDataStore: ObservableObject {
|
|||
// no-op
|
||||
return
|
||||
case .failure(let error):
|
||||
self.pihole = .failure(error)
|
||||
self.pihole = .failure(.networkError(error))
|
||||
}
|
||||
}, receiveValue: { newStatus in
|
||||
self.pihole = .success(oldPihole.copy(status: newStatus.status))
|
||||
})
|
||||
}
|
||||
|
||||
let objectWillChange = ObservableObjectPublisher()
|
||||
let apiService = PiHoleApiService()
|
||||
static let HOST_KEY = "host"
|
||||
static let API_KEY = "apiKey"
|
||||
init() {
|
||||
if let host = UserDefaults.standard.string(forKey: PiHoleDataStore.HOST_KEY) {
|
||||
let apiKey = UserDefaults.standard.string(forKey: PiHoleDataStore.API_KEY)
|
||||
loadSummary(host, apiKey: apiKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ShortcutAction: String {
|
||||
case enable = "EnableAction"
|
||||
case disable = "DisableAction"
|
||||
}
|
||||
|
||||
enum PiHoleError : Error {
|
||||
case networkError(_ error: NetworkError)
|
||||
case scanning(_ ipAddress: String)
|
||||
case notConfigured
|
||||
}
|
||||
|
|
|
@ -18,8 +18,20 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
|
||||
self.dataStore = PiHoleDataStore()
|
||||
if let host = UserDefaults.standard.string(forKey: PiHoleDataStore.HOST_KEY) {
|
||||
// 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!.baseUrl = host
|
||||
self.dataStore!.apiKey = apiKey
|
||||
self.dataStore!.loadSummary()
|
||||
} 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 there
|
||||
let dnsServerIp = String(cString: cString)
|
||||
self.dataStore!.baseUrl = dnsServerIp
|
||||
self.dataStore!.loadSummary()
|
||||
}
|
||||
|
||||
// Create the SwiftUI view that provides the window contents.
|
||||
let contentView = ContentView(self.dataStore!)
|
||||
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
"add_pihole" = "Add the IP address or hostname of your Pi-Hole to be able to view statistics. To enable or disable the Pi-Hole, you'll also need to enter the API key.";
|
||||
"ip_address" = "IP Address";
|
||||
"api_key_optional" = "API Key (Optional)";
|
||||
"scanning_ip_address" = "Scanning the network for your Pi-Hole…";
|
||||
"scan" = "Scan";
|
||||
"cancel" = "Cancel";
|
||||
"connect" = "Connect";
|
||||
"status" = "Status";
|
||||
"enabled" = "Enabled";
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
"add_pihole" = "Agrega la dirección IP o nombre de host de tu Pi-Hole para poder ver las estadísticas. Para habilitar o deshabilitar el Pi-Hole, también es necesario entregar la clave API.";
|
||||
"ip_address" = "Dirección IP";
|
||||
"api_key_optional" = "Clave API (Opcional)";
|
||||
"scanning_ip_address" = "Escaneando la red para el Pi-Hole…";
|
||||
"scan" = "Escanear";
|
||||
"cancel" = "Cancelar";
|
||||
"connect" = "Conectar";
|
||||
"status" = "Estado";
|
||||
"enabled" = "Habilitado";
|
||||
|
|
77
Pi-Helper/resolver.c
Normal file
77
Pi-Helper/resolver.c
Normal file
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// resolver.c
|
||||
// Pi-Helper
|
||||
//
|
||||
// Created by Billy Brawner on 11/16/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
#include "resolver.h"
|
||||
|
||||
char * resolver_get_dns_server_ip(void) {
|
||||
res_state state = malloc(sizeof(res_state));
|
||||
res_ninit(state);
|
||||
union res_sockaddr_union sockaddr_unions[MAX_SERVERS];
|
||||
int servers_found = res_getservers(state, sockaddr_unions, MAX_SERVERS);
|
||||
res_ndestroy(state);
|
||||
for (int i = 0; i < servers_found; i++) {
|
||||
union res_sockaddr_union sockaddr_union = sockaddr_unions[i];
|
||||
if (sockaddr_union.sin.sin_len < 1) {
|
||||
continue;
|
||||
}
|
||||
char * ipAddress = malloc(NI_MAXHOST + 1);
|
||||
ipAddress[NI_MAXHOST] = '\0';
|
||||
getnameinfo(
|
||||
&sockaddr_union.sin,
|
||||
sockaddr_union.sin.sin_len,
|
||||
ipAddress,
|
||||
MAX_IP_ADDR_LEN,
|
||||
NULL,
|
||||
0,
|
||||
NI_NUMERICHOST
|
||||
);
|
||||
return ipAddress;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char * resolver_get_device_ip(void) {
|
||||
struct ifaddrs *ifaddr, *ifa;
|
||||
if (getifaddrs(&ifaddr) == -1) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int n;
|
||||
for (ifa = ifaddr, n = 0; ifa != NULL; ifa = ifa->ifa_next, n++) {
|
||||
if (ifa->ifa_addr == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ifa->ifa_addr->sa_family != AF_INET) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp("en0", ifa->ifa_name) == 0
|
||||
|| strcmp("en1", ifa->ifa_name) == 0
|
||||
|| strcmp("en2", ifa->ifa_name) == 0
|
||||
|| strcmp("en3", ifa->ifa_name) == 0
|
||||
|| strcmp("en4", ifa->ifa_name) == 0) {
|
||||
char *ipAddress = malloc(NI_MAXHOST + 1);
|
||||
ipAddress[NI_NUMERICHOST] = '\0';
|
||||
getnameinfo(
|
||||
ifa->ifa_addr,
|
||||
ifa->ifa_addr->sa_len,
|
||||
ipAddress,
|
||||
NI_MAXHOST,
|
||||
NULL,
|
||||
0,
|
||||
NI_NUMERICHOST
|
||||
);
|
||||
freeifaddrs(ifaddr);
|
||||
return ipAddress;
|
||||
}
|
||||
}
|
||||
|
||||
freeifaddrs(ifaddr);
|
||||
return NULL;
|
||||
}
|
26
Pi-Helper/resolver.h
Normal file
26
Pi-Helper/resolver.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// resolver.h
|
||||
// Pi-Helper
|
||||
//
|
||||
// Created by Billy Brawner on 11/16/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef resolver_h
|
||||
#define resolver_h
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <resolv.h>
|
||||
#include <limits.h>
|
||||
#include <netdb.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <string.h>
|
||||
|
||||
const int MAX_SERVERS = 10;
|
||||
// String length for max IP address length (255.255.255.255)
|
||||
const int MAX_IP_ADDR_LEN = 15;
|
||||
|
||||
char * resolver_get_dns_server_ip(void);
|
||||
char * resolver_get_device_ip(void);
|
||||
|
||||
#endif /* resolver_h */
|
Loading…
Reference in a new issue