mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-15 14:48:34 -04:00
feat(prefs): enable wasmJs via a platform-neutral PrefsStore abstraction
androidx.datastore.preferences publishes no wasmJs variant at any version, so core:prefs can no longer depend on DataStore<Preferences> from commonMain. Introduces PrefsStore/PrefsSnapshot/PrefsKey (core/prefs/store/PrefsStore.kt), a minimal interface mirroring DataStore<Preferences>'s own shape closely enough that porting each *PrefsImpl off DataStore is a mechanical key-factory rename, not a rewrite. Two implementations: nonWebMain's DataStorePrefsStore adapts a real DataStore<Preferences> (android/jvm/iOS unchanged underneath), and wasmJsMain's LocalStoragePrefsStore is backed by the browser's localStorage directly -- synchronous and built-in, so unlike core:database's OPFS story this needs no Worker or npm dependency. Unlike core:database's DatabaseManager split, none of the 17 *PrefsImpl classes needed to move out of commonMain: the platform difference is fully absorbed inside PrefsStore's two implementations, so every impl's diff is exactly the mechanical shape promised (defaults, migration logic, dynamic per-node keys all unchanged). PrefsDataStores.kt's 12 marker interfaces/wrappers get the same mechanical swap. Enabling core:prefs's wasmJs target exposed a second, one-module-upstream gap: core:repository had no wasmJs target at all (its `expect class Location` had android/jvm/iOS actuals but no wasmJs one). Fixed with a wasmJsMain placeholder mirroring the existing jvmMain placeholder exactly -- desktop has no real location hardware either, so this introduces no new product decision, just parity with the platform that already has none. core:service's MeshLogCleanupWorkerTest fake DataStore<Preferences> is updated to fake PrefsStore/PrefsSnapshot instead, following the interface change through its one external consumer. CorePrefsWasmJsModule (new Koin module, localStorage-backed) is not yet registered anywhere -- no webApp module exists yet to wire it into, same as core:database's SingleDatabaseProvider precedent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f92259f6d
commit
e3dc4b5ab0
35 files changed
+675
-226
No files matched your search
@@ -15,6 +15,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyTemplate
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.kmp.library)
|
||||
alias(libs.plugins.meshtastic.koin)
|
||||
@@ -23,16 +27,48 @@ plugins {
|
||||
kotlin {
|
||||
android { withHostTest {} }
|
||||
|
||||
// Library module: bare wasmJs(), no browser() (that's for the eventual webApp executable).
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs()
|
||||
|
||||
// nonWebMain: androidx.datastore.preferences has no wasmJs variant (the `Preferences` type itself doesn't
|
||||
// resolve there), so DataStorePrefsStore — the adapter wrapping a real DataStore<Preferences> — and the
|
||||
// Android-only CorePrefsAndroidModule live here/in androidMain instead of commonMain. All 17 `*PrefsImpl`
|
||||
// classes themselves stay in commonMain: they depend only on the platform-neutral PrefsStore/PrefsSnapshot/
|
||||
// PrefsKey abstraction (see core/prefs/store/PrefsStore.kt), not on Preferences directly, so unlike
|
||||
// core:database's DatabaseManager split, nothing about their own code needs to move. Predicate, not
|
||||
// withAndroidTarget()/withApple() — those silently drop androidMain under
|
||||
// com.android.kotlin.multiplatform.library (KT-80409). See core/ble/build.gradle.kts for the same pattern.
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
applyHierarchyTemplate(KotlinHierarchyTemplate.default) {
|
||||
common { group("nonWeb") { withCompilations { it.target.targetName != "wasmJs" } } }
|
||||
}
|
||||
|
||||
// The predicate above misses iosMain itself (only reaches the two leaf iOS compilations), so any
|
||||
// nonWebMain-only actual/declaration can't see nonWebMain's members without this explicit edge.
|
||||
sourceSets.getByName("iosMain") { dependsOn(sourceSets.getByName("nonWebMain")) }
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(projects.core.repository)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.di)
|
||||
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.kotlinx.atomicfu)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
|
||||
// android/jvm/ios only (see hierarchy template above) — androidx.datastore.preferences has no wasmJs
|
||||
// variant, and DataStorePrefsStore (the sole consumer) lives here.
|
||||
getByName("nonWebMain").dependencies { implementation(libs.androidx.datastore.preferences) }
|
||||
|
||||
wasmJsMain.dependencies { implementation(libs.kotlinx.browser) }
|
||||
|
||||
// All 7 existing commonTest files construct a real DataStore<Preferences> via PreferenceDataStoreFactory,
|
||||
// so they move to nonWebTest wholesale (same nonWebTest split core:database's DataStore/DAO tests got) —
|
||||
// nothing wasmJs-reachable in this module's tests needs a real DataStore today. No extra dependency
|
||||
// needed here: kotlin("test")/kotest/turbine/coroutines-test come from configureKmpTestDependencies()'s
|
||||
// commonTest additions, and nonWebTest inherits them via the hierarchy template's dependsOn edge.
|
||||
}
|
||||
}
|
||||
+15
-6
@@ -27,9 +27,11 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.store.PrefsStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
|
||||
/**
|
||||
* Koin module providing Android [DataStore] instances for each preference domain.
|
||||
* Koin module providing Android [PrefsStore] instances (each backed by a real [DataStore]) for each preference domain.
|
||||
*
|
||||
* Each DataStore is a singleton backed by its own [CoroutineScope] using the injected [CoroutineDispatchers.io]
|
||||
* dispatcher, and includes a [SharedPreferencesMigration] to migrate legacy SharedPreferences data on first access.
|
||||
@@ -97,14 +99,21 @@ class CorePrefsAndroidModule {
|
||||
/**
|
||||
* [legacyName] is the SharedPreferences file this domain migrates from, [fileName] the DataStore file it lives in now.
|
||||
* Both are on-disk identities — changing either orphans existing user data.
|
||||
*
|
||||
* Returns [PrefsStore] (not the underlying `DataStore<Preferences>` directly) so every `provideXDataStore()` call site
|
||||
* above can keep calling `.asXDataStore()` unchanged — `asPrefsStore()` is the one line doing the adaptation.
|
||||
*/
|
||||
private fun store(
|
||||
context: Context,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
legacyName: String,
|
||||
fileName: String,
|
||||
): DataStore<Preferences> = PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(SharedPreferencesMigration(context, legacyName)),
|
||||
scope = CoroutineScope(dispatchers.io + SupervisorJob()),
|
||||
produceFile = { context.preferencesDataStoreFile(fileName) },
|
||||
)
|
||||
): PrefsStore {
|
||||
val dataStore: DataStore<Preferences> =
|
||||
PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(SharedPreferencesMigration(context, legacyName)),
|
||||
scope = CoroutineScope(dispatchers.io + SupervisorJob()),
|
||||
produceFile = { context.preferencesDataStoreFile(fileName) },
|
||||
)
|
||||
return dataStore.asPrefsStore()
|
||||
}
|
||||
+4
-5
@@ -16,9 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.analytics
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -30,6 +27,8 @@ import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.AnalyticsDataStore
|
||||
import org.meshtastic.core.prefs.di.AppDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.AnalyticsPrefs
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@@ -67,7 +66,7 @@ class AnalyticsPrefsImpl(
|
||||
const val KEY_ANALYTICS_ALLOWED = "allowed"
|
||||
const val KEY_INSTALL_ID = "appPrefs_install_id"
|
||||
|
||||
val KEY_ANALYTICS_ALLOWED_PREF = booleanPreferencesKey(KEY_ANALYTICS_ALLOWED)
|
||||
val KEY_INSTALL_ID_PREF = stringPreferencesKey(KEY_INSTALL_ID)
|
||||
val KEY_ANALYTICS_ALLOWED_PREF = booleanPrefsKey(KEY_ANALYTICS_ALLOWED)
|
||||
val KEY_INSTALL_ID_PREF = stringPrefsKey(KEY_INSTALL_ID)
|
||||
}
|
||||
}
|
||||
+14
-15
@@ -16,9 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.appfunctions
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.AppDataStore
|
||||
import org.meshtastic.core.prefs.store.PrefsKey
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.repository.AppFunctionsPrefs
|
||||
|
||||
@Single
|
||||
@@ -68,23 +67,23 @@ class AppFunctionsPrefsImpl(private val dataStore: AppDataStore, dispatchers: Co
|
||||
|
||||
override fun setGetUnreadSummaryEnabled(enabled: Boolean) = set(KEY_GET_UNREAD_SUMMARY, enabled)
|
||||
|
||||
private fun booleanPref(key: Preferences.Key<Boolean>, default: Boolean): StateFlow<Boolean> =
|
||||
private fun booleanPref(key: PrefsKey<Boolean>, default: Boolean): StateFlow<Boolean> =
|
||||
dataStore.data.map { it[key] ?: default }.stateIn(scope, SharingStarted.Eagerly, default)
|
||||
|
||||
private fun set(key: Preferences.Key<Boolean>, value: Boolean) {
|
||||
private fun set(key: PrefsKey<Boolean>, value: Boolean) {
|
||||
scope.launch { dataStore.edit { prefs -> prefs[key] = value } }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KEY_MASTER = booleanPreferencesKey("appfn_master_enabled")
|
||||
private val KEY_SEND_MESSAGE = booleanPreferencesKey("appfn_send_message")
|
||||
private val KEY_GET_MESH_STATUS = booleanPreferencesKey("appfn_get_mesh_status")
|
||||
private val KEY_GET_NODE_LIST = booleanPreferencesKey("appfn_get_node_list")
|
||||
private val KEY_GET_CHANNEL_INFO = booleanPreferencesKey("appfn_get_channel_info")
|
||||
private val KEY_GET_DEVICE_STATUS = booleanPreferencesKey("appfn_get_device_status")
|
||||
private val KEY_GET_NODE_DETAILS = booleanPreferencesKey("appfn_get_node_details")
|
||||
private val KEY_GET_MESH_METRICS = booleanPreferencesKey("appfn_get_mesh_metrics")
|
||||
private val KEY_GET_RECENT_MESSAGES = booleanPreferencesKey("appfn_get_recent_messages")
|
||||
private val KEY_GET_UNREAD_SUMMARY = booleanPreferencesKey("appfn_get_unread_summary")
|
||||
private val KEY_MASTER = booleanPrefsKey("appfn_master_enabled")
|
||||
private val KEY_SEND_MESSAGE = booleanPrefsKey("appfn_send_message")
|
||||
private val KEY_GET_MESH_STATUS = booleanPrefsKey("appfn_get_mesh_status")
|
||||
private val KEY_GET_NODE_LIST = booleanPrefsKey("appfn_get_node_list")
|
||||
private val KEY_GET_CHANNEL_INFO = booleanPrefsKey("appfn_get_channel_info")
|
||||
private val KEY_GET_DEVICE_STATUS = booleanPrefsKey("appfn_get_device_status")
|
||||
private val KEY_GET_NODE_DETAILS = booleanPrefsKey("appfn_get_node_details")
|
||||
private val KEY_GET_MESH_METRICS = booleanPrefsKey("appfn_get_mesh_metrics")
|
||||
private val KEY_GET_RECENT_MESSAGES = booleanPrefsKey("appfn_get_recent_messages")
|
||||
private val KEY_GET_UNREAD_SUMMARY = booleanPrefsKey("appfn_get_unread_summary")
|
||||
}
|
||||
}
|
||||
@@ -18,76 +18,72 @@
|
||||
|
||||
package org.meshtastic.core.prefs.di
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import org.meshtastic.core.prefs.store.PrefsStore
|
||||
|
||||
// One type per preference domain, so the compiler distinguishes them instead of a Koin string qualifier. Each is a
|
||||
// transparent `DataStore<Preferences>` — inject and use it exactly like one.
|
||||
// transparent PrefsStore — inject and use it exactly like one. PrefsStore itself (not `DataStore<Preferences>`
|
||||
// directly — androidx.datastore.preferences has no wasmJs variant) is what makes these usable from every target:
|
||||
// nonWebMain's DataStorePrefsStore adapts a real DataStore<Preferences>, wasmJsMain's LocalStoragePrefsStore is
|
||||
// backed by localStorage. See core/prefs/store/PrefsStore.kt.
|
||||
|
||||
interface AnalyticsDataStore : DataStore<Preferences>
|
||||
interface AnalyticsDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [AnalyticsDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asAnalyticsDataStore(): AnalyticsDataStore =
|
||||
object : AnalyticsDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asAnalyticsDataStore(): AnalyticsDataStore = object : AnalyticsDataStore, PrefsStore by this {}
|
||||
|
||||
interface AppDataStore : DataStore<Preferences>
|
||||
interface AppDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [AppDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asAppDataStore(): AppDataStore = object : AppDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asAppDataStore(): AppDataStore = object : AppDataStore, PrefsStore by this {}
|
||||
|
||||
interface CustomEmojiDataStore : DataStore<Preferences>
|
||||
interface CustomEmojiDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [CustomEmojiDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asCustomEmojiDataStore(): CustomEmojiDataStore =
|
||||
object : CustomEmojiDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asCustomEmojiDataStore(): CustomEmojiDataStore = object : CustomEmojiDataStore, PrefsStore by this {}
|
||||
|
||||
interface FilterDataStore : DataStore<Preferences>
|
||||
interface FilterDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [FilterDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asFilterDataStore(): FilterDataStore =
|
||||
object : FilterDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asFilterDataStore(): FilterDataStore = object : FilterDataStore, PrefsStore by this {}
|
||||
|
||||
interface HomoglyphEncodingDataStore : DataStore<Preferences>
|
||||
interface HomoglyphEncodingDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [HomoglyphEncodingDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asHomoglyphEncodingDataStore(): HomoglyphEncodingDataStore =
|
||||
object : HomoglyphEncodingDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asHomoglyphEncodingDataStore(): HomoglyphEncodingDataStore =
|
||||
object : HomoglyphEncodingDataStore, PrefsStore by this {}
|
||||
|
||||
interface MapConsentDataStore : DataStore<Preferences>
|
||||
interface MapConsentDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [MapConsentDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asMapConsentDataStore(): MapConsentDataStore =
|
||||
object : MapConsentDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asMapConsentDataStore(): MapConsentDataStore = object : MapConsentDataStore, PrefsStore by this {}
|
||||
|
||||
interface MapDataStore : DataStore<Preferences>
|
||||
interface MapDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [MapDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asMapDataStore(): MapDataStore = object : MapDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asMapDataStore(): MapDataStore = object : MapDataStore, PrefsStore by this {}
|
||||
|
||||
interface MapTileProviderDataStore : DataStore<Preferences>
|
||||
interface MapTileProviderDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [MapTileProviderDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asMapTileProviderDataStore(): MapTileProviderDataStore =
|
||||
object : MapTileProviderDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asMapTileProviderDataStore(): MapTileProviderDataStore =
|
||||
object : MapTileProviderDataStore, PrefsStore by this {}
|
||||
|
||||
interface MeshDataStore : DataStore<Preferences>
|
||||
interface MeshDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [MeshDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asMeshDataStore(): MeshDataStore = object : MeshDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asMeshDataStore(): MeshDataStore = object : MeshDataStore, PrefsStore by this {}
|
||||
|
||||
interface MeshLogDataStore : DataStore<Preferences>
|
||||
interface MeshLogDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [MeshLogDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asMeshLogDataStore(): MeshLogDataStore =
|
||||
object : MeshLogDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asMeshLogDataStore(): MeshLogDataStore = object : MeshLogDataStore, PrefsStore by this {}
|
||||
|
||||
interface RadioDataStore : DataStore<Preferences>
|
||||
interface RadioDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [RadioDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asRadioDataStore(): RadioDataStore =
|
||||
object : RadioDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asRadioDataStore(): RadioDataStore = object : RadioDataStore, PrefsStore by this {}
|
||||
|
||||
interface UiDataStore : DataStore<Preferences>
|
||||
interface UiDataStore : PrefsStore
|
||||
|
||||
/** Presents an existing store as [UiDataStore]; the wrapper adds nothing but identity. */
|
||||
fun DataStore<Preferences>.asUiDataStore(): UiDataStore = object : UiDataStore, DataStore<Preferences> by this {}
|
||||
fun PrefsStore.asUiDataStore(): UiDataStore = object : UiDataStore, PrefsStore by this {}
|
||||
+7
-8
@@ -16,10 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.discovery
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -30,6 +26,9 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.UiDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.intPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.DiscoveryPrefs
|
||||
|
||||
@Single
|
||||
@@ -72,10 +71,10 @@ class DiscoveryPrefsImpl(private val dataStore: UiDataStore, dispatchers: Corout
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KEY_DWELL_MINUTES = intPreferencesKey("discovery_dwell_minutes")
|
||||
private val KEY_SELECTED_PRESETS = stringPreferencesKey("discovery_selected_presets")
|
||||
private val KEY_AI_ENABLED = booleanPreferencesKey("discovery_ai_enabled")
|
||||
private val KEY_TOPOLOGY_OVERLAY = booleanPreferencesKey("discovery_topology_overlay")
|
||||
private val KEY_DWELL_MINUTES = intPrefsKey("discovery_dwell_minutes")
|
||||
private val KEY_SELECTED_PRESETS = stringPrefsKey("discovery_selected_presets")
|
||||
private val KEY_AI_ENABLED = booleanPrefsKey("discovery_ai_enabled")
|
||||
private val KEY_TOPOLOGY_OVERLAY = booleanPrefsKey("discovery_topology_overlay")
|
||||
private const val PRESET_DELIMITER = ","
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -16,8 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.discovery
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -31,6 +29,7 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.UiDataStore
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.MeshBeaconPrefs
|
||||
|
||||
@Single
|
||||
@@ -64,7 +63,7 @@ class MeshBeaconPrefsImpl(private val dataStore: UiDataStore, dispatchers: Corou
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val KEY_STORED_BEACONS = stringPreferencesKey("mesh_beacon_stored_offers")
|
||||
val KEY_STORED_BEACONS = stringPrefsKey("mesh_beacon_stored_offers")
|
||||
|
||||
// Newline can never appear in a beacon record (fields are numeric + base64), so it is a safe row separator.
|
||||
const val RECORD_DELIMITER = "\n"
|
||||
|
||||
+4
-5
@@ -16,9 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.emoji
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.CustomEmojiDataStore
|
||||
import org.meshtastic.core.prefs.store.intPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.CustomEmojiPrefs
|
||||
|
||||
@Single
|
||||
@@ -60,7 +59,7 @@ class CustomEmojiPrefsImpl(private val dataStore: CustomEmojiDataStore, dispatch
|
||||
|
||||
companion object {
|
||||
const val KEY_EMOJI_FREQ = "pref_key_custom_emoji_freq"
|
||||
val KEY_EMOJI_FREQ_PREF = stringPreferencesKey(KEY_EMOJI_FREQ)
|
||||
val KEY_SKIN_TONE_PREF = intPreferencesKey("pref_key_skin_tone")
|
||||
val KEY_EMOJI_FREQ_PREF = stringPrefsKey(KEY_EMOJI_FREQ)
|
||||
val KEY_SKIN_TONE_PREF = intPrefsKey("pref_key_skin_tone")
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -16,9 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.filter
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.FilterDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringSetPrefsKey
|
||||
import org.meshtastic.core.repository.FilterPrefs
|
||||
|
||||
@Single
|
||||
@@ -56,7 +55,7 @@ class FilterPrefsImpl(private val dataStore: FilterDataStore, dispatchers: Corou
|
||||
const val KEY_FILTER_WORDS = "filter_words"
|
||||
const val FILTER_PREFS_NAME = "filter-prefs"
|
||||
|
||||
val KEY_FILTER_ENABLED_PREF = booleanPreferencesKey(KEY_FILTER_ENABLED)
|
||||
val KEY_FILTER_WORDS_PREF = stringSetPreferencesKey(KEY_FILTER_WORDS)
|
||||
val KEY_FILTER_ENABLED_PREF = booleanPrefsKey(KEY_FILTER_ENABLED)
|
||||
val KEY_FILTER_WORDS_PREF = stringSetPrefsKey(KEY_FILTER_WORDS)
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -16,8 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.homoglyph
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -28,6 +26,7 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.repository.HomoglyphPrefs
|
||||
|
||||
@Single
|
||||
@@ -44,6 +43,6 @@ class HomoglyphPrefsImpl(private val dataStore: HomoglyphEncodingDataStore, disp
|
||||
|
||||
companion object {
|
||||
const val KEY_ENABLED = "enabled"
|
||||
val KEY_ENABLED_PREF = booleanPreferencesKey(KEY_ENABLED)
|
||||
val KEY_ENABLED_PREF = booleanPrefsKey(KEY_ENABLED)
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -16,8 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.map
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -31,6 +29,7 @@ import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.cachedFlow
|
||||
import org.meshtastic.core.prefs.di.MapConsentDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.repository.MapConsentPrefs
|
||||
|
||||
@Single
|
||||
@@ -41,11 +40,11 @@ class MapConsentPrefsImpl(private val dataStore: MapConsentDataStore, dispatcher
|
||||
private val consentFlows = atomic(persistentMapOf<Int?, Lazy<StateFlow<Boolean>>>())
|
||||
|
||||
override fun shouldReportLocation(nodeNum: Int?): StateFlow<Boolean> = cachedFlow(consentFlows, nodeNum) {
|
||||
val key = booleanPreferencesKey(nodeNum.toString())
|
||||
val key = booleanPrefsKey(nodeNum.toString())
|
||||
dataStore.data.map { it[key] ?: false }.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
}
|
||||
|
||||
override fun setShouldReportLocation(nodeNum: Int?, report: Boolean) {
|
||||
scope.launch { dataStore.edit { prefs -> prefs[booleanPreferencesKey(nodeNum.toString())] = report } }
|
||||
scope.launch { dataStore.edit { prefs -> prefs[booleanPrefsKey(nodeNum.toString())] = report } }
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.map
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.doublePreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -33,6 +27,11 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.MapDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.doublePrefsKey
|
||||
import org.meshtastic.core.prefs.store.intPrefsKey
|
||||
import org.meshtastic.core.prefs.store.longPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringSetPrefsKey
|
||||
import org.meshtastic.core.repository.MapCameraPosition
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
|
||||
@@ -190,23 +189,23 @@ class MapPrefsImpl(private val dataStore: MapDataStore, dispatchers: CoroutineDi
|
||||
.first()
|
||||
|
||||
companion object {
|
||||
val KEY_MAP_STYLE_PREF = intPreferencesKey("map_style_id")
|
||||
val KEY_SHOW_ONLY_FAVORITES_PREF = booleanPreferencesKey("show_only_favorites")
|
||||
val KEY_SHOW_WAYPOINTS_PREF = booleanPreferencesKey("show_waypoints")
|
||||
val KEY_SHOW_PRECISION_CIRCLE_PREF = booleanPreferencesKey("show_precision_circle")
|
||||
val KEY_LAST_HEARD_FILTER_PREF = longPreferencesKey("last_heard_filter")
|
||||
val KEY_LAST_HEARD_TRACK_FILTER_PREF = longPreferencesKey("last_heard_track_filter")
|
||||
val KEY_HIDDEN_LAYER_URLS_PREF = stringSetPreferencesKey("hidden_layer_urls")
|
||||
val KEY_NETWORK_MAP_LAYERS_PREF = stringSetPreferencesKey("network_map_layers")
|
||||
val KEY_LAYER_OPACITY_PREF = stringSetPreferencesKey("layer_opacity")
|
||||
val KEY_ONLY_ONLINE_PREF = booleanPreferencesKey("map_only_online")
|
||||
val KEY_ONLY_DIRECT_PREF = booleanPreferencesKey("map_only_direct")
|
||||
val KEY_EXCLUDE_MQTT_PREF = booleanPreferencesKey("map_exclude_mqtt")
|
||||
val KEY_SHOW_IGNORED_PREF = booleanPreferencesKey("map_show_ignored")
|
||||
val KEY_INCLUDE_UNKNOWN_PREF = booleanPreferencesKey("map_include_unknown")
|
||||
val KEY_EXCLUDED_ROLES_PREF = stringSetPreferencesKey("map_excluded_roles")
|
||||
val KEY_CAMERA_LATITUDE = doublePreferencesKey("camera_latitude")
|
||||
val KEY_CAMERA_LONGITUDE = doublePreferencesKey("camera_longitude")
|
||||
val KEY_CAMERA_ZOOM = doublePreferencesKey("camera_zoom")
|
||||
val KEY_MAP_STYLE_PREF = intPrefsKey("map_style_id")
|
||||
val KEY_SHOW_ONLY_FAVORITES_PREF = booleanPrefsKey("show_only_favorites")
|
||||
val KEY_SHOW_WAYPOINTS_PREF = booleanPrefsKey("show_waypoints")
|
||||
val KEY_SHOW_PRECISION_CIRCLE_PREF = booleanPrefsKey("show_precision_circle")
|
||||
val KEY_LAST_HEARD_FILTER_PREF = longPrefsKey("last_heard_filter")
|
||||
val KEY_LAST_HEARD_TRACK_FILTER_PREF = longPrefsKey("last_heard_track_filter")
|
||||
val KEY_HIDDEN_LAYER_URLS_PREF = stringSetPrefsKey("hidden_layer_urls")
|
||||
val KEY_NETWORK_MAP_LAYERS_PREF = stringSetPrefsKey("network_map_layers")
|
||||
val KEY_LAYER_OPACITY_PREF = stringSetPrefsKey("layer_opacity")
|
||||
val KEY_ONLY_ONLINE_PREF = booleanPrefsKey("map_only_online")
|
||||
val KEY_ONLY_DIRECT_PREF = booleanPrefsKey("map_only_direct")
|
||||
val KEY_EXCLUDE_MQTT_PREF = booleanPrefsKey("map_exclude_mqtt")
|
||||
val KEY_SHOW_IGNORED_PREF = booleanPrefsKey("map_show_ignored")
|
||||
val KEY_INCLUDE_UNKNOWN_PREF = booleanPrefsKey("map_include_unknown")
|
||||
val KEY_EXCLUDED_ROLES_PREF = stringSetPrefsKey("map_excluded_roles")
|
||||
val KEY_CAMERA_LATITUDE = doublePrefsKey("camera_latitude")
|
||||
val KEY_CAMERA_LONGITUDE = doublePrefsKey("camera_longitude")
|
||||
val KEY_CAMERA_ZOOM = doublePrefsKey("camera_zoom")
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -16,8 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.map
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -28,6 +26,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.MapTileProviderDataStore
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.MapTileProviderPrefs
|
||||
|
||||
@Single
|
||||
@@ -69,7 +68,7 @@ class MapTileProviderPrefsImpl(private val dataStore: MapTileProviderDataStore,
|
||||
companion object {
|
||||
const val KEY_CUSTOM_PROVIDERS = "custom_tile_providers"
|
||||
const val KEY_SELECTED_CUSTOM_PROVIDER_ID = "selected_custom_tile_provider_id"
|
||||
val KEY_CUSTOM_PROVIDERS_PREF = stringPreferencesKey(KEY_CUSTOM_PROVIDERS)
|
||||
val KEY_SELECTED_CUSTOM_PROVIDER_ID_PREF = stringPreferencesKey(KEY_SELECTED_CUSTOM_PROVIDER_ID)
|
||||
val KEY_CUSTOM_PROVIDERS_PREF = stringPrefsKey(KEY_CUSTOM_PROVIDERS)
|
||||
val KEY_SELECTED_CUSTOM_PROVIDER_ID_PREF = stringPrefsKey(KEY_SELECTED_CUSTOM_PROVIDER_ID)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.mesh
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -34,6 +31,8 @@ import org.meshtastic.core.common.util.normalizeAddress
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.cachedFlow
|
||||
import org.meshtastic.core.prefs.di.MeshDataStore
|
||||
import org.meshtastic.core.prefs.store.intPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.MeshPrefs
|
||||
|
||||
@Single
|
||||
@@ -63,14 +62,14 @@ class MeshPrefsImpl(private val dataStore: MeshDataStore, dispatchers: Coroutine
|
||||
dataStore.data.first()[KEY_DEVICE_ADDRESS_PREF] ?: NO_DEVICE_SELECTED
|
||||
|
||||
override fun getStoreForwardLastRequest(address: String?): StateFlow<Int> = cachedFlow(storeForwardFlows, address) {
|
||||
val key = intPreferencesKey(storeForwardKey(address))
|
||||
val key = intPrefsKey(storeForwardKey(address))
|
||||
dataStore.data.map { it[key] ?: 0 }.stateIn(scope, SharingStarted.Eagerly, 0)
|
||||
}
|
||||
|
||||
override fun setStoreForwardLastRequest(address: String?, timestamp: Int) {
|
||||
scope.launch {
|
||||
dataStore.edit { prefs ->
|
||||
val key = intPreferencesKey(storeForwardKey(address))
|
||||
val key = intPrefsKey(storeForwardKey(address))
|
||||
if (timestamp <= 0) {
|
||||
prefs.remove(key)
|
||||
} else {
|
||||
@@ -83,7 +82,7 @@ class MeshPrefsImpl(private val dataStore: MeshDataStore, dispatchers: Coroutine
|
||||
private fun storeForwardKey(address: String?): String = "store-forward-last-request-${normalizeAddress(address)}"
|
||||
|
||||
companion object {
|
||||
val KEY_DEVICE_ADDRESS_PREF = stringPreferencesKey("device_address")
|
||||
val KEY_DEVICE_ADDRESS_PREF = stringPrefsKey("device_address")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -16,9 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.meshlog
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -30,6 +27,8 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.MeshLogDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.intPrefsKey
|
||||
import org.meshtastic.core.repository.MeshLogCleanupPolicy
|
||||
import org.meshtastic.core.repository.MeshLogPrefs
|
||||
|
||||
@@ -68,7 +67,7 @@ class MeshLogPrefsImpl(private val dataStore: MeshLogDataStore, dispatchers: Cor
|
||||
const val DEFAULT_RETENTION_DAYS = 30
|
||||
const val DEFAULT_LOGGING_ENABLED = true
|
||||
|
||||
val KEY_RETENTION_DAYS_PREF = intPreferencesKey(RETENTION_DAYS_KEY)
|
||||
val KEY_LOGGING_ENABLED_PREF = booleanPreferencesKey(LOGGING_ENABLED_KEY)
|
||||
val KEY_RETENTION_DAYS_PREF = intPrefsKey(RETENTION_DAYS_KEY)
|
||||
val KEY_LOGGING_ENABLED_PREF = booleanPrefsKey(LOGGING_ENABLED_KEY)
|
||||
}
|
||||
}
|
||||
+7
-8
@@ -16,9 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.notification
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.UiDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.NotificationPrefs
|
||||
|
||||
@Single
|
||||
@@ -116,10 +115,10 @@ class NotificationPrefsImpl(private val dataStore: UiDataStore, dispatchers: Cor
|
||||
private fun parseOptInIds(csv: String?): List<Int> =
|
||||
csv?.split(',')?.mapNotNull { it.toIntOrNull() }?.distinct() ?: emptyList()
|
||||
|
||||
private val KEY_MESSAGES_ENABLED = booleanPreferencesKey("notif_messages_enabled")
|
||||
private val KEY_NODE_EVENTS_ENABLED = booleanPreferencesKey("notif_node_events_enabled")
|
||||
private val KEY_NODE_EVENTS_AUTO_DISABLED = booleanPreferencesKey("notif_node_events_auto_disabled_event")
|
||||
private val KEY_LOW_BATTERY_ENABLED = booleanPreferencesKey("notif_low_battery_enabled")
|
||||
private val KEY_GEOFENCE_ALERT_OPT_INS = stringPreferencesKey("notif_geofence_alert_opt_ins")
|
||||
private val KEY_MESSAGES_ENABLED = booleanPrefsKey("notif_messages_enabled")
|
||||
private val KEY_NODE_EVENTS_ENABLED = booleanPrefsKey("notif_node_events_enabled")
|
||||
private val KEY_NODE_EVENTS_AUTO_DISABLED = booleanPrefsKey("notif_node_events_auto_disabled_event")
|
||||
private val KEY_LOW_BATTERY_ENABLED = booleanPrefsKey("notif_low_battery_enabled")
|
||||
private val KEY_GEOFENCE_ALERT_OPT_INS = stringPrefsKey("notif_geofence_alert_opt_ins")
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.radio
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -28,6 +26,7 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.RadioDataStore
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.RadioPrefs
|
||||
|
||||
@Single
|
||||
@@ -65,7 +64,7 @@ class RadioPrefsImpl(private val dataStore: RadioDataStore, dispatchers: Corouti
|
||||
}
|
||||
|
||||
companion object {
|
||||
val KEY_DEV_ADDR_PREF = stringPreferencesKey("devAddr2")
|
||||
val KEY_DEV_NAME_PREF = stringPreferencesKey("devName")
|
||||
val KEY_DEV_ADDR_PREF = stringPrefsKey("devAddr2")
|
||||
val KEY_DEV_NAME_PREF = stringPrefsKey("devName")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.prefs.store
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* The value shapes a [PrefsKey] can address — every case a real preference in this module actually uses. `internal`:
|
||||
* only the two platform-specific [PrefsStore] implementations (nonWebMain's `DataStorePrefsStore`, wasmJsMain's
|
||||
* `LocalStoragePrefsStore`) ever need to switch on it; every `*PrefsImpl` consumer only ever calls
|
||||
* [PrefsSnapshot.get]/[PrefsSnapshot.Editor.set] generically and never inspects a key's shape.
|
||||
*/
|
||||
internal enum class PrefsKeyType {
|
||||
BOOLEAN,
|
||||
INT,
|
||||
LONG,
|
||||
DOUBLE,
|
||||
STRING,
|
||||
STRING_SET,
|
||||
}
|
||||
|
||||
/**
|
||||
* A single, platform-neutral key into a [PrefsStore]. Mirrors `androidx.datastore.preferences.core.Preferences.Key`
|
||||
* closely enough that porting a `*PrefsImpl` off DataStore is a mechanical rename, not a rewrite: swap the
|
||||
* `androidx.datastore.preferences.core.*PreferencesKey` import/call for the matching `*PrefsKey` factory below, and
|
||||
* `Preferences`/`MutablePreferences` for [PrefsSnapshot]/[PrefsSnapshot.Editor].
|
||||
*
|
||||
* Equality/hashing is by [name] alone (matching `Preferences.Key`), so two keys constructed for the same name — even
|
||||
* from different call sites, e.g. a dynamic per-node key built fresh on every read and write — address the same
|
||||
* underlying value.
|
||||
*/
|
||||
class PrefsKey<T> internal constructor(internal val name: String, internal val type: PrefsKeyType) {
|
||||
override fun equals(other: Any?): Boolean = other is PrefsKey<*> && name == other.name
|
||||
|
||||
override fun hashCode(): Int = name.hashCode()
|
||||
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
fun booleanPrefsKey(name: String): PrefsKey<Boolean> = PrefsKey(name, PrefsKeyType.BOOLEAN)
|
||||
|
||||
fun intPrefsKey(name: String): PrefsKey<Int> = PrefsKey(name, PrefsKeyType.INT)
|
||||
|
||||
fun longPrefsKey(name: String): PrefsKey<Long> = PrefsKey(name, PrefsKeyType.LONG)
|
||||
|
||||
fun doublePrefsKey(name: String): PrefsKey<Double> = PrefsKey(name, PrefsKeyType.DOUBLE)
|
||||
|
||||
fun stringPrefsKey(name: String): PrefsKey<String> = PrefsKey(name, PrefsKeyType.STRING)
|
||||
|
||||
fun stringSetPrefsKey(name: String): PrefsKey<Set<String>> = PrefsKey(name, PrefsKeyType.STRING_SET)
|
||||
|
||||
/** Read-only view of a [PrefsStore]'s current values. Mirrors `androidx.datastore.preferences.core.Preferences`. */
|
||||
interface PrefsSnapshot {
|
||||
operator fun <T> get(key: PrefsKey<T>): T?
|
||||
|
||||
operator fun contains(key: PrefsKey<*>): Boolean
|
||||
|
||||
/**
|
||||
* Mutable view passed to [PrefsStore.edit]'s transform block. Mirrors `MutablePreferences` — including reading back
|
||||
* values already written earlier in the same transform, which several `*PrefsImpl` read-modify-write call sites
|
||||
* rely on (e.g. bumping an insertion-ordered CSV, or defaulting a value only if absent).
|
||||
*/
|
||||
interface Editor : PrefsSnapshot {
|
||||
operator fun <T> set(key: PrefsKey<T>, value: T)
|
||||
|
||||
fun <T> remove(key: PrefsKey<T>)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-neutral replacement for `DataStore<Preferences>`. `androidx.datastore.preferences` itself publishes no
|
||||
* wasmJs (or even plain JS) variant at any version — the `Preferences` type doesn't exist for that target — so this
|
||||
* module can't reference it from commonMain at all. Two implementations exist:
|
||||
* - nonWebMain's `DataStorePrefsStore` — a thin wrapper over a real `DataStore<Preferences>`; android/jvm/iOS keep
|
||||
* using the real DataStore machinery underneath, unchanged.
|
||||
* - wasmJsMain's `LocalStoragePrefsStore` — backed by the browser's `localStorage`, a synchronous, built-in key-value
|
||||
* string store. Unlike core:database's OPFS story, no Worker or npm dependency is needed here at all.
|
||||
*
|
||||
* `edit`'s [transform] is deliberately non-suspend (androidx's `DataStore.edit`'s is `suspend`) and returns `Unit`
|
||||
* (androidx's returns the resulting `Preferences`) — no call site in this module uses either capability, and keeping it
|
||||
* synchronous means no suspension point can interleave between a transform's reads and its writes on any platform.
|
||||
*/
|
||||
interface PrefsStore {
|
||||
val data: Flow<PrefsSnapshot>
|
||||
|
||||
suspend fun edit(transform: (PrefsSnapshot.Editor) -> Unit)
|
||||
}
|
||||
@@ -16,9 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.tak
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -29,6 +26,8 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.UiDataStore
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.intPrefsKey
|
||||
import org.meshtastic.core.repository.TakPrefs
|
||||
|
||||
@Single(binds = [TakPrefs::class])
|
||||
@@ -57,8 +56,8 @@ class TakPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDis
|
||||
}
|
||||
|
||||
companion object {
|
||||
val KEY_TAK_SERVER_ENABLED = booleanPreferencesKey("tak_server_enabled")
|
||||
val KEY_TAK_MESH_TO_COT = booleanPreferencesKey("tak_mesh_to_cot")
|
||||
val KEY_TAK_SERVER_CHANNEL = intPreferencesKey("tak_server_channel")
|
||||
val KEY_TAK_SERVER_ENABLED = booleanPrefsKey("tak_server_enabled")
|
||||
val KEY_TAK_MESH_TO_COT = booleanPrefsKey("tak_mesh_to_cot")
|
||||
val KEY_TAK_SERVER_CHANNEL = intPrefsKey("tak_server_channel")
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -16,9 +16,9 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.ui
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import org.meshtastic.core.model.NodeListDensity
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
|
||||
/**
|
||||
* DataStore preference keys for node list layout configuration. Key strings are used directly by DataStore — do not
|
||||
@@ -40,15 +40,15 @@ enum class NodeListLayoutPreferences(val key: String, val defaultBoolean: Boolea
|
||||
private const val DENSITY_KEY = "node-list-density"
|
||||
val DEFAULT_DENSITY = NodeListDensity.COMPLETE.name
|
||||
|
||||
val KEY_DENSITY = stringPreferencesKey(DENSITY_KEY)
|
||||
val KEY_SHOW_POWER = booleanPreferencesKey(SHOULD_SHOW_POWER.key)
|
||||
val KEY_SHOW_LAST_HEARD = booleanPreferencesKey(SHOULD_SHOW_LAST_HEARD.key)
|
||||
val KEY_LAST_HEARD_RELATIVE = booleanPreferencesKey(LAST_HEARD_IS_RELATIVE.key)
|
||||
val KEY_SHOW_LOCATION = booleanPreferencesKey(SHOULD_SHOW_LOCATION.key)
|
||||
val KEY_SHOW_HOPS = booleanPreferencesKey(SHOULD_SHOW_HOPS.key)
|
||||
val KEY_SHOW_SIGNAL = booleanPreferencesKey(SHOULD_SHOW_SIGNAL.key)
|
||||
val KEY_SHOW_CHANNEL = booleanPreferencesKey(SHOULD_SHOW_CHANNEL.key)
|
||||
val KEY_SHOW_ROLE = booleanPreferencesKey(SHOULD_SHOW_ROLE.key)
|
||||
val KEY_SHOW_TELEMETRY = booleanPreferencesKey(SHOULD_SHOW_TELEMETRY.key)
|
||||
val KEY_DENSITY = stringPrefsKey(DENSITY_KEY)
|
||||
val KEY_SHOW_POWER = booleanPrefsKey(SHOULD_SHOW_POWER.key)
|
||||
val KEY_SHOW_LAST_HEARD = booleanPrefsKey(SHOULD_SHOW_LAST_HEARD.key)
|
||||
val KEY_LAST_HEARD_RELATIVE = booleanPrefsKey(LAST_HEARD_IS_RELATIVE.key)
|
||||
val KEY_SHOW_LOCATION = booleanPrefsKey(SHOULD_SHOW_LOCATION.key)
|
||||
val KEY_SHOW_HOPS = booleanPrefsKey(SHOULD_SHOW_HOPS.key)
|
||||
val KEY_SHOW_SIGNAL = booleanPrefsKey(SHOULD_SHOW_SIGNAL.key)
|
||||
val KEY_SHOW_CHANNEL = booleanPrefsKey(SHOULD_SHOW_CHANNEL.key)
|
||||
val KEY_SHOW_ROLE = booleanPrefsKey(SHOULD_SHOW_ROLE.key)
|
||||
val KEY_SHOW_TELEMETRY = booleanPrefsKey(SHOULD_SHOW_TELEMETRY.key)
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.prefs.ui
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -35,6 +30,10 @@ import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DeviceType
|
||||
import org.meshtastic.core.prefs.cachedFlow
|
||||
import org.meshtastic.core.prefs.di.UiDataStore
|
||||
import org.meshtastic.core.prefs.store.PrefsSnapshot
|
||||
import org.meshtastic.core.prefs.store.booleanPrefsKey
|
||||
import org.meshtastic.core.prefs.store.intPrefsKey
|
||||
import org.meshtastic.core.prefs.store.stringPrefsKey
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
@@ -209,12 +208,12 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp
|
||||
|
||||
override fun shouldProvideNodeLocation(nodeNum: Int): StateFlow<Boolean> =
|
||||
cachedFlow(provideNodeLocationFlows, nodeNum) {
|
||||
val key = booleanPreferencesKey(provideLocationKey(nodeNum))
|
||||
val key = booleanPrefsKey(provideLocationKey(nodeNum))
|
||||
dataStore.data.map { it[key] ?: false }.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
}
|
||||
|
||||
override fun setShouldProvideNodeLocation(nodeNum: Int, provide: Boolean) {
|
||||
scope.launch { dataStore.edit { it[booleanPreferencesKey(provideLocationKey(nodeNum))] = provide } }
|
||||
scope.launch { dataStore.edit { it[booleanPrefsKey(provideLocationKey(nodeNum))] = provide } }
|
||||
}
|
||||
|
||||
private fun provideLocationKey(nodeNum: Int) = "provide-location-$nodeNum"
|
||||
@@ -312,34 +311,34 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp
|
||||
}
|
||||
|
||||
companion object {
|
||||
val KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF = booleanPreferencesKey("has_shown_not_paired_warning")
|
||||
val KEY_SHOW_QUICK_CHAT_PREF = booleanPreferencesKey("show-quick-chat")
|
||||
val KEY_SHOW_FULL_MESSAGE_TIMESTAMPS = booleanPreferencesKey("show-full-message-timestamps")
|
||||
val KEY_EVENT_THEME_ENABLED = booleanPreferencesKey("event-theme-enabled")
|
||||
val KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF = booleanPrefsKey("has_shown_not_paired_warning")
|
||||
val KEY_SHOW_QUICK_CHAT_PREF = booleanPrefsKey("show-quick-chat")
|
||||
val KEY_SHOW_FULL_MESSAGE_TIMESTAMPS = booleanPrefsKey("show-full-message-timestamps")
|
||||
val KEY_EVENT_THEME_ENABLED = booleanPrefsKey("event-theme-enabled")
|
||||
|
||||
val KEY_APP_INTRO_COMPLETED = booleanPreferencesKey("app_intro_completed")
|
||||
val KEY_THEME = intPreferencesKey("theme")
|
||||
private val KEY_UNITS_OVERRIDE = intPreferencesKey("units_override")
|
||||
val KEY_LOCALE = stringPreferencesKey("locale")
|
||||
val KEY_NODE_SORT = intPreferencesKey("node-sort-option")
|
||||
val KEY_INCLUDE_UNKNOWN = booleanPreferencesKey("include-unknown")
|
||||
val KEY_EXCLUDE_INFRASTRUCTURE = booleanPreferencesKey("exclude-infrastructure")
|
||||
val KEY_ONLY_ONLINE = booleanPreferencesKey("only-online")
|
||||
val KEY_ONLY_DIRECT = booleanPreferencesKey("only-direct")
|
||||
val KEY_SHOW_IGNORED = booleanPreferencesKey("show-ignored")
|
||||
val KEY_EXCLUDE_MQTT = booleanPreferencesKey("exclude-mqtt")
|
||||
val KEY_BLE_AUTO_SCAN = booleanPreferencesKey("ble-auto-scan")
|
||||
val KEY_NETWORK_AUTO_SCAN = booleanPreferencesKey("network-auto-scan")
|
||||
val KEY_SELECTED_CONNECTION_TRANSPORT = stringPreferencesKey("selected-connection-transport")
|
||||
val KEY_FIRMWARE_UPDATE_NOTIFICATION_KEYS = stringPreferencesKey("firmware-update-notification-keys")
|
||||
val KEY_SHOW_BLE_TRANSPORT = booleanPreferencesKey("show-ble-transport")
|
||||
val KEY_SHOW_NETWORK_TRANSPORT = booleanPreferencesKey("show-network-transport")
|
||||
val KEY_SHOW_USB_TRANSPORT = booleanPreferencesKey("show-usb-transport")
|
||||
val KEY_APP_INTRO_COMPLETED = booleanPrefsKey("app_intro_completed")
|
||||
val KEY_THEME = intPrefsKey("theme")
|
||||
private val KEY_UNITS_OVERRIDE = intPrefsKey("units_override")
|
||||
val KEY_LOCALE = stringPrefsKey("locale")
|
||||
val KEY_NODE_SORT = intPrefsKey("node-sort-option")
|
||||
val KEY_INCLUDE_UNKNOWN = booleanPrefsKey("include-unknown")
|
||||
val KEY_EXCLUDE_INFRASTRUCTURE = booleanPrefsKey("exclude-infrastructure")
|
||||
val KEY_ONLY_ONLINE = booleanPrefsKey("only-online")
|
||||
val KEY_ONLY_DIRECT = booleanPrefsKey("only-direct")
|
||||
val KEY_SHOW_IGNORED = booleanPrefsKey("show-ignored")
|
||||
val KEY_EXCLUDE_MQTT = booleanPrefsKey("exclude-mqtt")
|
||||
val KEY_BLE_AUTO_SCAN = booleanPrefsKey("ble-auto-scan")
|
||||
val KEY_NETWORK_AUTO_SCAN = booleanPrefsKey("network-auto-scan")
|
||||
val KEY_SELECTED_CONNECTION_TRANSPORT = stringPrefsKey("selected-connection-transport")
|
||||
val KEY_FIRMWARE_UPDATE_NOTIFICATION_KEYS = stringPrefsKey("firmware-update-notification-keys")
|
||||
val KEY_SHOW_BLE_TRANSPORT = booleanPrefsKey("show-ble-transport")
|
||||
val KEY_SHOW_NETWORK_TRANSPORT = booleanPrefsKey("show-network-transport")
|
||||
val KEY_SHOW_USB_TRANSPORT = booleanPrefsKey("show-usb-transport")
|
||||
private const val MAX_FIRMWARE_UPDATE_NOTIFICATION_KEYS = 100
|
||||
|
||||
private fun parseDeviceType(name: String): DeviceType? = DeviceType.entries.firstOrNull { it.name == name }
|
||||
|
||||
private fun legacySelectedConnectionTransport(preferences: Preferences): DeviceType? {
|
||||
private fun legacySelectedConnectionTransport(preferences: PrefsSnapshot): DeviceType? {
|
||||
val hasLegacyTransportPreference =
|
||||
KEY_SHOW_BLE_TRANSPORT in preferences ||
|
||||
KEY_SHOW_NETWORK_TRANSPORT in preferences ||
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.prefs.store
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.doublePreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Adapts a real `DataStore<Preferences>` (android/jvm/iOS only — `androidx.datastore.preferences` has no wasmJs
|
||||
* variant) to the platform-neutral [PrefsStore]. Every `CorePrefsAndroidModule`/`DesktopPlatformModule` call site that
|
||||
* used to hand a `DataStore<Preferences>` straight to an `asXDataStore()` wrapper now inserts this adapter first:
|
||||
* `store(...).asPrefsStore().asXDataStore()`.
|
||||
*/
|
||||
fun DataStore<Preferences>.asPrefsStore(): PrefsStore = DataStorePrefsStore(this)
|
||||
|
||||
private class DataStorePrefsStore(private val delegate: DataStore<Preferences>) : PrefsStore {
|
||||
override val data: Flow<PrefsSnapshot> = delegate.data.map { DataStorePrefsSnapshot(it) }
|
||||
|
||||
override suspend fun edit(transform: (PrefsSnapshot.Editor) -> Unit) {
|
||||
delegate.edit { prefs -> transform(DataStorePrefsEditor(prefs)) }
|
||||
}
|
||||
}
|
||||
|
||||
private class DataStorePrefsSnapshot(private val prefs: Preferences) : PrefsSnapshot {
|
||||
override fun <T> get(key: PrefsKey<T>): T? = prefs[key.toPreferencesKey()]
|
||||
|
||||
override fun contains(key: PrefsKey<*>): Boolean = key.toRawPreferencesKey() in prefs
|
||||
}
|
||||
|
||||
private class DataStorePrefsEditor(private val prefs: MutablePreferences) : PrefsSnapshot.Editor {
|
||||
override fun <T> get(key: PrefsKey<T>): T? = prefs[key.toPreferencesKey()]
|
||||
|
||||
override fun contains(key: PrefsKey<*>): Boolean = key.toRawPreferencesKey() in prefs
|
||||
|
||||
override fun <T> set(key: PrefsKey<T>, value: T) {
|
||||
prefs[key.toPreferencesKey()] = value
|
||||
}
|
||||
|
||||
override fun <T> remove(key: PrefsKey<T>) {
|
||||
prefs.remove(key.toPreferencesKey())
|
||||
}
|
||||
}
|
||||
|
||||
private fun PrefsKey<*>.toRawPreferencesKey(): Preferences.Key<*> = when (type) {
|
||||
PrefsKeyType.BOOLEAN -> booleanPreferencesKey(name)
|
||||
PrefsKeyType.INT -> intPreferencesKey(name)
|
||||
PrefsKeyType.LONG -> longPreferencesKey(name)
|
||||
PrefsKeyType.DOUBLE -> doublePreferencesKey(name)
|
||||
PrefsKeyType.STRING -> stringPreferencesKey(name)
|
||||
PrefsKeyType.STRING_SET -> stringSetPreferencesKey(name)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> PrefsKey<T>.toPreferencesKey(): Preferences.Key<T> = toRawPreferencesKey() as Preferences.Key<T>
|
||||
+2
-1
@@ -26,6 +26,7 @@ import okio.FileSystem
|
||||
import okio.Path
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.asFilterDataStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
import org.meshtastic.core.repository.FilterPrefs
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
@@ -55,7 +56,7 @@ class FilterPrefsTest {
|
||||
produceFile = { tmpDir / "test.preferences_pb" },
|
||||
)
|
||||
dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
|
||||
filterPrefs = FilterPrefsImpl(dataStore.asFilterDataStore(), dispatchers)
|
||||
filterPrefs = FilterPrefsImpl(dataStore.asPrefsStore().asFilterDataStore(), dispatchers)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
+2
-1
@@ -27,6 +27,7 @@ import okio.FileSystem
|
||||
import okio.Path
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.asMapDataStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
import org.meshtastic.core.repository.MapCameraPosition
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
@@ -54,7 +55,7 @@ class MapPrefsImplTest {
|
||||
)
|
||||
prefs =
|
||||
MapPrefsImpl(
|
||||
dataStore.asMapDataStore(),
|
||||
dataStore.asPrefsStore().asMapDataStore(),
|
||||
CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher),
|
||||
)
|
||||
}
|
||||
+3
-3
@@ -19,7 +19,6 @@ package org.meshtastic.core.prefs.mesh
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
@@ -32,6 +31,7 @@ import okio.Path
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.MeshDataStore
|
||||
import org.meshtastic.core.prefs.di.asMeshDataStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
@@ -68,9 +68,9 @@ class MeshPrefsImplTest {
|
||||
@Test
|
||||
fun `await device address waits for persisted data instead of returning the flow default`() = testScope.runTest {
|
||||
val persistedAddress = "xAA:BB:CC:DD:EE:FF"
|
||||
dataStore.edit { preferences -> preferences[MeshPrefsImpl.KEY_DEVICE_ADDRESS_PREF] = persistedAddress }
|
||||
dataStore.asPrefsStore().edit { editor -> editor[MeshPrefsImpl.KEY_DEVICE_ADDRESS_PREF] = persistedAddress }
|
||||
val loadGate = CompletableDeferred<Unit>()
|
||||
val delegate = dataStore.asMeshDataStore()
|
||||
val delegate = dataStore.asPrefsStore().asMeshDataStore()
|
||||
val delayedDataStore =
|
||||
object : MeshDataStore by delegate {
|
||||
override val data = delegate.data.onStart { loadGate.await() }
|
||||
+2
-1
@@ -26,6 +26,7 @@ import okio.FileSystem
|
||||
import okio.Path
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.asUiDataStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
import org.meshtastic.core.repository.NotificationPrefs
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
@@ -55,7 +56,7 @@ class NotificationPrefsTest {
|
||||
produceFile = { tmpDir / "test.preferences_pb" },
|
||||
)
|
||||
dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
|
||||
notificationPrefs = NotificationPrefsImpl(dataStore.asUiDataStore(), dispatchers)
|
||||
notificationPrefs = NotificationPrefsImpl(dataStore.asPrefsStore().asUiDataStore(), dispatchers)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
+2
-1
@@ -26,6 +26,7 @@ import okio.FileSystem
|
||||
import okio.Path
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.asUiDataStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
import org.meshtastic.core.repository.TakPrefs
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
@@ -54,7 +55,7 @@ class TakPrefsTest {
|
||||
produceFile = { tmpDir / "test.preferences_pb" },
|
||||
)
|
||||
dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
|
||||
takPrefs = TakPrefsImpl(dataStore.asUiDataStore(), dispatchers)
|
||||
takPrefs = TakPrefsImpl(dataStore.asPrefsStore().asUiDataStore(), dispatchers)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
+2
-1
@@ -27,6 +27,7 @@ import okio.Path
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.NodeListDensity
|
||||
import org.meshtastic.core.prefs.di.asUiDataStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
@@ -52,7 +53,7 @@ class NodeListLayoutPrefsTest {
|
||||
produceFile = { tmpDir / "test.preferences_pb" },
|
||||
)
|
||||
val dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
|
||||
prefs = UiPrefsImpl(dataStore.asUiDataStore(), dispatchers)
|
||||
prefs = UiPrefsImpl(dataStore.asPrefsStore().asUiDataStore(), dispatchers)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
+9
-8
@@ -19,7 +19,6 @@ package org.meshtastic.core.prefs.ui
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
@@ -31,6 +30,7 @@ import okio.Path
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DeviceType
|
||||
import org.meshtastic.core.prefs.di.asUiDataStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
@@ -59,7 +59,7 @@ class UiPrefsImplTest {
|
||||
produceFile = { tmpDir / "test.preferences_pb" },
|
||||
)
|
||||
val dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
|
||||
prefs = UiPrefsImpl(dataStore.asUiDataStore(), dispatchers)
|
||||
prefs = UiPrefsImpl(dataStore.asPrefsStore().asUiDataStore(), dispatchers)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
@@ -70,7 +70,7 @@ class UiPrefsImplTest {
|
||||
|
||||
@Test
|
||||
fun `explicit selected connection transport wins over legacy booleans`() = testScope.runTest {
|
||||
dataStore.edit {
|
||||
dataStore.asPrefsStore().edit {
|
||||
it[UiPrefsImpl.KEY_SELECTED_CONNECTION_TRANSPORT] = DeviceType.USB.name
|
||||
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = true
|
||||
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = true
|
||||
@@ -82,7 +82,7 @@ class UiPrefsImplTest {
|
||||
|
||||
@Test
|
||||
fun `invalid selected connection transport falls back to legacy booleans`() = testScope.runTest {
|
||||
dataStore.edit {
|
||||
dataStore.asPrefsStore().edit {
|
||||
it[UiPrefsImpl.KEY_SELECTED_CONNECTION_TRANSPORT] = "WIFI"
|
||||
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = false
|
||||
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = true
|
||||
@@ -98,7 +98,7 @@ class UiPrefsImplTest {
|
||||
|
||||
@Test
|
||||
fun `legacy selected connection transport defaults to BLE when all transports are visible`() = testScope.runTest {
|
||||
dataStore.edit {
|
||||
dataStore.asPrefsStore().edit {
|
||||
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = true
|
||||
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = true
|
||||
it[UiPrefsImpl.KEY_SHOW_USB_TRANSPORT] = true
|
||||
@@ -109,7 +109,7 @@ class UiPrefsImplTest {
|
||||
|
||||
@Test
|
||||
fun `legacy selected connection transport chooses TCP when BLE is hidden`() = testScope.runTest {
|
||||
dataStore.edit {
|
||||
dataStore.asPrefsStore().edit {
|
||||
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = false
|
||||
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = true
|
||||
it[UiPrefsImpl.KEY_SHOW_USB_TRANSPORT] = true
|
||||
@@ -120,7 +120,7 @@ class UiPrefsImplTest {
|
||||
|
||||
@Test
|
||||
fun `legacy selected connection transport chooses USB when only USB is visible`() = testScope.runTest {
|
||||
dataStore.edit {
|
||||
dataStore.asPrefsStore().edit {
|
||||
it[UiPrefsImpl.KEY_SHOW_BLE_TRANSPORT] = false
|
||||
it[UiPrefsImpl.KEY_SHOW_NETWORK_TRANSPORT] = false
|
||||
it[UiPrefsImpl.KEY_SHOW_USB_TRANSPORT] = true
|
||||
@@ -137,7 +137,8 @@ class UiPrefsImplTest {
|
||||
fun `full message timestamps persist when enabled`() = testScope.runTest {
|
||||
prefs.setShowFullMessageTimestamps(true)
|
||||
|
||||
val stored = dataStore.data.first { it[UiPrefsImpl.KEY_SHOW_FULL_MESSAGE_TIMESTAMPS] == true }
|
||||
val stored =
|
||||
dataStore.asPrefsStore().data.first { it[UiPrefsImpl.KEY_SHOW_FULL_MESSAGE_TIMESTAMPS] == true }
|
||||
assertTrue(prefs.showFullMessageTimestamps.value)
|
||||
assertEquals(true, stored[UiPrefsImpl.KEY_SHOW_FULL_MESSAGE_TIMESTAMPS])
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.prefs.di
|
||||
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.prefs.store.LocalStoragePrefsStore
|
||||
|
||||
/**
|
||||
* Koin module providing wasmJs [org.meshtastic.core.prefs.store.PrefsStore] instances for each preference domain,
|
||||
* backed by the browser's `localStorage`. Mirrors `CorePrefsAndroidModule`/desktopApp's `DesktopPlatformModule` — one
|
||||
* singleton store per domain, same `_ds`-suffixed names — minus the `SharedPreferencesMigration` (nothing to migrate
|
||||
* from on web) and the `CoroutineScope`/dispatcher plumbing (localStorage reads and writes are synchronous).
|
||||
*
|
||||
* Not yet registered anywhere: like core:database's `SingleDatabaseProvider`, nothing on wasmJs composes a Koin graph
|
||||
* yet (no `webApp` module exists in this repo pass) — a future one wires this in explicitly, the same way
|
||||
* `CorePrefsAndroidModule` is listed by name in androidApp's `AppKoinModule` despite already sitting inside
|
||||
* `CorePrefsModule`'s `@ComponentScan("org.meshtastic.core.prefs")`.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Module
|
||||
class CorePrefsWasmJsModule {
|
||||
@Single
|
||||
fun provideAnalyticsDataStore(): AnalyticsDataStore = LocalStoragePrefsStore("analytics_ds").asAnalyticsDataStore()
|
||||
|
||||
@Single
|
||||
fun provideHomoglyphEncodingDataStore(): HomoglyphEncodingDataStore =
|
||||
LocalStoragePrefsStore("homoglyph_encoding_ds").asHomoglyphEncodingDataStore()
|
||||
|
||||
@Single fun provideAppDataStore(): AppDataStore = LocalStoragePrefsStore("app_ds").asAppDataStore()
|
||||
|
||||
@Single
|
||||
fun provideCustomEmojiDataStore(): CustomEmojiDataStore =
|
||||
LocalStoragePrefsStore("custom_emoji_ds").asCustomEmojiDataStore()
|
||||
|
||||
@Single fun provideMapDataStore(): MapDataStore = LocalStoragePrefsStore("map_ds").asMapDataStore()
|
||||
|
||||
@Single
|
||||
fun provideMapConsentDataStore(): MapConsentDataStore =
|
||||
LocalStoragePrefsStore("map_consent_ds").asMapConsentDataStore()
|
||||
|
||||
@Single
|
||||
fun provideMapTileProviderDataStore(): MapTileProviderDataStore =
|
||||
LocalStoragePrefsStore("map_tile_provider_ds").asMapTileProviderDataStore()
|
||||
|
||||
@Single fun provideMeshDataStore(): MeshDataStore = LocalStoragePrefsStore("mesh_ds").asMeshDataStore()
|
||||
|
||||
@Single fun provideRadioDataStore(): RadioDataStore = LocalStoragePrefsStore("radio_ds").asRadioDataStore()
|
||||
|
||||
@Single fun provideUiDataStore(): UiDataStore = LocalStoragePrefsStore("ui_ds").asUiDataStore()
|
||||
|
||||
@Single fun provideMeshLogDataStore(): MeshLogDataStore = LocalStoragePrefsStore("meshlog_ds").asMeshLogDataStore()
|
||||
|
||||
@Single fun provideFilterDataStore(): FilterDataStore = LocalStoragePrefsStore("filter_ds").asFilterDataStore()
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.prefs.store
|
||||
|
||||
import kotlinx.browser.localStorage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* Backed by the browser's `localStorage` — a synchronous, built-in key-value string store, so (unlike core:database's
|
||||
* OPFS story) no Worker or npm dependency is needed here at all.
|
||||
*
|
||||
* Every domain's store gets its own [namespace] prefix on every localStorage key, matching the file-per-domain
|
||||
* isolation the real `DataStore<Preferences>` instances get on android/jvm/iOS (see CorePrefsAndroidModule /
|
||||
* DesktopPlatformModule's per-domain `fileName`).
|
||||
*
|
||||
* Reads are live: [LocalStorageSnapshot] holds no cached state, it re-reads `localStorage` on every
|
||||
* [PrefsSnapshot.get]/[PrefsSnapshot.contains] call. [revision] exists purely to give [data] a *new* emission after
|
||||
* every [edit] — `LocalStorageSnapshot` has no `equals` override, so each freshly-constructed instance is unequal by
|
||||
* reference to the last, which is exactly what [MutableStateFlow] needs to avoid conflating the update away.
|
||||
*
|
||||
* [DEFERRED]: same-tab writes are reflected immediately (every `edit()` publishes a new revision, and every read goes
|
||||
* straight to localStorage), but a write from another browser tab is not observed — that would need a
|
||||
* `window.onstorage` listener re-publishing a revision when this namespace's keys change, not attempted in this pass.
|
||||
*/
|
||||
internal class LocalStoragePrefsStore(private val namespace: String) : PrefsStore {
|
||||
private val revision = MutableStateFlow(LocalStorageSnapshot(namespace))
|
||||
|
||||
override val data: Flow<PrefsSnapshot> = revision
|
||||
|
||||
override suspend fun edit(transform: (PrefsSnapshot.Editor) -> Unit) {
|
||||
transform(LocalStorageEditor(namespace))
|
||||
revision.value = LocalStorageSnapshot(namespace)
|
||||
}
|
||||
}
|
||||
|
||||
private class LocalStorageSnapshot(private val namespace: String) : PrefsSnapshot {
|
||||
override fun <T> get(key: PrefsKey<T>): T? = decode(localStorage.getItem(storageKey(namespace, key)), key.type)
|
||||
|
||||
override fun contains(key: PrefsKey<*>): Boolean = localStorage.getItem(storageKey(namespace, key)) != null
|
||||
}
|
||||
|
||||
private class LocalStorageEditor(private val namespace: String) : PrefsSnapshot.Editor {
|
||||
override fun <T> get(key: PrefsKey<T>): T? = decode(localStorage.getItem(storageKey(namespace, key)), key.type)
|
||||
|
||||
override fun contains(key: PrefsKey<*>): Boolean = localStorage.getItem(storageKey(namespace, key)) != null
|
||||
|
||||
override fun <T> set(key: PrefsKey<T>, value: T) {
|
||||
localStorage.setItem(storageKey(namespace, key), encode(value, key.type))
|
||||
}
|
||||
|
||||
override fun <T> remove(key: PrefsKey<T>) {
|
||||
localStorage.removeItem(storageKey(namespace, key))
|
||||
}
|
||||
}
|
||||
|
||||
private fun storageKey(namespace: String, key: PrefsKey<*>): String = "$namespace:${key.name}"
|
||||
|
||||
// Matches the '|'-joined convention UiPrefsImpl's own firmwareUpdateNotificationKeys already uses for a
|
||||
// Set<String>-shaped preference — reusing it here rather than inventing a new serialization format. As there, a set
|
||||
// element containing '|' itself would corrupt round-tripping; no current preference stores one.
|
||||
private const val SET_DELIMITER = "|"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> decode(raw: String?, type: PrefsKeyType): T? {
|
||||
if (raw == null) return null
|
||||
val value: Any =
|
||||
when (type) {
|
||||
PrefsKeyType.BOOLEAN -> raw.toBooleanStrictOrNull() ?: return null
|
||||
PrefsKeyType.INT -> raw.toIntOrNull() ?: return null
|
||||
PrefsKeyType.LONG -> raw.toLongOrNull() ?: return null
|
||||
PrefsKeyType.DOUBLE -> raw.toDoubleOrNull() ?: return null
|
||||
PrefsKeyType.STRING -> raw
|
||||
PrefsKeyType.STRING_SET -> raw.split(SET_DELIMITER).filter(String::isNotEmpty).toSet()
|
||||
}
|
||||
return value as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> encode(value: T, type: PrefsKeyType): String = when (type) {
|
||||
PrefsKeyType.STRING_SET -> (value as Set<String>).joinToString(SET_DELIMITER)
|
||||
else -> value.toString()
|
||||
}
|
||||
@@ -23,6 +23,13 @@ plugins {
|
||||
kotlin {
|
||||
android { withHostTest {} }
|
||||
|
||||
// Library module: bare wasmJs(), no browser() (that's for the eventual webApp executable). Unlike
|
||||
// core:ble/core:database/core:prefs, no custom hierarchy group is needed: Location's android/jvm/ios
|
||||
// actuals already each live in their own independent source set (no shared nonWeb code to hide from
|
||||
// wasmJs), so a 4th, wasmJsMain/Location.kt, is all this module needs.
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
api(projects.core.model)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.repository
|
||||
|
||||
/**
|
||||
* wasmJs placeholder location type, mirroring jvmMain's — no consumer needs real location on web yet, same as desktop.
|
||||
* A real one would wrap the browser Geolocation API (`navigator.geolocation`); deferred until something actually calls
|
||||
* [LocationService]/[LocationRepository] on this target.
|
||||
*/
|
||||
actual class Location
|
||||
+38
-8
@@ -17,8 +17,6 @@
|
||||
package org.meshtastic.core.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.preferencesOf
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkerFactory
|
||||
@@ -41,6 +39,8 @@ import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.prefs.di.MeshLogDataStore
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefsImpl
|
||||
import org.meshtastic.core.prefs.store.PrefsKey
|
||||
import org.meshtastic.core.prefs.store.PrefsSnapshot
|
||||
import org.meshtastic.core.service.worker.MeshLogCleanupWorker
|
||||
import org.meshtastic.core.testing.FakeMeshLogRepository
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
@@ -169,23 +169,28 @@ class MeshLogCleanupWorkerTest {
|
||||
private val loadGate = CompletableDeferred<Unit>()
|
||||
private val persisted =
|
||||
MutableStateFlow(
|
||||
preferencesOf(
|
||||
MeshLogPrefsImpl.KEY_LOGGING_ENABLED_PREF to loggingEnabled,
|
||||
MeshLogPrefsImpl.KEY_RETENTION_DAYS_PREF to retentionDays,
|
||||
FakePrefsSnapshot(
|
||||
mapOf(
|
||||
MeshLogPrefsImpl.KEY_LOGGING_ENABLED_PREF to loggingEnabled,
|
||||
MeshLogPrefsImpl.KEY_RETENTION_DAYS_PREF to retentionDays,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
var collectionCount: Int = 0
|
||||
private set
|
||||
|
||||
override val data: Flow<Preferences> = flow {
|
||||
override val data: Flow<PrefsSnapshot> = flow {
|
||||
collectionCount += 1
|
||||
loadGate.await()
|
||||
emitAll(persisted)
|
||||
}
|
||||
|
||||
override suspend fun updateData(transform: suspend (Preferences) -> Preferences): Preferences =
|
||||
transform(persisted.value).also { persisted.value = it }
|
||||
override suspend fun edit(transform: (PrefsSnapshot.Editor) -> Unit) {
|
||||
val editor = FakePrefsEditor(persisted.value.values)
|
||||
transform(editor)
|
||||
persisted.value = FakePrefsSnapshot(editor.values)
|
||||
}
|
||||
|
||||
fun releaseLoad() {
|
||||
loadGate.complete(Unit)
|
||||
@@ -195,4 +200,29 @@ class MeshLogCleanupWorkerTest {
|
||||
loadGate.completeExceptionally(cause)
|
||||
}
|
||||
}
|
||||
|
||||
/** Minimal in-memory [PrefsSnapshot]/[PrefsSnapshot.Editor] test double — no real DataStore/localStorage. */
|
||||
private class FakePrefsSnapshot(val values: Map<PrefsKey<*>, Any?>) : PrefsSnapshot {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> get(key: PrefsKey<T>): T? = values[key] as T?
|
||||
|
||||
override fun contains(key: PrefsKey<*>): Boolean = values.containsKey(key)
|
||||
}
|
||||
|
||||
private class FakePrefsEditor(initial: Map<PrefsKey<*>, Any?>) : PrefsSnapshot.Editor {
|
||||
val values: MutableMap<PrefsKey<*>, Any?> = initial.toMutableMap()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> get(key: PrefsKey<T>): T? = values[key] as T?
|
||||
|
||||
override fun contains(key: PrefsKey<*>): Boolean = values.containsKey(key)
|
||||
|
||||
override fun <T> set(key: PrefsKey<T>, value: T) {
|
||||
values[key] = value
|
||||
}
|
||||
|
||||
override fun <T> remove(key: PrefsKey<T>) {
|
||||
values.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ import org.meshtastic.core.prefs.di.asMeshDataStore
|
||||
import org.meshtastic.core.prefs.di.asMeshLogDataStore
|
||||
import org.meshtastic.core.prefs.di.asRadioDataStore
|
||||
import org.meshtastic.core.prefs.di.asUiDataStore
|
||||
import org.meshtastic.core.prefs.store.asPrefsStore
|
||||
import org.meshtastic.desktop.DesktopBuildConfig
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
@@ -144,20 +145,31 @@ fun desktopPlatformModule() = module {
|
||||
single(named(PROCESS_LIFECYCLE)) { DesktopProcessLifecycleOwner().lifecycle }
|
||||
}
|
||||
|
||||
/** Typed preference-datastore singletons for each preference domain. */
|
||||
/**
|
||||
* Typed preference-datastore singletons for each preference domain.
|
||||
*
|
||||
* Every core:prefs store (all but the last line) inserts [asPrefsStore] before its `asXDataStore()` call: those marker
|
||||
* types are now `PrefsStore`-backed (androidx.datastore.preferences has no wasmJs variant, so core:prefs abstracts over
|
||||
* it — see core/prefs/store/PrefsStore.kt), not `DataStore<Preferences>` directly. `CorePreferencesDataStore` belongs
|
||||
* to core:datastore, a separate, unrelated module still typed directly against `DataStore<Preferences>` — left as-is.
|
||||
*/
|
||||
private fun desktopPreferencesDataStoreModule() = module {
|
||||
single<AnalyticsDataStore> { prefsStore("analytics", get()).asAnalyticsDataStore() }
|
||||
single<HomoglyphEncodingDataStore> { prefsStore("homoglyph_encoding", get()).asHomoglyphEncodingDataStore() }
|
||||
single<AppDataStore> { prefsStore("app", get()).asAppDataStore() }
|
||||
single<CustomEmojiDataStore> { prefsStore("custom_emoji", get()).asCustomEmojiDataStore() }
|
||||
single<MapDataStore> { prefsStore("map", get()).asMapDataStore() }
|
||||
single<MapConsentDataStore> { prefsStore("map_consent", get()).asMapConsentDataStore() }
|
||||
single<MapTileProviderDataStore> { prefsStore("map_tile_provider", get()).asMapTileProviderDataStore() }
|
||||
single<MeshDataStore> { prefsStore("mesh", get()).asMeshDataStore() }
|
||||
single<RadioDataStore> { prefsStore("radio", get()).asRadioDataStore() }
|
||||
single<UiDataStore> { prefsStore("ui", get()).asUiDataStore() }
|
||||
single<MeshLogDataStore> { prefsStore("meshlog", get()).asMeshLogDataStore() }
|
||||
single<FilterDataStore> { prefsStore("filter", get()).asFilterDataStore() }
|
||||
single<AnalyticsDataStore> { prefsStore("analytics", get()).asPrefsStore().asAnalyticsDataStore() }
|
||||
single<HomoglyphEncodingDataStore> {
|
||||
prefsStore("homoglyph_encoding", get()).asPrefsStore().asHomoglyphEncodingDataStore()
|
||||
}
|
||||
single<AppDataStore> { prefsStore("app", get()).asPrefsStore().asAppDataStore() }
|
||||
single<CustomEmojiDataStore> { prefsStore("custom_emoji", get()).asPrefsStore().asCustomEmojiDataStore() }
|
||||
single<MapDataStore> { prefsStore("map", get()).asPrefsStore().asMapDataStore() }
|
||||
single<MapConsentDataStore> { prefsStore("map_consent", get()).asPrefsStore().asMapConsentDataStore() }
|
||||
single<MapTileProviderDataStore> {
|
||||
prefsStore("map_tile_provider", get()).asPrefsStore().asMapTileProviderDataStore()
|
||||
}
|
||||
single<MeshDataStore> { prefsStore("mesh", get()).asPrefsStore().asMeshDataStore() }
|
||||
single<RadioDataStore> { prefsStore("radio", get()).asPrefsStore().asRadioDataStore() }
|
||||
single<UiDataStore> { prefsStore("ui", get()).asPrefsStore().asUiDataStore() }
|
||||
single<MeshLogDataStore> { prefsStore("meshlog", get()).asPrefsStore().asMeshLogDataStore() }
|
||||
single<FilterDataStore> { prefsStore("filter", get()).asPrefsStore().asFilterDataStore() }
|
||||
single<CorePreferencesDataStore> { prefsStore("core_preferences", get()).asCorePreferencesDataStore() }
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user