diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 8989db5..37e6888 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 60eed15..2842237 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -4,5 +4,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 89ababe..a7c7cb9 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -24,6 +24,10 @@
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1a060f9..2f6b1ee 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -61,10 +61,13 @@ android {
buildFeatures {
compose = true
}
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.compose.get()
+ }
}
dependencies {
- implementation(project(":piholeclient"))
+ implementation(project(":shared"))
implementation(libs.bundles.coroutines)
implementation(libs.bundles.compose)
implementation(libs.hilt.android.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a0fe58a..89e5426 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,11 +11,13 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
+ android:usesCleartextTraffic="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
diff --git a/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt b/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt
index e829370..51f377f 100644
--- a/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt
@@ -4,8 +4,8 @@ import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import androidx.lifecycle.ViewModel
-import com.wbrawner.piholeclient.PiHoleApiService
-import com.wbrawner.piholeclient.VersionResponse
+import com.wbrawner.pihelper.shared.PiholeAPIService
+import com.wbrawner.pihelper.shared.VersionResponse
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -22,7 +22,7 @@ const val IP_MAX = 255
@HiltViewModel
class AddPiHelperViewModel @Inject constructor(
private val sharedPreferences: SharedPreferences,
- private val apiService: PiHoleApiService
+ private val apiService: PiholeAPIService
) : ViewModel() {
@Volatile
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt b/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
index bb85343..07a0fd7 100644
--- a/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
@@ -19,8 +19,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
+import com.wbrawner.pihelper.shared.Status
import com.wbrawner.pihelper.ui.PihelperTheme
-import com.wbrawner.piholeclient.Status
import java.util.*
import kotlin.math.pow
import kotlin.math.roundToLong
@@ -168,7 +168,7 @@ fun CustomTimeDialog(
onTimeSelected: (Long) -> Unit
) {
if (!visible) return
- val (time: Long, setTime: (Long) -> Unit) = remember { mutableStateOf(0L) }
+ val (time: String, setTime: (String) -> Unit) = remember { mutableStateOf("10") }
val (duration, selectDuration: (Duration) -> Unit) = remember {
mutableStateOf(Duration.SECONDS)
}
diff --git a/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt b/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt
index 6867083..e3b39d7 100644
--- a/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt
@@ -3,7 +3,8 @@ package com.wbrawner.pihelper
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
-import com.wbrawner.piholeclient.NAME_BASE_URL
+import com.wbrawer.pihelper.shared.create
+import com.wbrawner.pihelper.shared.PiholeAPIService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -13,6 +14,7 @@ import javax.inject.Named
import javax.inject.Singleton
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs"
+const val NAME_BASE_URL = "baseUrl"
@Module
@InstallIn(SingletonComponent::class)
@@ -33,4 +35,8 @@ object PiHelperModule {
@Named(NAME_BASE_URL)
fun providesBaseUrl(sharedPreferences: SharedPreferences) = sharedPreferences
.getString(KEY_BASE_URL, "")
+
+ @Provides
+ @Singleton
+ fun providesPiholeAPIService(): PiholeAPIService = PiholeAPIService.create()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt b/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt
index af61f11..a4a949b 100644
--- a/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt
@@ -1,9 +1,10 @@
package com.wbrawner.pihelper
+import android.util.Log
import androidx.lifecycle.ViewModel
-import com.wbrawner.piholeclient.PiHoleApiService
-import com.wbrawner.piholeclient.Status
-import com.wbrawner.piholeclient.StatusProvider
+import com.wbrawner.pihelper.shared.PiholeAPIService
+import com.wbrawner.pihelper.shared.Status
+import com.wbrawner.pihelper.shared.StatusProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -14,7 +15,7 @@ import kotlin.coroutines.coroutineContext
@HiltViewModel
class PiHelperViewModel @Inject constructor(
- private val apiService: PiHoleApiService
+ private val apiService: PiholeAPIService
) : ViewModel() {
private val _status = MutableStateFlow(Status.LOADING)
val status = _status.asStateFlow()
@@ -30,7 +31,7 @@ class PiHelperViewModel @Inject constructor(
_status.value = action!!.invoke().status
action = null
} catch (ignored: Exception) {
- break
+ _status.value = Status.UNKNOWN
}
delay(1000)
}
diff --git a/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt b/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
index 0876ab0..b2e14ba 100644
--- a/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
@@ -55,10 +55,6 @@ fun ScanScreen(navController: NavController, addPiHelperViewModel: AddPiHelperVi
?.linkAddresses
?.filter { !it.address.isLoopbackAddress && it.address is Inet4Address }
?.forEach { address ->
- Log.d(
- "Pi-helper",
- "Found link address: ${address.address.hostName}"
- )
addPiHelperViewModel.beginScanning(
address.address.hostAddress,
onSuccess,
diff --git a/build.gradle.kts b/build.gradle.kts
index c93f256..ce709d8 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,13 +1,10 @@
buildscript {
repositories {
+ gradlePluginPortal()
google()
mavenCentral()
}
- val hiltVersion by extra("2.36")
- val kotlinVersion by extra("1.4.32")
dependencies {
- classpath("com.android.tools.build:gradle:7.0.0-beta03")
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
- classpath("com.google.dagger:hilt-android-gradle-plugin:$hiltVersion")
+ classpath(libs.bundles.plugins)
}
}
diff --git a/gradle.properties b/gradle.properties
index 23339e0..40f6cb9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -19,3 +19,6 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
+
+kotlin.mpp.enableGranularSourceSetsMetadata=true
+kotlin.native.enableDependencyPropagation=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4ad83e9..5a2e10f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,50 +1,66 @@
[versions]
androidx-core = "1.3.2"
androidx-appcompat = "1.2.0"
-compose = "1.0.0-beta07"
+compose = "1.1.1"
coroutines = "1.4.3"
espresso = "3.3.0"
-hilt-android = "2.36"
-kotlin = "1.4.32"
+hilt-android = "2.38.1"
+kotlin = "1.6.10"
+kotlinx-serialization = "1.3.2"
+kotlinx-coroutines = "1.6.0-native-mt"
+ktor = "2.0.0-beta-1"
lifecycle = "2.2.0"
material = "1.3.0"
-maxSdk = "30"
+maxSdk = "31"
moshi = "1.9.2"
minSdk = "23"
-navigation = "2.3.2"
+navigation = "2.4.1"
okhttp = "4.2.2"
versionCode = "1"
versionName = "1.0"
[libraries]
-androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
+android-gradle = { module = "com.android.tools.build:gradle", version = "7.1.2"}
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
+androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
+compose-activity = { module = "androidx.activity:activity-compose", version = "1.4.0" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
-compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
-compose-activity = { module = "androidx.activity:activity-compose", version = "1.3.0-alpha07" }
compose-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
+compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
+compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
+coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
+coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
+dagger-hilt = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt-android" }
+espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
hilt-android-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" }
-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0-alpha02" }
-navigation-compose = { module = "androidx.navigation:navigation-compose", version = "2.4.0-alpha01" }
-material = { module = "com.google.android.material:material", version.ref = "material" }
-moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
-moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
-navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
-preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
+hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0" }
junit = { module = "junit:junit", version = "4.12" }
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
+kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
+ktor-client-ios = { module = "io.ktor:ktor-client-ios", version.ref = "ktor" }
+ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
+ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
+ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
+ktor-client-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
+ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
+ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
+material = { module = "com.google.android.material:material", version.ref = "material" }
+moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
+moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
+navigation-compose = { module = "androidx.navigation:navigation-compose", version = "navigation" }
+navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
+navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
+preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
test-ext = { module = "androidx.test.ext:junit", version = "1.1.2" }
-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
[bundles]
compose = ["compose-ui", "compose-material", "compose-tooling", "compose-activity", "navigation-compose"]
coroutines = ["coroutines-core", "coroutines-android"]
+plugins = ["android-gradle", "kotlin-gradle", "dagger-hilt"]
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e3c1305..d7773de 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
diff --git a/gradlew b/gradlew
index cccdd3d..5944d0e 100755
--- a/gradlew
+++ b/gradlew
@@ -82,7 +82,7 @@ location of your Java installation."
fi
else
JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ which kotlin >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
@@ -109,7 +109,7 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
-# For Cygwin, switch paths to Windows format before running java
+# For Cygwin, switch paths to Windows format before running kotlin
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
@@ -161,7 +161,7 @@ save () {
}
APP_ARGS=$(save "$@")
-# Collect all arguments for the java command, following the shell quoting and substitution rules
+# Collect all arguments for the kotlin command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
diff --git a/piholeclient/.gitignore b/piholeclient/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/piholeclient/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/piholeclient/build.gradle.kts b/piholeclient/build.gradle.kts
deleted file mode 100644
index b581c06..0000000
--- a/piholeclient/build.gradle.kts
+++ /dev/null
@@ -1,45 +0,0 @@
-plugins {
- id("com.android.library")
- id("kotlin-android")
- id("kotlin-kapt")
- id("dagger.hilt.android.plugin")
-}
-
-android {
- compileSdk = libs.versions.maxSdk.get().toInt()
- defaultConfig {
- minSdk = libs.versions.minSdk.get().toInt()
- targetSdk = libs.versions.maxSdk.get().toInt()
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
-}
-
-dependencies {
- api("com.squareup.okhttp3:okhttp:${libs.versions.okhttp.get()}")
- implementation("com.squareup.okhttp3:logging-interceptor:${libs.versions.okhttp.get()}")
- implementation(libs.androidx.core)
- implementation(libs.androidx.appcompat)
- implementation(libs.bundles.coroutines)
- implementation(libs.hilt.android.core)
- kapt(libs.hilt.android.kapt)
- implementation(libs.kotlin.reflect)
- api(libs.moshi.core)
- kapt(libs.moshi.codegen)
- testImplementation(libs.junit)
-}
diff --git a/piholeclient/consumer-rules.pro b/piholeclient/consumer-rules.pro
deleted file mode 100644
index b10b8cf..0000000
--- a/piholeclient/consumer-rules.pro
+++ /dev/null
@@ -1,67 +0,0 @@
-# JSR 305 annotations are for embedding nullability information.
--dontwarn javax.annotation.**
-
--keepclasseswithmembers class * {
- @com.squareup.moshi.* ;
-}
-
--keep @com.squareup.moshi.JsonQualifier interface *
-
-# Enum field names are used by the integrated EnumJsonAdapter.
-# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
-# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
--keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
- ;
- **[] values();
-}
-
-# The name of @JsonClass types is used to look up the generated adapter.
--keepnames @com.squareup.moshi.JsonClass class *
-
-# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
-# name. We will look this up reflectively to invoke the type's constructor.
-#
-# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
-# matching preceding parameters.
--keepnames class kotlin.jvm.internal.DefaultConstructorMarker
--keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
- synthetic (...);
-}
-
-# Retain generated JsonAdapters if annotated type is retained.
--if @com.squareup.moshi.JsonClass class *
--keep class <1>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*
--keep class <1>_<2>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*$*
--keep class <1>_<2>_<3>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*$*$*
--keep class <1>_<2>_<3>_<4>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*$*$*$*
--keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
--keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
- (...);
- ;
-}
-
--keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
-
--keepclassmembers class kotlin.Metadata {
- public ;
-}
diff --git a/piholeclient/proguard-rules.pro b/piholeclient/proguard-rules.pro
deleted file mode 100644
index 767e1cd..0000000
--- a/piholeclient/proguard-rules.pro
+++ /dev/null
@@ -1,23 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.kts.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
-
--keepclassmembers @com.squareup.moshi.JsonClass class *
diff --git a/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleApiService.kt b/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleApiService.kt
deleted file mode 100644
index c06f575..0000000
--- a/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleApiService.kt
+++ /dev/null
@@ -1,146 +0,0 @@
-package com.wbrawner.piholeclient
-
-import com.squareup.moshi.Moshi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import okhttp3.HttpUrl
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import kotlin.reflect.KClass
-
-interface PiHoleApiService {
- var baseUrl: String?
- var apiKey: String?
-
- suspend fun getSummary(
- version: Boolean = false,
- type: Boolean = false
- ): Summary
-
- suspend fun getVersion(): VersionResponse
-
- suspend fun getTopItems(): TopItemsResponse
-
- suspend fun enable(): StatusResponse
-
- suspend fun disable(duration: Long? = null): StatusResponse
-
- /**
- @Query("overTimeData10mins") overTimeData10mins: Boolean = true,
- @Query("topItems") topItems: Int? = null,
- @Query("topClients") topClients: Int? = null,
- @Query("getForwardDestinations") getForwardDestinations: Boolean = true,
- @Query("getQueryTypes") getQueryTypes: Boolean = true,
- @Query("getAllQueries") getAllQueries: Boolean = true
-
- */
-
-// suspend fun login(password: String): Response
-//
-// @GET("/admin/scripts/pi-hole/php/api_token.php")
-// suspend fun apiKey(phpSession: String): Response
-}
-
-const val BASE_PATH = "/admin/api.php"
-
-class OkHttpPiHoleApiService(
- private val okHttpClient: OkHttpClient,
- private val moshi: Moshi
-) : PiHoleApiService {
- override var baseUrl: String? = null
- @Synchronized
- get
- @Synchronized
- set
- override var apiKey: String? = null
- @Synchronized
- get
- @Synchronized
- set
-
- private val urlBuilder: HttpUrl.Builder
- get() {
- val host = baseUrl ?: throw IllegalStateException("No base URL defined")
- return HttpUrl.Builder()
- .scheme("http")
- .host(host)
- .addPathSegments(BASE_PATH)
- }
-
- override suspend fun getSummary(version: Boolean, type: Boolean): Summary {
- val url = urlBuilder
- .addQueryParameter("summary", "")
- if (version) {
- url.addQueryParameter("version", "")
- }
- if (type) {
- url.addQueryParameter("type", "")
- }
- val request = Request.Builder()
- .get()
- .url(url.build())
- return sendRequest(request.build(), Summary::class)!!
- }
-
- override suspend fun getVersion(): VersionResponse {
- val url = urlBuilder
- .addQueryParameter("version", "")
- val request = Request.Builder()
- .get()
- .url(url.build())
- return sendRequest(request.build(), VersionResponse::class)!!
- }
-
- override suspend fun getTopItems(): TopItemsResponse {
- val apiToken = this.apiKey ?: throw java.lang.IllegalStateException("No API Token provided")
- val url = urlBuilder
- .addQueryParameter("topItems", "25")
- .addQueryParameter("auth", apiToken)
- val request = Request.Builder()
- .get()
- .url(url.build())
- return sendRequest(request.build(), TopItemsResponse::class)!!
- }
-
- override suspend fun enable(): StatusResponse {
- val apiToken = this.apiKey ?: throw java.lang.IllegalStateException("No API Token provided")
- val url = urlBuilder
- .addQueryParameter("enable", "")
- .addQueryParameter("auth", apiToken)
- val request = Request.Builder()
- .get()
- .url(url.build())
- return sendRequest(request.build(), StatusResponse::class)!!
- }
-
- override suspend fun disable(duration: Long?): StatusResponse {
- val apiToken = this.apiKey ?: throw java.lang.IllegalStateException("No API Token provided")
- val url = urlBuilder
- .addQueryParameter("disable", duration?.toString()?: "")
- .addQueryParameter("auth", apiToken)
- val request = Request.Builder()
- .get()
- .url(url.build())
- return sendRequest(request.build(), StatusResponse::class)!!
- }
-
- private suspend fun sendRequest(request: Request, responseType: KClass?): T? {
- return withContext(Dispatchers.IO) {
- val response = okHttpClient.newCall(request).execute()
- if (!response.isSuccessful) {
- null
- } else {
- @Suppress("UNCHECKED_CAST")
- when (responseType) {
- null -> null
- String::class -> response.body?.string() as T
- else -> response.body?.let {
- moshi
- .adapter(responseType.javaObjectType)
- .fromJson(it.source())
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleClientModule.kt b/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleClientModule.kt
deleted file mode 100644
index a9a9e9a..0000000
--- a/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleClientModule.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.wbrawner.piholeclient
-
-import com.squareup.moshi.JsonReader
-import com.squareup.moshi.Moshi
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import okhttp3.Cookie
-import okhttp3.CookieJar
-import okhttp3.HttpUrl
-import okhttp3.OkHttpClient
-import okhttp3.logging.HttpLoggingInterceptor
-import okio.Buffer
-import java.util.concurrent.TimeUnit
-import javax.inject.Singleton
-
-const val NAME_BASE_URL = "baseUrl"
-
-@Module
-@InstallIn(SingletonComponent::class)
-object PiHoleClientModule {
- @Provides
- @Singleton
- fun providesMoshi(): Moshi = Moshi.Builder().build()
-
- @Provides
- @Singleton
- fun providesOkHttpClient(): OkHttpClient {
- val client = OkHttpClient.Builder()
- .connectTimeout(500, TimeUnit.MILLISECONDS)
- .cookieJar(object : CookieJar {
- val cookies = mutableMapOf>()
- override fun loadForRequest(url: HttpUrl): List = cookies[url.host]
- ?: emptyList()
-
- override fun saveFromResponse(url: HttpUrl, cookies: List) {
- this.cookies[url.host] = cookies
- }
- })
- if (BuildConfig.DEBUG) {
- client.addInterceptor(HttpLoggingInterceptor(
- object : HttpLoggingInterceptor.Logger {
- val moshi = Moshi.Builder()
- .build()
- .adapter(Any::class.java)
- .indent(" ")
-
- override fun log(message: String) {
- val prettyMessage = try {
- val json = JsonReader.of(Buffer().writeUtf8(message))
- moshi.toJson(json.readJsonValue())
- } catch (ignored: Exception) {
- message
- }
- HttpLoggingInterceptor.Logger.DEFAULT.log(prettyMessage)
- }
- })
- .apply {
- level = HttpLoggingInterceptor.Level.BODY
- }
- )
- }
- return client.build()
- }
-
- @Provides
- @Singleton
- fun providesPiHoleApiService(okHttpClient: OkHttpClient, moshi: Moshi): PiHoleApiService =
- OkHttpPiHoleApiService(okHttpClient, moshi)
-}
\ No newline at end of file
diff --git a/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt b/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt
deleted file mode 100644
index c6bb17c..0000000
--- a/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.wbrawner.piholeclient
-
-import androidx.annotation.Keep
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class Summary(
- @Json(name = "domains_being_blocked")
- val domainsBeingBlocked: String,
- @Json(name = "dns_queries_today")
- val dnsQueriesToday: String,
- @Json(name = "ads_blocked_today")
- val adsBlockedToday: String,
- @Json(name = "ads_percentage_today")
- val adsPercentageToday: String,
- @Json(name = "unique_domains")
- val uniqueDomains: String,
- @Json(name = "queries_forwarded")
- val queriesForwarded: String,
- @Json(name = "clients_ever_seen")
- val clientsEverSeen: String,
- @Json(name = "unique_clients")
- val uniqueClients: String,
- @Json(name = "dns_queries_all_types")
- val dnsQueriesAllTypes: String,
- @Json(name = "queries_cached")
- val queriesCached: String,
- @Json(name = "no_data_replies")
- val noDataReplies: String?,
- @Json(name = "nx_domain_replies")
- val nxDomainReplies: String?,
- @Json(name = "cname_replies")
- val cnameReplies: String?,
- @Json(name = "in_replies")
- val ipReplies: String?,
- @Json(name = "privacy_level")
- val privacyLevel: String,
- override val status: Status,
- @Json(name = "gravity_last_updated")
- val gravity: Gravity?,
- val type: String?,
- val version: Int?
-) : StatusProvider
-
-@Keep
-enum class Status {
- @Json(name = "enabled")
- ENABLED,
- @Json(name = "disabled")
- DISABLED,
- @Transient
- LOADING,
- @Transient
- UNKNOWN
-}
-
-@JsonClass(generateAdapter = true)
-data class Gravity(
- @Json(name = "file_exists")
- val fileExists: Boolean,
- val absolute: Int,
- val relative: Relative
-)
-
-@JsonClass(generateAdapter = true)
-data class Relative(
- val days: String,
- val hours: String,
- val minutes: String
-)
-
-@JsonClass(generateAdapter = true)
-data class VersionResponse(val version: Int)
-
-@JsonClass(generateAdapter = true)
-data class TopItemsResponse(
- @Json(name = "top_queries") val topQueries: Map,
- @Json(name = "top_ads") val topAds: Map
-)
-
-@JsonClass(generateAdapter = true)
-data class StatusResponse(
- override val status: Status
-) : StatusProvider
-
-interface StatusProvider {
- val status: Status
-}
diff --git a/piholeclient/src/main/res/values/strings.xml b/piholeclient/src/main/res/values/strings.xml
deleted file mode 100644
index 9c995ef..0000000
--- a/piholeclient/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- PiHoleClient
-
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9f2e607..d29ac64 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -8,4 +8,4 @@ dependencyResolutionManagement {
}
rootProject.name = "Pi-helper"
include(":app")
-include(":piholeclient")
+include(":shared")
diff --git a/shared/.gitignore b/shared/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/shared/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
new file mode 100644
index 0000000..646afa2
--- /dev/null
+++ b/shared/build.gradle.kts
@@ -0,0 +1,63 @@
+plugins {
+ kotlin("multiplatform")
+ id("com.android.library")
+ kotlin("plugin.serialization") version "1.6.10"
+}
+
+kotlin {
+ android()
+ listOf(
+ iosX64(),
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach {
+ it.binaries.framework {
+ baseName = "Pihelper"
+ }
+ }
+
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.logging)
+ implementation(libs.ktor.client.serialization)
+ implementation(libs.ktor.client.content.negotiation)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.serialization.json)
+ }
+ }
+
+ val androidMain by getting {
+ dependencies {
+ implementation(libs.ktor.client.android)
+ }
+ }
+
+ val iosX64Main by getting
+ val iosArm64Main by getting
+ val iosSimulatorArm64Main by getting
+ val iosMain by creating {
+ dependsOn(commonMain)
+ iosX64Main.dependsOn(this)
+ iosArm64Main.dependsOn(this)
+ iosSimulatorArm64Main.dependsOn(this)
+ dependencies {
+ implementation(libs.ktor.client.ios)
+ }
+ }
+ }
+}
+
+android {
+ sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ compileSdk = libs.versions.maxSdk.get().toInt()
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ targetSdk = libs.versions.maxSdk.get().toInt()
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
diff --git a/piholeclient/src/main/AndroidManifest.xml b/shared/src/androidMain/AndroidManifest.xml
similarity index 55%
rename from piholeclient/src/main/AndroidManifest.xml
rename to shared/src/androidMain/AndroidManifest.xml
index 3dc5fd8..6528d73 100644
--- a/piholeclient/src/main/AndroidManifest.xml
+++ b/shared/src/androidMain/AndroidManifest.xml
@@ -1,5 +1,6 @@
+
+ package="com.wbrawner.pihelper">
+
-
-
+
\ No newline at end of file
diff --git a/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/HttpClient.kt b/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/HttpClient.kt
new file mode 100644
index 0000000..d47f19a
--- /dev/null
+++ b/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/HttpClient.kt
@@ -0,0 +1,11 @@
+package com.wbrawer.pihelper.shared
+
+import com.wbrawner.pihelper.shared.KtorPiholeAPIService
+import com.wbrawner.pihelper.shared.PiholeAPIService
+import com.wbrawner.pihelper.shared.commonConfig
+import io.ktor.client.*
+import io.ktor.client.engine.android.*
+
+fun PiholeAPIService.Companion.create() = KtorPiholeAPIService(HttpClient(Android) {
+ commonConfig()
+})
\ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/PiholeAPIService.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/PiholeAPIService.kt
new file mode 100644
index 0000000..5d6ef28
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/PiholeAPIService.kt
@@ -0,0 +1,82 @@
+package com.wbrawner.pihelper.shared
+
+import io.ktor.client.*
+import io.ktor.client.call.*
+import io.ktor.client.engine.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.http.*
+import io.ktor.serialization.kotlinx.json.*
+import kotlinx.serialization.json.Json
+
+const val BASE_PATH = "/admin/api.php"
+
+abstract class PiholeAPIService {
+ abstract var baseUrl: String?
+ abstract var apiKey: String?
+
+ abstract suspend fun getSummary(): Summary
+
+ abstract suspend fun getVersion(): VersionResponse
+ abstract suspend fun getTopItems(): TopItemsResponse
+ abstract suspend fun enable(): StatusResponse
+ abstract suspend fun disable(duration: Long? = null): StatusResponse
+
+ companion object
+}
+
+fun HttpClientConfig.commonConfig() {
+ install(ContentNegotiation) {
+ json(Json {
+ ignoreUnknownKeys = true
+ isLenient = true
+ })
+ }
+}
+
+class KtorPiholeAPIService(val httpClient: HttpClient) : PiholeAPIService() {
+ override var baseUrl: String? = null
+ override var apiKey: String? = null
+
+ override suspend fun getSummary(): Summary = httpClient.get {
+ url {
+ host = baseUrl ?: error("baseUrl not set")
+ encodedPath = BASE_PATH
+ }
+ }.body()
+
+ override suspend fun getVersion(): VersionResponse = httpClient.get {
+ url {
+ host = baseUrl ?: error("baseUrl not set")
+ encodedPath = BASE_PATH
+ parameter("version", "")
+ }
+ }.body()
+
+ override suspend fun getTopItems(): TopItemsResponse = httpClient.get {
+ url {
+ host = baseUrl ?: error("baseUrl not set")
+ encodedPath = BASE_PATH
+ parameter("topItems", "25")
+ parameter("auth", apiKey)
+ }
+ }.body()
+
+ override suspend fun enable(): StatusResponse = httpClient.get {
+ url {
+ host = baseUrl ?: error("baseUrl not set")
+ encodedPath = BASE_PATH
+ parameter("enable", "")
+ parameter("auth", apiKey)
+ }
+ }.body()
+
+ override suspend fun disable(duration: Long?): StatusResponse = httpClient.get {
+ url {
+ host = baseUrl ?: error("baseUrl not set")
+ encodedPath = BASE_PATH
+ parameter("disable", duration?.toString()?: "")
+ parameter("auth", apiKey)
+ }
+ }.body()
+}
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Responses.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Responses.kt
new file mode 100644
index 0000000..b558948
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Responses.kt
@@ -0,0 +1,90 @@
+package com.wbrawner.pihelper.shared
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable()
+data class Summary(
+ @SerialName("domains_being_blocked")
+ val domainsBeingBlocked: String? = null,
+ @SerialName("dns_queries_today")
+ val dnsQueriesToday: String? = null,
+ @SerialName("ads_blocked_today")
+ val adsBlockedToday: String? = null,
+ @SerialName("ads_percentage_today")
+ val adsPercentageToday: String? = null,
+ @SerialName("unique_domains")
+ val uniqueDomains: String? = null,
+ @SerialName("queries_forwarded")
+ val queriesForwarded: String? = null,
+ @SerialName("clients_ever_seen")
+ val clientsEverSeen: String? = null,
+ @SerialName("unique_clients")
+ val uniqueClients: String? = null,
+ @SerialName("dns_queries_all_types")
+ val dnsQueriesAllTypes: String? = null,
+ @SerialName("queries_cached")
+ val queriesCached: String? = null,
+ @SerialName("no_data_replies")
+ val noDataReplies: String? = null,
+ @SerialName("nx_domain_replies")
+ val nxDomainReplies: String? = null,
+ @SerialName("cname_replies")
+ val cnameReplies: String? = null,
+ @SerialName("in_replies")
+ val ipReplies: String? = null,
+ @SerialName("privacy_level")
+ val privacyLevel: String,
+ override val status: Status,
+ @SerialName("gravity_last_updated")
+ val gravity: Gravity? = null,
+ val type: String? = null,
+ val version: Int? = null
+) : StatusProvider
+
+@Serializable
+enum class Status {
+ @SerialName("enabled")
+ ENABLED,
+ @SerialName("disabled")
+ DISABLED,
+ @kotlinx.serialization.Transient
+ LOADING,
+ @kotlinx.serialization.Transient
+ UNKNOWN,
+ @kotlinx.serialization.Transient
+ ERROR
+}
+
+@Serializable()
+data class Gravity(
+ @SerialName("file_exists")
+ val fileExists: Boolean,
+ val absolute: Int,
+ val relative: Relative
+)
+
+@Serializable()
+data class Relative(
+ val days: String,
+ val hours: String,
+ val minutes: String
+)
+
+@Serializable()
+data class VersionResponse(val version: Int)
+
+@Serializable()
+data class TopItemsResponse(
+ @SerialName("top_queries") val topQueries: Map,
+ @SerialName("top_ads") val topAds: Map
+)
+
+@Serializable()
+data class StatusResponse(
+ override val status: Status
+) : StatusProvider
+
+interface StatusProvider {
+ val status: Status
+}
diff --git a/shared/src/iosMain/Kotlin/com/wbrawner/pihelper/shared/HttpClient.kt b/shared/src/iosMain/Kotlin/com/wbrawner/pihelper/shared/HttpClient.kt
new file mode 100644
index 0000000..6152545
--- /dev/null
+++ b/shared/src/iosMain/Kotlin/com/wbrawner/pihelper/shared/HttpClient.kt
@@ -0,0 +1,11 @@
+package com.wbrawer.pihelper.shared
+
+import com.wbrawner.pihelper.shared.KtorPiholeAPIService
+import com.wbrawner.pihelper.shared.PiholeAPIService
+import io.ktor.client.*
+import com.wbrawner.pihelper.shared.commonConfig
+import io.ktor.client.engine.darwin.*
+
+fun PiholeAPIService.Companion.create() = KtorPiholeAPIService(HttpClient(Darwin) {
+ commonConfig()
+})
\ No newline at end of file