feat: implement Phase 1 MVP of Implausibly
Working Android dashboard app for self-hosted Plausible Analytics CE. Connects to Plausible API v2 (POST /api/v2/query), displays top stats, visitor chart, top sources, and top pages with date range selection. Architecture: Kotlin + Jetpack Compose + Material 3 + Hilt + Ktor + SQLDelight + EncryptedSharedPreferences. Single :app module, four-layer unidirectional data flow (UI → ViewModel → Repository → Data). Includes: instance management, site list, caching with TTL per date range, encrypted API key storage, custom Canvas visitor chart, pull-to-refresh, and unit tests for API, cache, repository, and domain model layers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
aa66172d58
69 changed files with 4778 additions and 0 deletions
62
app/src/main/java/no/naiv/implausibly/data/AppPreferences.kt
Normal file
62
app/src/main/java/no/naiv/implausibly/data/AppPreferences.kt
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package no.naiv.implausibly.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = "implausibly_preferences"
|
||||
)
|
||||
|
||||
/**
|
||||
* Non-sensitive preferences stored via DataStore.
|
||||
* Includes the user's selected instance, site, and date range.
|
||||
*/
|
||||
@Singleton
|
||||
class AppPreferences @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
private object Keys {
|
||||
val SELECTED_INSTANCE_ID = stringPreferencesKey("selected_instance_id")
|
||||
val SELECTED_SITE_ID = stringPreferencesKey("selected_site_id")
|
||||
val SELECTED_DATE_RANGE = stringPreferencesKey("selected_date_range")
|
||||
}
|
||||
|
||||
val selectedInstanceId: Flow<String?> = context.dataStore.data
|
||||
.map { it[Keys.SELECTED_INSTANCE_ID] }
|
||||
|
||||
val selectedSiteId: Flow<String?> = context.dataStore.data
|
||||
.map { it[Keys.SELECTED_SITE_ID] }
|
||||
|
||||
val selectedDateRange: Flow<String?> = context.dataStore.data
|
||||
.map { it[Keys.SELECTED_DATE_RANGE] }
|
||||
|
||||
suspend fun setSelectedInstanceId(id: String?) {
|
||||
context.dataStore.edit { prefs ->
|
||||
if (id != null) prefs[Keys.SELECTED_INSTANCE_ID] = id
|
||||
else prefs.remove(Keys.SELECTED_INSTANCE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setSelectedSiteId(id: String?) {
|
||||
context.dataStore.edit { prefs ->
|
||||
if (id != null) prefs[Keys.SELECTED_SITE_ID] = id
|
||||
else prefs.remove(Keys.SELECTED_SITE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setSelectedDateRange(range: String) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[Keys.SELECTED_DATE_RANGE] = range
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue