diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index e34606c..147f384 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -26,5 +26,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt b/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
index e5127d6..fbe5263 100644
--- a/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
@@ -26,10 +26,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import com.wbrawner.pihelper.shared.Action
-import com.wbrawner.pihelper.shared.AuthenticationString
-import com.wbrawner.pihelper.shared.Effect
-import com.wbrawner.pihelper.shared.Store
+import com.wbrawner.pihelper.shared.*
const val AUTH_SCREEN_TAG = "authScreen"
const val SUCCESS_TEXT_TAG = "successText"
diff --git a/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt b/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
index 20e11a5..9d513e3 100644
--- a/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
@@ -22,8 +22,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.wbrawner.pihelper.shared.Action
-import com.wbrawner.pihelper.shared.Store
+import com.wbrawner.pihelper.shared.*
import com.wbrawner.pihelper.ui.PihelperTheme
@Composable
@@ -107,6 +106,11 @@ fun InfoScreen(onBackClicked: () -> Unit, onForgetPiholeClicked: () -> Unit) {
) {
message.getStringAnnotations(it, it).firstOrNull()?.let { annotation ->
uriHandler.openUri(annotation.item)
+ // TODO: Move this to the store?
+ PlausibleAnalyticsHelper.event(
+ AnalyticsEvent.LinkClicked(annotation.item),
+ Route.ABOUT
+ )
}
}
TextButton(onClick = onForgetPiholeClicked) {
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
index 0190059..bd619a0 100644
--- a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
@@ -65,7 +65,6 @@ class MainActivity : AppCompatActivity() {
val state by store.state.collectAsState()
val navController = rememberNavController()
LaunchedEffect(state.route) {
- println("navigating to ${state.route.name}")
navController.navigate(state.route.name)
}
val effect by store.effects.collectAsState(initial = Effect.Empty)
diff --git a/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt b/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt
index 43bc100..fb2b03b 100644
--- a/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt
@@ -1,8 +1,6 @@
package com.wbrawner.pihelper
-import com.wbrawner.pihelper.shared.PiholeAPIService
-import com.wbrawner.pihelper.shared.Store
-import com.wbrawner.pihelper.shared.create
+import com.wbrawner.pihelper.shared.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -16,9 +14,14 @@ object PiHelperModule {
@Singleton
fun providesPiholeAPIService(): PiholeAPIService = PiholeAPIService.create()
+ @Provides
+ @Singleton
+ fun providesAnalyticsHelper(): AnalyticsHelper = PlausibleAnalyticsHelper
+
@Provides
@Singleton
fun providesStore(
apiService: PiholeAPIService,
- ): Store = Store(apiService)
+ analyticsHelper: AnalyticsHelper
+ ): Store = Store(apiService, analyticsHelper)
}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bede7b7..2f94246 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7,4 +7,6 @@
Disable for 30 seconds
Disable for 5 minutes
Disable for 5 minutes
+ pihelper.android.wbrawner.com
+ https://plausible.wbrawner.com
diff --git a/build.gradle.kts b/build.gradle.kts
index d855d84..323a0bf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,3 +1,5 @@
+import java.net.URI
+
buildscript {
repositories {
gradlePluginPortal()
@@ -13,5 +15,8 @@ allprojects {
repositories {
google()
mavenCentral()
+ maven {
+ url = URI("https://s01.oss.sonatype.org/content/repositories/snapshots/")
+ }
}
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1cb9f3a..2eda677 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,6 +19,7 @@ maxSdk = "33"
minSdk = "23"
navigation = "2.4.1"
okhttp = "4.10.0"
+plausible = "0.1.0-SNAPSHOT"
settings = "0.8.1"
versionCode = "1"
versionName = "1.0"
@@ -66,6 +67,7 @@ multiplatform-settings = { module = "com.russhwolf:multiplatform-settings-no-arg
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" }
+plausible = { module = "com.wbrawner.plausible:plausible-android", version.ref = "plausible" }
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
test-ext = { module = "androidx.test.ext:junit", version = "1.1.4" }
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index 8f61ed6..311bbfa 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -29,6 +29,7 @@ kotlin {
val androidMain by getting {
dependencies {
implementation(libs.ktor.client.android)
+ implementation(libs.plausible)
}
}
diff --git a/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/PlausibleAnalyticsHelper.kt b/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/PlausibleAnalyticsHelper.kt
new file mode 100644
index 0000000..0a6dab0
--- /dev/null
+++ b/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/PlausibleAnalyticsHelper.kt
@@ -0,0 +1,18 @@
+package com.wbrawner.pihelper.shared
+
+import com.wbrawner.plausible.android.Plausible
+
+object PlausibleAnalyticsHelper : AnalyticsHelper {
+ override fun pageView(route: Route) {
+ Plausible.pageView(route.path)
+ }
+
+ override fun event(event: AnalyticsEvent, route: Route) {
+ val props = when (event) {
+ is AnalyticsEvent.DisableButtonClicked -> mapOf("duration" to event.duration)
+ is AnalyticsEvent.LinkClicked -> mapOf("link" to event.link)
+ else -> null
+ }
+ Plausible.event(event.name, route.path, props = props)
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/AnalyticsHelper.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/AnalyticsHelper.kt
new file mode 100644
index 0000000..53670f1
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/AnalyticsHelper.kt
@@ -0,0 +1,23 @@
+package com.wbrawner.pihelper.shared
+
+interface AnalyticsHelper {
+ fun pageView(route: Route)
+ fun event(event: AnalyticsEvent, route: Route)
+
+ companion object
+}
+
+sealed class AnalyticsEvent(val name: String) {
+ object ScanButtonClick : AnalyticsEvent("Scan button clicked")
+ object ConnectButtonClick : AnalyticsEvent("Connect button clicked")
+ object AuthenticateWithPasswordButtonClicked
+ : AnalyticsEvent("Authenticate with password button clicked")
+
+ object AuthenticateWithApiKeyButtonClicked
+ : AnalyticsEvent("Authenticate with API key button clicked")
+
+ object EnableButtonClicked : AnalyticsEvent("Enable button clicked")
+ data class DisableButtonClicked(val duration: Long?) : AnalyticsEvent("Disable button clicked")
+ object ForgetButtonClicked : AnalyticsEvent("Forget button clicked")
+ data class LinkClicked(val link: String) : AnalyticsEvent("Link clicked")
+}
\ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Store.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Store.kt
index 8c55434..f01e1c8 100644
--- a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Store.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Store.kt
@@ -63,6 +63,7 @@ private const val ONE_SECOND = 1_000
class Store(
private val apiService: PiholeAPIService,
+ private val analyticsHelper: AnalyticsHelper? = null,
private val settings: Settings = Settings(),
initialState: State = State()
) : CoroutineScope by CoroutineScope(Dispatchers.Main) {
@@ -74,14 +75,12 @@ class Store(
private var scanJob: Job? = null
init {
- launch {
- _state.collect {
- println(it)
- }
- }
+ var previousRoute: Route? = null
val host: String? = initialState.host ?: settings[KEY_HOST]
val apiKey: String? = initialState.apiKey ?: settings[KEY_API_KEY]
if (!host.isNullOrBlank() && !apiKey.isNullOrBlank()) {
+ // This avoids reporting a page view for the connect page when it isn't actually viewed
+ previousRoute = Route.CONNECT
apiService.baseUrl = host
apiService.apiKey = apiKey
_state.value = initialState.copy(
@@ -96,6 +95,16 @@ class Store(
connect("pi.hole", false)
}
}
+ launch {
+ delay(1000)
+ _state.collect {
+ println(it)
+ if (it.route != previousRoute) {
+ previousRoute = it.route
+ analyticsHelper?.pageView(it.route)
+ }
+ }
+ }
}
fun dispatch(action: Action) {
@@ -104,17 +113,47 @@ class Store(
is Action.Authenticate -> {
when (action.authString) {
// The Pi-hole API key is just the web password hashed twice with SHA-256
- is AuthenticationString.Password -> authenticate(
- action.authString.value.hash().hash()
- )
- is AuthenticationString.Token -> authenticate(action.authString.value)
+ is AuthenticationString.Password -> {
+ authenticate(
+ action.authString.value.hash().hash()
+ )
+ analyticsHelper?.event(
+ AnalyticsEvent.AuthenticateWithPasswordButtonClicked,
+ _state.value.route
+ )
+ }
+ is AuthenticationString.Token -> {
+ authenticate(action.authString.value)
+ analyticsHelper?.event(
+ AnalyticsEvent.AuthenticateWithApiKeyButtonClicked,
+ _state.value.route
+ )
+ }
}
}
- is Action.Connect -> connect(action.host)
- is Action.Disable -> disable(action.duration)
- Action.Enable -> enable()
- Action.Forget -> forget()
- is Action.Scan -> scan(action.deviceIp)
+ is Action.Connect -> {
+ connect(action.host)
+ analyticsHelper?.event(AnalyticsEvent.ConnectButtonClick, _state.value.route)
+ }
+ is Action.Disable -> {
+ disable(action.duration)
+ analyticsHelper?.event(
+ AnalyticsEvent.DisableButtonClicked(action.duration),
+ _state.value.route
+ )
+ }
+ Action.Enable -> {
+ enable()
+ analyticsHelper?.event(AnalyticsEvent.EnableButtonClicked, _state.value.route)
+ }
+ Action.Forget -> {
+ forget()
+ analyticsHelper?.event(AnalyticsEvent.ForgetButtonClicked, _state.value.route)
+ }
+ is Action.Scan -> {
+ scan(action.deviceIp)
+ analyticsHelper?.event(AnalyticsEvent.ScanButtonClick, _state.value.route)
+ }
Action.About -> _state.value = _state.value.copy(route = Route.ABOUT)
Action.Back -> back()
}