Migrate networking code to Kotlin Multiplatform compatible module
This commit is contained in:
parent
65838d6905
commit
09f261b034
31 changed files with 351 additions and 497 deletions
|
@ -12,7 +12,7 @@
|
|||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/piholeclient" />
|
||||
<option value="$PROJECT_DIR$/shared" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
|
|
|
@ -4,5 +4,17 @@
|
|||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
|
@ -24,6 +24,10 @@
|
|||
<entry key="../../../../layout/compose-model-1623358815178.xml" value="1.6830769230769231" />
|
||||
<entry key="../../../../layout/compose-model-1623359670724.xml" value="0.11413043478260869" />
|
||||
<entry key="../../../../layout/compose-model-1623360495184.xml" value="0.20880752102919348" />
|
||||
<entry key="../../../../layout/compose-model-1646330539642.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1646331413004.xml" value="0.4506079027355623" />
|
||||
<entry key="../../../../layout/compose-model-1646332602415.xml" value="0.13775083612040134" />
|
||||
<entry key="../../../../layout/compose-model-1646358533232.xml" value="0.30153061224489797" />
|
||||
<entry key="app/src/main/res/drawable-v26/ic_shortcut_enable.xml" value="0.27447916666666666" />
|
||||
<entry key="app/src/main/res/drawable-v26/ic_shortcut_pause.xml" value="0.27447916666666666" />
|
||||
<entry key="app/src/main/res/drawable/background_splash.xml" value="0.409375" />
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
6
gradlew
vendored
6
gradlew
vendored
|
@ -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
|
||||
|
|
1
piholeclient/.gitignore
vendored
1
piholeclient/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -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)
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@com.squareup.moshi.* <methods>;
|
||||
}
|
||||
|
||||
-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 {
|
||||
<fields>;
|
||||
**[] 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 <init>(...);
|
||||
}
|
||||
|
||||
# Retain generated JsonAdapters if annotated type is retained.
|
||||
-if @com.squareup.moshi.JsonClass class *
|
||||
-keep class <1>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*
|
||||
-keep class <1>_<2>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*
|
||||
-keep class <1>_<2>_<3>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
|
||||
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
|
||||
|
||||
-keepclassmembers class kotlin.Metadata {
|
||||
public <methods>;
|
||||
}
|
23
piholeclient/proguard-rules.pro
vendored
23
piholeclient/proguard-rules.pro
vendored
|
@ -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 *
|
|
@ -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<String>
|
||||
//
|
||||
// @GET("/admin/scripts/pi-hole/php/api_token.php")
|
||||
// suspend fun apiKey(phpSession: String): Response<String>
|
||||
}
|
||||
|
||||
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 <T : Any> sendRequest(request: Request, responseType: KClass<T>?): 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, List<Cookie>>()
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> = cookies[url.host]
|
||||
?: emptyList()
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
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)
|
||||
}
|
|
@ -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<String, String>,
|
||||
@Json(name = "top_ads") val topAds: Map<String, Double>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusResponse(
|
||||
override val status: Status
|
||||
) : StatusProvider
|
||||
|
||||
interface StatusProvider {
|
||||
val status: Status
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
<resources>
|
||||
<string name="app_name">PiHoleClient</string>
|
||||
</resources>
|
|
@ -8,4 +8,4 @@ dependencyResolutionManagement {
|
|||
}
|
||||
rootProject.name = "Pi-helper"
|
||||
include(":app")
|
||||
include(":piholeclient")
|
||||
include(":shared")
|
||||
|
|
1
shared/.gitignore
vendored
Normal file
1
shared/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
63
shared/build.gradle.kts
Normal file
63
shared/build.gradle.kts
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.wbrawner.piholeclient">
|
||||
package="com.wbrawner.pihelper">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application android:usesCleartextTraffic="true" />
|
||||
</manifest>
|
||||
</manifest>
|
|
@ -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()
|
||||
})
|
|
@ -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 <T: HttpClientEngineConfig> HttpClientConfig<T>.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()
|
||||
}
|
|
@ -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<String, String>,
|
||||
@SerialName("top_ads") val topAds: Map<String, Double>
|
||||
)
|
||||
|
||||
@Serializable()
|
||||
data class StatusResponse(
|
||||
override val status: Status
|
||||
) : StatusProvider
|
||||
|
||||
interface StatusProvider {
|
||||
val status: Status
|
||||
}
|
|
@ -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()
|
||||
})
|
Loading…
Reference in a new issue