mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: Consolidate UI preference handling (#4895)
This commit is contained in:
parent
b4afe22030
commit
57242d905c
19 changed files with 197 additions and 297 deletions
|
|
@ -45,12 +45,12 @@ import org.koin.core.annotation.KoinViewModel
|
|||
import org.meshtastic.app.map.model.CustomTileProviderConfig
|
||||
import org.meshtastic.app.map.prefs.map.GoogleMapsPrefs
|
||||
import org.meshtastic.app.map.repository.CustomTileProviderRepository
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.BaseMapViewModel
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
@ -84,7 +84,7 @@ class MapViewModel(
|
|||
radioConfigRepository: RadioConfigRepository,
|
||||
radioController: RadioController,
|
||||
private val customTileProviderRepository: CustomTileProviderRepository,
|
||||
uiPreferencesDataSource: UiPreferencesDataSource,
|
||||
uiPrefs: UiPrefs,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) {
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ class MapViewModel(
|
|||
),
|
||||
)
|
||||
|
||||
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
|
||||
val theme: StateFlow<Int> = uiPrefs.theme
|
||||
|
||||
private val _errorFlow = MutableSharedFlow<String>()
|
||||
val errorFlow: SharedFlow<String> = _errorFlow.asSharedFlow()
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* 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.common
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface UiPreferences {
|
||||
val appIntroCompleted: StateFlow<Boolean>
|
||||
val theme: StateFlow<Int>
|
||||
val locale: StateFlow<String>
|
||||
val nodeSort: StateFlow<Int>
|
||||
val includeUnknown: StateFlow<Boolean>
|
||||
val excludeInfrastructure: StateFlow<Boolean>
|
||||
val onlyOnline: StateFlow<Boolean>
|
||||
val onlyDirect: StateFlow<Boolean>
|
||||
val showIgnored: StateFlow<Boolean>
|
||||
val excludeMqtt: StateFlow<Boolean>
|
||||
|
||||
fun setLocale(languageTag: String)
|
||||
|
||||
fun setAppIntroCompleted(completed: Boolean)
|
||||
|
||||
fun setTheme(value: Int)
|
||||
|
||||
fun setNodeSort(value: Int)
|
||||
|
||||
fun setIncludeUnknown(value: Boolean)
|
||||
|
||||
fun setExcludeInfrastructure(value: Boolean)
|
||||
|
||||
fun setOnlyOnline(value: Boolean)
|
||||
|
||||
fun setOnlyDirect(value: Boolean)
|
||||
|
||||
fun setShowIgnored(value: Boolean)
|
||||
|
||||
fun setExcludeMqtt(value: Boolean)
|
||||
|
||||
fun shouldProvideNodeLocation(nodeNum: Int): StateFlow<Boolean>
|
||||
|
||||
fun setShouldProvideNodeLocation(nodeNum: Int, provide: Boolean)
|
||||
}
|
||||
|
|
@ -33,13 +33,6 @@ import okio.Path.Companion.toOkioPath
|
|||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.datastore.KEY_APP_INTRO_COMPLETED
|
||||
import org.meshtastic.core.datastore.KEY_INCLUDE_UNKNOWN
|
||||
import org.meshtastic.core.datastore.KEY_NODE_SORT
|
||||
import org.meshtastic.core.datastore.KEY_ONLY_DIRECT
|
||||
import org.meshtastic.core.datastore.KEY_ONLY_ONLINE
|
||||
import org.meshtastic.core.datastore.KEY_SHOW_IGNORED
|
||||
import org.meshtastic.core.datastore.KEY_THEME
|
||||
import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
|
||||
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
|
||||
import org.meshtastic.core.datastore.serializer.LocalStatsSerializer
|
||||
|
|
@ -61,23 +54,7 @@ class PreferencesDataStoreModule {
|
|||
): DataStore<Preferences> = PreferenceDataStoreFactory.create(
|
||||
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
|
||||
migrations =
|
||||
listOf(
|
||||
SharedPreferencesMigration(context = context, sharedPreferencesName = USER_PREFERENCES_NAME),
|
||||
SharedPreferencesMigration(
|
||||
context = context,
|
||||
sharedPreferencesName = "ui-prefs",
|
||||
keysToMigrate =
|
||||
setOf(
|
||||
KEY_APP_INTRO_COMPLETED,
|
||||
KEY_THEME,
|
||||
KEY_NODE_SORT,
|
||||
KEY_INCLUDE_UNKNOWN,
|
||||
KEY_ONLY_ONLINE,
|
||||
KEY_ONLY_DIRECT,
|
||||
KEY_SHOW_IGNORED,
|
||||
),
|
||||
),
|
||||
),
|
||||
listOf(SharedPreferencesMigration(context = context, sharedPreferencesName = USER_PREFERENCES_NAME)),
|
||||
scope = scope,
|
||||
produceFile = { context.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-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.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
|
||||
const val KEY_APP_INTRO_COMPLETED = "app_intro_completed"
|
||||
const val KEY_THEME = "theme"
|
||||
const val KEY_LOCALE = "locale"
|
||||
|
||||
// Node list filters/sort
|
||||
const val KEY_NODE_SORT = "node-sort-option"
|
||||
const val KEY_INCLUDE_UNKNOWN = "include-unknown"
|
||||
const val KEY_EXCLUDE_INFRASTRUCTURE = "exclude-infrastructure"
|
||||
const val KEY_ONLY_ONLINE = "only-online"
|
||||
const val KEY_ONLY_DIRECT = "only-direct"
|
||||
const val KEY_SHOW_IGNORED = "show-ignored"
|
||||
const val KEY_EXCLUDE_MQTT = "exclude-mqtt"
|
||||
|
||||
@Single
|
||||
@Suppress("TooManyFunctions") // One setter per preference field — inherently grows with preferences.
|
||||
open class UiPreferencesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore<Preferences>) :
|
||||
UiPreferences {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
||||
|
||||
// Start this flow eagerly, so app intro doesn't flash (when disabled) on cold app start.
|
||||
override val appIntroCompleted: StateFlow<Boolean> =
|
||||
dataStore.prefStateFlow(key = APP_INTRO_COMPLETED, default = false, started = SharingStarted.Eagerly)
|
||||
|
||||
// Default value for AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
override val theme: StateFlow<Int> = dataStore.prefStateFlow(key = THEME, default = -1)
|
||||
|
||||
/** Persisted language tag (e.g. "de", "pt-BR"). Empty string means system default. */
|
||||
override val locale: StateFlow<String> =
|
||||
dataStore.prefStateFlow(key = LOCALE, default = "", started = SharingStarted.Eagerly)
|
||||
|
||||
override fun setLocale(languageTag: String) {
|
||||
dataStore.setPref(key = LOCALE, value = languageTag)
|
||||
}
|
||||
|
||||
override val nodeSort: StateFlow<Int> = dataStore.prefStateFlow(key = NODE_SORT, default = -1)
|
||||
override val includeUnknown: StateFlow<Boolean> = dataStore.prefStateFlow(key = INCLUDE_UNKNOWN, default = false)
|
||||
override val excludeInfrastructure: StateFlow<Boolean> =
|
||||
dataStore.prefStateFlow(key = EXCLUDE_INFRASTRUCTURE, default = false)
|
||||
override val onlyOnline: StateFlow<Boolean> = dataStore.prefStateFlow(key = ONLY_ONLINE, default = false)
|
||||
override val onlyDirect: StateFlow<Boolean> = dataStore.prefStateFlow(key = ONLY_DIRECT, default = false)
|
||||
override val showIgnored: StateFlow<Boolean> = dataStore.prefStateFlow(key = SHOW_IGNORED, default = false)
|
||||
override val excludeMqtt: StateFlow<Boolean> = dataStore.prefStateFlow(key = EXCLUDE_MQTT, default = false)
|
||||
|
||||
override fun setAppIntroCompleted(completed: Boolean) {
|
||||
dataStore.setPref(key = APP_INTRO_COMPLETED, value = completed)
|
||||
}
|
||||
|
||||
override fun setTheme(value: Int) {
|
||||
dataStore.setPref(key = THEME, value = value)
|
||||
}
|
||||
|
||||
override fun setNodeSort(value: Int) {
|
||||
dataStore.setPref(key = NODE_SORT, value = value)
|
||||
}
|
||||
|
||||
override fun setIncludeUnknown(value: Boolean) {
|
||||
dataStore.setPref(key = INCLUDE_UNKNOWN, value = value)
|
||||
}
|
||||
|
||||
override fun setExcludeInfrastructure(value: Boolean) {
|
||||
dataStore.setPref(key = EXCLUDE_INFRASTRUCTURE, value = value)
|
||||
}
|
||||
|
||||
override fun setOnlyOnline(value: Boolean) {
|
||||
dataStore.setPref(key = ONLY_ONLINE, value = value)
|
||||
}
|
||||
|
||||
override fun setOnlyDirect(value: Boolean) {
|
||||
dataStore.setPref(key = ONLY_DIRECT, value = value)
|
||||
}
|
||||
|
||||
override fun setShowIgnored(value: Boolean) {
|
||||
dataStore.setPref(key = SHOW_IGNORED, value = value)
|
||||
}
|
||||
|
||||
override fun setExcludeMqtt(value: Boolean) {
|
||||
dataStore.setPref(key = EXCLUDE_MQTT, value = value)
|
||||
}
|
||||
|
||||
override fun shouldProvideNodeLocation(nodeNum: Int): StateFlow<Boolean> =
|
||||
dataStore.prefStateFlow(key = booleanPreferencesKey("provide-location-$nodeNum"), default = false)
|
||||
|
||||
override fun setShouldProvideNodeLocation(nodeNum: Int, provide: Boolean) {
|
||||
dataStore.setPref(key = booleanPreferencesKey("provide-location-$nodeNum"), value = provide)
|
||||
}
|
||||
|
||||
private fun <T : Any> DataStore<Preferences>.prefStateFlow(
|
||||
key: Preferences.Key<T>,
|
||||
default: T,
|
||||
started: SharingStarted = SharingStarted.Lazily,
|
||||
): StateFlow<T> = data.map { it[key] ?: default }.stateIn(scope = scope, started = started, initialValue = default)
|
||||
|
||||
private fun <T : Any> DataStore<Preferences>.setPref(key: Preferences.Key<T>, value: T) {
|
||||
scope.launch { edit { it[key] = value } }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val APP_INTRO_COMPLETED = booleanPreferencesKey(KEY_APP_INTRO_COMPLETED)
|
||||
val THEME = intPreferencesKey(KEY_THEME)
|
||||
val LOCALE = stringPreferencesKey(KEY_LOCALE)
|
||||
val NODE_SORT = intPreferencesKey(KEY_NODE_SORT)
|
||||
val INCLUDE_UNKNOWN = booleanPreferencesKey(KEY_INCLUDE_UNKNOWN)
|
||||
val EXCLUDE_INFRASTRUCTURE = booleanPreferencesKey(KEY_EXCLUDE_INFRASTRUCTURE)
|
||||
val ONLY_ONLINE = booleanPreferencesKey(KEY_ONLY_ONLINE)
|
||||
val ONLY_DIRECT = booleanPreferencesKey(KEY_ONLY_DIRECT)
|
||||
val SHOW_IGNORED = booleanPreferencesKey(KEY_SHOW_IGNORED)
|
||||
val EXCLUDE_MQTT = booleanPreferencesKey(KEY_EXCLUDE_MQTT)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,11 @@
|
|||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class SetAppIntroCompletedUseCase constructor(private val uiPreferences: UiPreferences) {
|
||||
open class SetAppIntroCompletedUseCase constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(value: Boolean) {
|
||||
uiPreferences.setAppIntroCompleted(value)
|
||||
uiPrefs.setAppIntroCompleted(value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class SetLocaleUseCase constructor(private val uiPreferences: UiPreferences) {
|
||||
open class SetLocaleUseCase constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(value: String) {
|
||||
uiPreferences.setLocale(value)
|
||||
uiPrefs.setLocale(value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class SetProvideLocationUseCase constructor(private val uiPreferences: UiPreferences) {
|
||||
open class SetProvideLocationUseCase constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(myNodeNum: Int, provideLocation: Boolean) {
|
||||
uiPreferences.setShouldProvideNodeLocation(myNodeNum, provideLocation)
|
||||
uiPrefs.setShouldProvideNodeLocation(myNodeNum, provideLocation)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class SetThemeUseCase constructor(private val uiPreferences: UiPreferences) {
|
||||
open class SetThemeUseCase constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(value: Int) {
|
||||
uiPreferences.setTheme(value)
|
||||
uiPrefs.setTheme(value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,19 +18,19 @@ package org.meshtastic.core.domain.usecase.settings
|
|||
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
class SetAppIntroCompletedUseCaseTest {
|
||||
|
||||
private lateinit var uiPreferencesDataSource: UiPreferencesDataSource
|
||||
private lateinit var uiPrefs: UiPrefs
|
||||
private lateinit var useCase: SetAppIntroCompletedUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
uiPreferencesDataSource = mock(dev.mokkery.MockMode.autofill)
|
||||
useCase = SetAppIntroCompletedUseCase(uiPreferencesDataSource)
|
||||
uiPrefs = mock(dev.mokkery.MockMode.autofill)
|
||||
useCase = SetAppIntroCompletedUseCase(uiPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -39,6 +39,6 @@ class SetAppIntroCompletedUseCaseTest {
|
|||
useCase(true)
|
||||
|
||||
// Assert
|
||||
verify { uiPreferencesDataSource.setAppIntroCompleted(true) }
|
||||
verify { uiPrefs.setAppIntroCompleted(true) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,24 +21,24 @@ import dev.mokkery.every
|
|||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
class SetLocaleUseCaseTest {
|
||||
|
||||
private val uiPreferences: UiPreferences = mock()
|
||||
private val uiPrefs: UiPrefs = mock()
|
||||
private lateinit var useCase: SetLocaleUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
useCase = SetLocaleUseCase(uiPreferences)
|
||||
useCase = SetLocaleUseCase(uiPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke calls setLocale on uiPreferences`() {
|
||||
every { uiPreferences.setLocale(any()) } returns Unit
|
||||
every { uiPrefs.setLocale(any()) } returns Unit
|
||||
useCase("en")
|
||||
verify { uiPreferences.setLocale("en") }
|
||||
verify { uiPrefs.setLocale("en") }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,19 +20,19 @@ import dev.mokkery.MockMode
|
|||
import dev.mokkery.mock
|
||||
import dev.mokkery.verifySuspend
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
class SetProvideLocationUseCaseTest {
|
||||
|
||||
private lateinit var uiPreferences: UiPreferences
|
||||
private lateinit var uiPrefs: UiPrefs
|
||||
private lateinit var useCase: SetProvideLocationUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
uiPreferences = mock(MockMode.autofill)
|
||||
useCase = SetProvideLocationUseCase(uiPreferences)
|
||||
uiPrefs = mock(MockMode.autofill)
|
||||
useCase = SetProvideLocationUseCase(uiPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -41,6 +41,6 @@ class SetProvideLocationUseCaseTest {
|
|||
useCase(123, true)
|
||||
|
||||
// Assert
|
||||
verifySuspend { uiPreferences.setShouldProvideNodeLocation(123, true) }
|
||||
verifySuspend { uiPrefs.setShouldProvideNodeLocation(123, true) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,19 +18,19 @@ package org.meshtastic.core.domain.usecase.settings
|
|||
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
class SetThemeUseCaseTest {
|
||||
|
||||
private lateinit var uiPreferencesDataSource: UiPreferencesDataSource
|
||||
private lateinit var uiPrefs: UiPrefs
|
||||
private lateinit var useCase: SetThemeUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
uiPreferencesDataSource = mock(dev.mokkery.MockMode.autofill)
|
||||
useCase = SetThemeUseCase(uiPreferencesDataSource)
|
||||
uiPrefs = mock(dev.mokkery.MockMode.autofill)
|
||||
useCase = SetThemeUseCase(uiPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -39,6 +39,6 @@ class SetThemeUseCaseTest {
|
|||
useCase(1)
|
||||
|
||||
// Assert
|
||||
verify { uiPreferencesDataSource.setTheme(1) }
|
||||
verify { uiPrefs.setTheme(1) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import androidx.datastore.core.DataStore
|
|||
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
|
||||
|
|
@ -36,6 +38,7 @@ import org.meshtastic.core.prefs.cachedFlow
|
|||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
@Suppress("TooManyFunctions")
|
||||
class UiPrefsImpl(
|
||||
@Named("UiDataStore") private val dataStore: DataStore<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
|
|
@ -45,6 +48,76 @@ class UiPrefsImpl(
|
|||
// Maps nodeNum to a flow for the for the "provide-location-nodeNum" pref
|
||||
private val provideNodeLocationFlows = atomic(persistentMapOf<Int, StateFlow<Boolean>>())
|
||||
|
||||
override val appIntroCompleted: StateFlow<Boolean> =
|
||||
dataStore.data.map { it[KEY_APP_INTRO_COMPLETED] ?: false }.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
|
||||
override fun setAppIntroCompleted(completed: Boolean) {
|
||||
scope.launch { dataStore.edit { it[KEY_APP_INTRO_COMPLETED] = completed } }
|
||||
}
|
||||
|
||||
override val theme: StateFlow<Int> =
|
||||
dataStore.data.map { it[KEY_THEME] ?: -1 }.stateIn(scope, SharingStarted.Lazily, -1)
|
||||
|
||||
override fun setTheme(value: Int) {
|
||||
scope.launch { dataStore.edit { it[KEY_THEME] = value } }
|
||||
}
|
||||
|
||||
override val locale: StateFlow<String> =
|
||||
dataStore.data.map { it[KEY_LOCALE] ?: "" }.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
|
||||
override fun setLocale(languageTag: String) {
|
||||
scope.launch { dataStore.edit { it[KEY_LOCALE] = languageTag } }
|
||||
}
|
||||
|
||||
override val nodeSort: StateFlow<Int> =
|
||||
dataStore.data.map { it[KEY_NODE_SORT] ?: -1 }.stateIn(scope, SharingStarted.Lazily, -1)
|
||||
|
||||
override fun setNodeSort(value: Int) {
|
||||
scope.launch { dataStore.edit { it[KEY_NODE_SORT] = value } }
|
||||
}
|
||||
|
||||
override val includeUnknown: StateFlow<Boolean> =
|
||||
dataStore.data.map { it[KEY_INCLUDE_UNKNOWN] ?: false }.stateIn(scope, SharingStarted.Lazily, false)
|
||||
|
||||
override fun setIncludeUnknown(value: Boolean) {
|
||||
scope.launch { dataStore.edit { it[KEY_INCLUDE_UNKNOWN] = value } }
|
||||
}
|
||||
|
||||
override val excludeInfrastructure: StateFlow<Boolean> =
|
||||
dataStore.data.map { it[KEY_EXCLUDE_INFRASTRUCTURE] ?: false }.stateIn(scope, SharingStarted.Lazily, false)
|
||||
|
||||
override fun setExcludeInfrastructure(value: Boolean) {
|
||||
scope.launch { dataStore.edit { it[KEY_EXCLUDE_INFRASTRUCTURE] = value } }
|
||||
}
|
||||
|
||||
override val onlyOnline: StateFlow<Boolean> =
|
||||
dataStore.data.map { it[KEY_ONLY_ONLINE] ?: false }.stateIn(scope, SharingStarted.Lazily, false)
|
||||
|
||||
override fun setOnlyOnline(value: Boolean) {
|
||||
scope.launch { dataStore.edit { it[KEY_ONLY_ONLINE] = value } }
|
||||
}
|
||||
|
||||
override val onlyDirect: StateFlow<Boolean> =
|
||||
dataStore.data.map { it[KEY_ONLY_DIRECT] ?: false }.stateIn(scope, SharingStarted.Lazily, false)
|
||||
|
||||
override fun setOnlyDirect(value: Boolean) {
|
||||
scope.launch { dataStore.edit { it[KEY_ONLY_DIRECT] = value } }
|
||||
}
|
||||
|
||||
override val showIgnored: StateFlow<Boolean> =
|
||||
dataStore.data.map { it[KEY_SHOW_IGNORED] ?: false }.stateIn(scope, SharingStarted.Lazily, false)
|
||||
|
||||
override fun setShowIgnored(value: Boolean) {
|
||||
scope.launch { dataStore.edit { it[KEY_SHOW_IGNORED] = value } }
|
||||
}
|
||||
|
||||
override val excludeMqtt: StateFlow<Boolean> =
|
||||
dataStore.data.map { it[KEY_EXCLUDE_MQTT] ?: false }.stateIn(scope, SharingStarted.Lazily, false)
|
||||
|
||||
override fun setExcludeMqtt(value: Boolean) {
|
||||
scope.launch { dataStore.edit { it[KEY_EXCLUDE_MQTT] = value } }
|
||||
}
|
||||
|
||||
override val hasShownNotPairedWarning: StateFlow<Boolean> =
|
||||
dataStore.data
|
||||
.map { it[KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF] ?: false }
|
||||
|
|
@ -76,5 +149,16 @@ class UiPrefsImpl(
|
|||
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_APP_INTRO_COMPLETED = booleanPreferencesKey("app_intro_completed")
|
||||
val KEY_THEME = intPreferencesKey("theme")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package org.meshtastic.core.repository
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
|
||||
/** Reactive interface for analytics-related preferences. */
|
||||
interface AnalyticsPrefs {
|
||||
|
|
@ -71,7 +70,48 @@ interface CustomEmojiPrefs {
|
|||
}
|
||||
|
||||
/** Reactive interface for general UI preferences. */
|
||||
@Suppress("TooManyFunctions")
|
||||
interface UiPrefs {
|
||||
val appIntroCompleted: StateFlow<Boolean>
|
||||
|
||||
fun setAppIntroCompleted(completed: Boolean)
|
||||
|
||||
val theme: StateFlow<Int>
|
||||
|
||||
fun setTheme(value: Int)
|
||||
|
||||
val locale: StateFlow<String>
|
||||
|
||||
fun setLocale(languageTag: String)
|
||||
|
||||
val nodeSort: StateFlow<Int>
|
||||
|
||||
fun setNodeSort(value: Int)
|
||||
|
||||
val includeUnknown: StateFlow<Boolean>
|
||||
|
||||
fun setIncludeUnknown(value: Boolean)
|
||||
|
||||
val excludeInfrastructure: StateFlow<Boolean>
|
||||
|
||||
fun setExcludeInfrastructure(value: Boolean)
|
||||
|
||||
val onlyOnline: StateFlow<Boolean>
|
||||
|
||||
fun setOnlyOnline(value: Boolean)
|
||||
|
||||
val onlyDirect: StateFlow<Boolean>
|
||||
|
||||
fun setOnlyDirect(value: Boolean)
|
||||
|
||||
val showIgnored: StateFlow<Boolean>
|
||||
|
||||
fun setShowIgnored(value: Boolean)
|
||||
|
||||
val excludeMqtt: StateFlow<Boolean>
|
||||
|
||||
fun setExcludeMqtt(value: Boolean)
|
||||
|
||||
val hasShownNotPairedWarning: StateFlow<Boolean>
|
||||
|
||||
fun setHasShownNotPairedWarning(shown: Boolean)
|
||||
|
|
@ -181,7 +221,6 @@ interface AppPreferences {
|
|||
val meshLog: MeshLogPrefs
|
||||
val emoji: CustomEmojiPrefs
|
||||
val ui: UiPrefs
|
||||
val uiPrefs: UiPreferences
|
||||
val map: MapPrefs
|
||||
val mapConsent: MapConsentPrefs
|
||||
val mapTileProvider: MapTileProviderPrefs
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import org.koin.core.annotation.KoinViewModel
|
|||
import org.meshtastic.core.common.util.MeshtasticUri
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.MeshActivity
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.RadioController
|
||||
|
|
@ -51,6 +50,7 @@ import org.meshtastic.core.repository.NotificationManager
|
|||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.client_notification
|
||||
import org.meshtastic.core.resources.compromised_keys
|
||||
|
|
@ -76,7 +76,7 @@ class UIViewModel(
|
|||
radioInterfaceService: RadioInterfaceService,
|
||||
meshLogRepository: MeshLogRepository,
|
||||
firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
||||
private val uiPrefs: UiPrefs,
|
||||
private val notificationManager: NotificationManager,
|
||||
packetRepository: PacketRepository,
|
||||
val alertManager: AlertManager,
|
||||
|
|
@ -99,7 +99,7 @@ class UIViewModel(
|
|||
)
|
||||
}
|
||||
|
||||
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
|
||||
val theme: StateFlow<Int> = uiPrefs.theme
|
||||
|
||||
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmware_edition }
|
||||
|
||||
|
|
@ -257,9 +257,9 @@ class UIViewModel(
|
|||
serviceRepository.clearNeighborInfoResponse()
|
||||
}
|
||||
|
||||
val appIntroCompleted: StateFlow<Boolean> = uiPreferencesDataSource.appIntroCompleted
|
||||
val appIntroCompleted: StateFlow<Boolean> = uiPrefs.appIntroCompleted
|
||||
|
||||
fun onAppIntroCompleted() {
|
||||
uiPreferencesDataSource.setAppIntroCompleted(true)
|
||||
uiPrefs.setAppIntroCompleted(true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,10 +63,10 @@ import okio.Path.Companion.toPath
|
|||
import org.jetbrains.skia.Image
|
||||
import org.koin.core.context.startKoin
|
||||
import org.meshtastic.core.common.util.MeshtasticUri
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.navigation.MeshtasticNavSavedStateConfig
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.navigation.TopLevelDestination
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.core.service.MeshServiceOrchestrator
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||
|
|
@ -151,7 +151,7 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
|
|||
onDispose { meshServiceController.stop() }
|
||||
}
|
||||
|
||||
val uiPrefs = remember { koinApp.koin.get<UiPreferencesDataSource>() }
|
||||
val uiPrefs = remember { koinApp.koin.get<UiPrefs>() }
|
||||
val themePref by uiPrefs.theme.collectAsState(initial = -1) // -1 is SYSTEM usually
|
||||
val localePref by uiPrefs.locale.collectAsState(initial = "")
|
||||
|
||||
|
|
|
|||
|
|
@ -37,13 +37,13 @@ import org.junit.Assert.assertTrue
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.feature.map.model.CustomTileProviderConfig
|
||||
import org.meshtastic.feature.map.prefs.map.GoogleMapsPrefs
|
||||
import org.meshtastic.feature.map.repository.CustomTileProviderRepository
|
||||
|
|
@ -61,7 +61,7 @@ class MapViewModelTest {
|
|||
private val radioConfigRepository = mock<RadioConfigRepository>(MockMode.autofill)
|
||||
private val radioController = mock<RadioController>(MockMode.autofill)
|
||||
private val customTileProviderRepository = mock<CustomTileProviderRepository>(MockMode.autofill)
|
||||
private val uiPreferencesDataSource = mock<UiPreferencesDataSource>(MockMode.autofill)
|
||||
private val uiPrefs = mock<UiPrefs>(MockMode.autofill)
|
||||
private val savedStateHandle = SavedStateHandle(mapOf("waypointId" to null))
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
|
@ -89,7 +89,7 @@ class MapViewModelTest {
|
|||
|
||||
every { customTileProviderRepository.getCustomTileProviders() } returns flowOf(emptyList())
|
||||
every { radioConfigRepository.deviceProfileFlow } returns flowOf(mock(MockMode.autofill))
|
||||
every { uiPreferencesDataSource.theme } returns MutableStateFlow(1)
|
||||
every { uiPrefs.theme } returns MutableStateFlow(1)
|
||||
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
|
||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null)
|
||||
every { nodeRepository.myId } returns MutableStateFlow(null)
|
||||
|
|
@ -108,7 +108,7 @@ class MapViewModelTest {
|
|||
radioConfigRepository,
|
||||
radioController,
|
||||
customTileProviderRepository,
|
||||
uiPreferencesDataSource,
|
||||
uiPrefs,
|
||||
savedStateHandle,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,46 +18,46 @@ package org.meshtastic.feature.node.list
|
|||
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class NodeFilterPreferences constructor(private val uiPreferences: UiPreferences) {
|
||||
open val includeUnknown = uiPreferences.includeUnknown
|
||||
open val excludeInfrastructure = uiPreferences.excludeInfrastructure
|
||||
open val onlyOnline = uiPreferences.onlyOnline
|
||||
open val onlyDirect = uiPreferences.onlyDirect
|
||||
open val showIgnored = uiPreferences.showIgnored
|
||||
open val excludeMqtt = uiPreferences.excludeMqtt
|
||||
open class NodeFilterPreferences constructor(private val uiPrefs: UiPrefs) {
|
||||
open val includeUnknown = uiPrefs.includeUnknown
|
||||
open val excludeInfrastructure = uiPrefs.excludeInfrastructure
|
||||
open val onlyOnline = uiPrefs.onlyOnline
|
||||
open val onlyDirect = uiPrefs.onlyDirect
|
||||
open val showIgnored = uiPrefs.showIgnored
|
||||
open val excludeMqtt = uiPrefs.excludeMqtt
|
||||
|
||||
open val nodeSortOption =
|
||||
uiPreferences.nodeSort.map { NodeSortOption.entries.getOrElse(it) { NodeSortOption.VIA_FAVORITE } }
|
||||
uiPrefs.nodeSort.map { NodeSortOption.entries.getOrElse(it) { NodeSortOption.VIA_FAVORITE } }
|
||||
|
||||
open fun setNodeSort(option: NodeSortOption) {
|
||||
uiPreferences.setNodeSort(option.ordinal)
|
||||
uiPrefs.setNodeSort(option.ordinal)
|
||||
}
|
||||
|
||||
open fun toggleIncludeUnknown() {
|
||||
uiPreferences.setIncludeUnknown(!includeUnknown.value)
|
||||
uiPrefs.setIncludeUnknown(!includeUnknown.value)
|
||||
}
|
||||
|
||||
open fun toggleExcludeInfrastructure() {
|
||||
uiPreferences.setExcludeInfrastructure(!excludeInfrastructure.value)
|
||||
uiPrefs.setExcludeInfrastructure(!excludeInfrastructure.value)
|
||||
}
|
||||
|
||||
open fun toggleOnlyOnline() {
|
||||
uiPreferences.setOnlyOnline(!onlyOnline.value)
|
||||
uiPrefs.setOnlyOnline(!onlyOnline.value)
|
||||
}
|
||||
|
||||
open fun toggleOnlyDirect() {
|
||||
uiPreferences.setOnlyDirect(!onlyDirect.value)
|
||||
uiPrefs.setOnlyDirect(!onlyDirect.value)
|
||||
}
|
||||
|
||||
open fun toggleShowIgnored() {
|
||||
uiPreferences.setShowIgnored(!showIgnored.value)
|
||||
uiPrefs.setShowIgnored(!showIgnored.value)
|
||||
}
|
||||
|
||||
open fun toggleExcludeMqtt() {
|
||||
uiPreferences.setExcludeMqtt(!excludeMqtt.value)
|
||||
uiPrefs.setExcludeMqtt(!excludeMqtt.value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
|
||||
|
|
@ -42,6 +41,7 @@ import org.meshtastic.core.domain.usecase.settings.SetNotificationSettingsUseCas
|
|||
import org.meshtastic.core.domain.usecase.settings.SetProvideLocationUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.repository.AppPreferences
|
||||
import org.meshtastic.core.repository.FileService
|
||||
import org.meshtastic.core.repository.MeshLogPrefs
|
||||
import org.meshtastic.core.repository.MeshLogRepository
|
||||
|
|
@ -63,7 +63,6 @@ class SettingsViewModelTest {
|
|||
private lateinit var radioController: FakeRadioController
|
||||
private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill)
|
||||
private val uiPrefs: UiPrefs = mock(MockMode.autofill)
|
||||
private val uiPreferences: UiPreferences = mock(MockMode.autofill)
|
||||
private val buildConfigProvider: BuildConfigProvider = mock(MockMode.autofill)
|
||||
private val databaseManager: DatabaseManager = mock(MockMode.autofill)
|
||||
private val meshLogPrefs: MeshLogPrefs = mock(MockMode.autofill)
|
||||
|
|
@ -88,10 +87,14 @@ class SettingsViewModelTest {
|
|||
val isOtaCapableUseCase: IsOtaCapableUseCase = mock(MockMode.autofill)
|
||||
every { isOtaCapableUseCase() } returns flowOf(true)
|
||||
|
||||
val setThemeUseCase = SetThemeUseCase(uiPreferences)
|
||||
val setLocaleUseCase = SetLocaleUseCase(uiPreferences)
|
||||
val setAppIntroCompletedUseCase = SetAppIntroCompletedUseCase(uiPreferences)
|
||||
val setProvideLocationUseCase = SetProvideLocationUseCase(uiPreferences)
|
||||
val setThemeUseCase = SetThemeUseCase(uiPrefs)
|
||||
val setLocaleUseCase = SetLocaleUseCase(uiPrefs)
|
||||
val setAppIntroCompletedUseCase = SetAppIntroCompletedUseCase(uiPrefs)
|
||||
|
||||
val appPreferences: AppPreferences = mock(MockMode.autofill)
|
||||
every { appPreferences.ui } returns uiPrefs
|
||||
val setProvideLocationUseCase = SetProvideLocationUseCase(uiPrefs)
|
||||
|
||||
val setDatabaseCacheLimitUseCase = SetDatabaseCacheLimitUseCase(databaseManager)
|
||||
val setMeshLogSettingsUseCase = SetMeshLogSettingsUseCase(meshLogRepository, meshLogPrefs)
|
||||
val setNotificationSettingsUseCase = SetNotificationSettingsUseCase(notificationPrefs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue