Separate User Preferences

This commit is contained in:
Sad Ellie 2023-05-18 23:43:08 +03:00
parent 1b61b4bd85
commit 86dce329ac
20 changed files with 373 additions and 309 deletions

View File

@ -120,8 +120,7 @@ dependencies {
implementation(project(mapOf("path" to ":feature:calculator")))
implementation(project(mapOf("path" to ":feature:settings")))
implementation(project(mapOf("path" to ":feature:unitslist")))
implementation(project(mapOf("path" to ":feature:datedifference")))
implementation(project(mapOf("path" to ":data:units")))
implementation(project(mapOf("path" to ":feature:datedifference")))
implementation(project(mapOf("path" to ":data:model")))
implementation(project(mapOf("path" to ":data:userprefs")))
implementation(project(mapOf("path" to ":core:ui")))

View File

@ -40,13 +40,11 @@ internal class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val userPrefsFlow = userPrefsRepository.userPreferencesFlow
setContent {
val userPrefs = userPrefsFlow
val uiPrefs = userPrefsRepository.uiPreferencesFlow
.collectAsStateWithLifecycle(null).value
if (userPrefs != null) UnittoApp(userPrefs)
if (uiPrefs != null) UnittoApp(uiPrefs)
}
}

View File

@ -44,23 +44,23 @@ import com.sadellie.unitto.core.ui.model.DrawerItems
import com.sadellie.unitto.core.ui.theme.AppTypography
import com.sadellie.unitto.core.ui.theme.DarkThemeColors
import com.sadellie.unitto.core.ui.theme.LightThemeColors
import com.sadellie.unitto.data.userprefs.UserPreferences
import com.sadellie.unitto.data.userprefs.UIPreferences
import io.github.sadellie.themmo.Themmo
import io.github.sadellie.themmo.rememberThemmoController
import kotlinx.coroutines.launch
@Composable
internal fun UnittoApp(userPrefs: UserPreferences) {
internal fun UnittoApp(uiPrefs: UIPreferences) {
val themmoController = rememberThemmoController(
lightColorScheme = LightThemeColors,
darkColorScheme = DarkThemeColors,
// Anything below will not be called if theming mode is still loading from DataStore
themingMode = userPrefs.themingMode,
dynamicThemeEnabled = userPrefs.enableDynamicTheme,
amoledThemeEnabled = userPrefs.enableAmoledTheme,
customColor = userPrefs.customColor,
monetMode = userPrefs.monetMode
themingMode = uiPrefs.themingMode,
dynamicThemeEnabled = uiPrefs.enableDynamicTheme,
amoledThemeEnabled = uiPrefs.enableAmoledTheme,
customColor = uiPrefs.customColor,
monetMode = uiPrefs.monetMode
)
val navController = rememberNavController()
val sysUiController = rememberSystemUiController()
@ -131,7 +131,7 @@ internal fun UnittoApp(userPrefs: UserPreferences) {
UnittoNavigation(
navController = navController,
themmoController = it,
startDestination = userPrefs.startingScreen,
startDestination = uiPrefs.startingScreen,
openDrawer = { drawerScope.launch { drawerState.open() } }
)
}

View File

@ -30,7 +30,6 @@ import com.sadellie.unitto.feature.calculator.navigation.calculatorScreen
import com.sadellie.unitto.feature.converter.ConverterViewModel
import com.sadellie.unitto.feature.converter.navigation.converterScreen
import com.sadellie.unitto.feature.datedifference.navigation.dateDifferenceScreen
import com.sadellie.unitto.feature.settings.SettingsViewModel
import com.sadellie.unitto.feature.settings.navigation.navigateToSettings
import com.sadellie.unitto.feature.settings.navigation.navigateToUnitGroups
import com.sadellie.unitto.feature.settings.navigation.settingGraph
@ -50,7 +49,6 @@ internal fun UnittoNavigation(
) {
val converterViewModel: ConverterViewModel = hiltViewModel()
val unitsListViewModel: UnitsListViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
NavHost(
navController = navController,
@ -90,7 +88,6 @@ internal fun UnittoNavigation(
)
settingGraph(
settingsViewModel = settingsViewModel,
themmoController = themmoController,
navController = navController,
menuButtonClick = openDrawer

View File

@ -1,115 +0,0 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022-2023 Elshan Agaev
*
* 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 com.sadellie.unitto.data.unitgroups
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
import javax.inject.Singleton
/**
* Repository that holds information about shown and hidden [UnitGroup]s and provides methods to
* show/hide [UnitGroup]s.
*/
@Singleton
class UnitGroupsRepository @Inject constructor() {
/**
* Mutex is need needed because we work with flow (sync stuff).
*/
private val mutex = Mutex()
/**
* Currently shown [UnitGroup]s.
*/
var shownUnitGroups = MutableStateFlow(listOf<UnitGroup>())
private set
/**
* Currently hidden [UnitGroup]s.
*/
var hiddenUnitGroups = MutableStateFlow(listOf<UnitGroup>())
private set
/**
* Sets [shownUnitGroups] and updates [hiddenUnitGroups] as a side effect. [hiddenUnitGroups] is
* everything from [ALL_UNIT_GROUPS] that was not in [shownUnitGroups].
*
* @param list List of [UnitGroup]s that need to be shown.
*/
suspend fun updateShownGroups(list: List<UnitGroup>) {
mutex.withLock {
shownUnitGroups.value = list
hiddenUnitGroups.value = ALL_UNIT_GROUPS - list.toSet()
}
}
/**
* Moves [UnitGroup] from [shownUnitGroups] to [hiddenUnitGroups]
*
* @param unitGroup [UnitGroup] to hide.
*/
suspend fun markUnitGroupAsHidden(unitGroup: UnitGroup) {
mutex.withLock {
shownUnitGroups.value = shownUnitGroups.value - unitGroup
// Newly hidden unit will appear at the top of the list
hiddenUnitGroups.value = listOf(unitGroup) + hiddenUnitGroups.value
}
}
/**
* Moves [UnitGroup] from [hiddenUnitGroups] to [shownUnitGroups]
*
* @param unitGroup [UnitGroup] to show.
*/
suspend fun markUnitGroupAsShown(unitGroup: UnitGroup) {
mutex.withLock {
hiddenUnitGroups.value = hiddenUnitGroups.value - unitGroup
shownUnitGroups.value = shownUnitGroups.value + unitGroup
}
}
/**
* Moves [UnitGroup] in [shownUnitGroups] from one index to another (reorder).
*
* @param from Position from which we need to move from
* @param to Position where to put [UnitGroup]
*/
suspend fun moveShownUnitGroups(from: ItemPosition, to: ItemPosition) {
mutex.withLock {
shownUnitGroups.value = shownUnitGroups.value.toMutableList().apply {
val initialIndex = shownUnitGroups.value.indexOfFirst { it == from.key }
/**
* No such item. Happens when dragging item and clicking "remove" while item is
* still being dragged.
*/
if (initialIndex == -1) return
add(
shownUnitGroups.value.indexOfFirst { it == to.key },
removeAt(initialIndex)
)
}
}
}
}

View File

@ -39,6 +39,7 @@ import io.github.sadellie.themmo.MonetMode
import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import java.io.IOException
import javax.inject.Inject
@ -84,6 +85,29 @@ data class UserPreferences(
val unitConverterSorting: UnitsListSorting = UnitsListSorting.USAGE,
)
data class UIPreferences(
val themingMode: ThemingMode = ThemingMode.AUTO,
val enableDynamicTheme: Boolean = false,
val enableAmoledTheme: Boolean = false,
val customColor: Color = Color.Unspecified,
val monetMode: MonetMode = MonetMode.TONAL_SPOT,
val startingScreen: String = TopLevelDestinations.Converter.route,
)
data class MainPreferences(
val digitsPrecision: Int = 3,
val separator: Int = Separator.SPACES,
val outputFormat: Int = OutputFormat.PLAIN,
val latestLeftSideUnit: String = MyUnitIDS.kilometer,
val latestRightSideUnit: String = MyUnitIDS.mile,
val shownUnitGroups: List<UnitGroup> = ALL_UNIT_GROUPS,
val enableVibrations: Boolean = true,
val radianMode: Boolean = true,
val unitConverterFavoritesOnly: Boolean = false,
val unitConverterFormatTime: Boolean = false,
val unitConverterSorting: UnitsListSorting = UnitsListSorting.USAGE,
)
/**
* Repository that works with DataStore
*/
@ -112,7 +136,7 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
val UNIT_CONVERTER_SORTING = stringPreferencesKey("UNIT_CONVERTER_SORTING_PREF_KEY")
}
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
val uiPreferencesFlow: Flow<UIPreferences> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
@ -128,6 +152,27 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
val customColor: Color = preferences[PrefsKeys.CUSTOM_COLOR]?.let { Color(it.toULong()) } ?: Color.Unspecified
val monetMode: MonetMode = preferences[PrefsKeys.MONET_MODE]?.let { MonetMode.valueOf(it) }
?: MonetMode.TONAL_SPOT
val startingScreen: String = preferences[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.Converter.route
UIPreferences(
themingMode = themingMode,
enableDynamicTheme = enableDynamicTheme,
enableAmoledTheme = enableAmoledTheme,
customColor = customColor,
monetMode = monetMode,
startingScreen = startingScreen
)
}
val mainPreferencesFlow: Flow<MainPreferences> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val digitsPrecision: Int = preferences[PrefsKeys.DIGITS_PRECISION] ?: 3
val separator: Int = preferences[PrefsKeys.SEPARATOR] ?: Separator.SPACES
val outputFormat: Int = preferences[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN
@ -147,19 +192,12 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
} ?: ALL_UNIT_GROUPS
val enableVibrations: Boolean = preferences[PrefsKeys.ENABLE_VIBRATIONS] ?: true
val enableToolsExperiment: Boolean = preferences[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false
val startingScreen: String = preferences[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.Converter.route
val radianMode: Boolean = preferences[PrefsKeys.RADIAN_MODE] ?: true
val unitConverterFavoritesOnly: Boolean = preferences[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] ?: false
val unitConverterFormatTime: Boolean = preferences[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] ?: false
val unitConverterSorting: UnitsListSorting = preferences[PrefsKeys.UNIT_CONVERTER_SORTING]?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE
UserPreferences(
themingMode = themingMode,
enableDynamicTheme = enableDynamicTheme,
enableAmoledTheme = enableAmoledTheme,
customColor = customColor,
monetMode = monetMode,
MainPreferences(
digitsPrecision = digitsPrecision,
separator = separator,
outputFormat = outputFormat,
@ -167,8 +205,6 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
latestRightSideUnit = latestRightSideUnit,
shownUnitGroups = shownUnitGroups,
enableVibrations = enableVibrations,
enableToolsExperiment = enableToolsExperiment,
startingScreen = startingScreen,
radianMode = radianMode,
unitConverterFavoritesOnly = unitConverterFavoritesOnly,
unitConverterFormatTime = unitConverterFormatTime,
@ -176,6 +212,31 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
)
}
val allPreferencesFlow = combine(
mainPreferencesFlow, uiPreferencesFlow
) { main, ui ->
return@combine UserPreferences(
themingMode = ui.themingMode,
enableDynamicTheme = ui.enableDynamicTheme,
enableAmoledTheme = ui.enableAmoledTheme,
customColor = ui.customColor,
monetMode = ui.monetMode,
digitsPrecision = main.digitsPrecision,
separator = main.separator,
outputFormat = main.outputFormat,
latestLeftSideUnit = main.latestLeftSideUnit,
latestRightSideUnit = main.latestRightSideUnit,
shownUnitGroups = main.shownUnitGroups,
enableVibrations = main.enableVibrations,
enableToolsExperiment = false,
startingScreen = ui.startingScreen,
radianMode = main.radianMode,
unitConverterFavoritesOnly = main.unitConverterFavoritesOnly,
unitConverterFormatTime = main.unitConverterFormatTime,
unitConverterSorting = main.unitConverterSorting,
)
}
/**
* Update precision preference in DataStore
*

View File

@ -29,7 +29,6 @@ android {
dependencies {
testImplementation(libs.junit)
implementation(libs.com.github.sadellie.themmo)
implementation(project(mapOf("path" to ":data:common")))
implementation(project(mapOf("path" to ":data:userprefs")))

View File

@ -31,7 +31,7 @@ import com.sadellie.unitto.data.common.isExpression
import com.sadellie.unitto.data.common.setMinimumRequiredScale
import com.sadellie.unitto.data.common.toStringWith
import com.sadellie.unitto.data.common.trimZeros
import com.sadellie.unitto.data.userprefs.UserPreferences
import com.sadellie.unitto.data.userprefs.MainPreferences
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.sadellie.evaluatto.Expression
@ -54,11 +54,11 @@ internal class CalculatorViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository,
private val calculatorHistoryRepository: CalculatorHistoryRepository,
) : ViewModel() {
private val _userPrefs: StateFlow<UserPreferences> =
userPrefsRepository.userPreferencesFlow.stateIn(
private val _userPrefs: StateFlow<MainPreferences> =
userPrefsRepository.mainPreferencesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
UserPreferences()
MainPreferences()
)
private val _input: MutableStateFlow<TextFieldValue> = MutableStateFlow(TextFieldValue())

View File

@ -36,7 +36,6 @@ dependencies {
kapt(libs.androidx.room.compiler)
testImplementation(libs.androidx.datastore)
implementation(libs.com.github.sadellie.themmo)
implementation(libs.com.squareup.moshi)
implementation(libs.com.squareup.retrofit2)

View File

@ -39,7 +39,7 @@ import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.combine
import com.sadellie.unitto.data.units.remote.CurrencyApi
import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse
import com.sadellie.unitto.data.userprefs.UserPreferences
import com.sadellie.unitto.data.userprefs.MainPreferences
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.sadellie.evaluatto.Expression
@ -65,10 +65,10 @@ class ConverterViewModel @Inject constructor(
private val allUnitsRepository: AllUnitsRepository
) : ViewModel() {
private val _userPrefs = userPrefsRepository.userPreferencesFlow.stateIn(
private val _userPrefs = userPrefsRepository.mainPreferencesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
UserPreferences()
MainPreferences()
)
/**
@ -334,7 +334,7 @@ class ConverterViewModel @Inject constructor(
private fun loadInitialUnitPair() {
viewModelScope.launch(Dispatchers.IO) {
val initialUserPrefs = userPrefsRepository.userPreferencesFlow.first()
val initialUserPrefs = userPrefsRepository.mainPreferencesFlow.first()
// First we load latest pair of units
_unitFrom.update {

View File

@ -43,6 +43,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.BuildConfig
import com.sadellie.unitto.core.base.R
@ -54,7 +55,7 @@ import com.sadellie.unitto.core.ui.openLink
internal fun AboutScreen(
navigateUpAction: () -> Unit,
navigateToThirdParty: () -> Unit,
viewModel: SettingsViewModel
viewModel: SettingsViewModel = hiltViewModel()
) {
val mContext = LocalContext.current
val userPrefs = viewModel.userPrefs.collectAsStateWithLifecycle()

View File

@ -43,6 +43,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.BuildConfig
import com.sadellie.unitto.core.base.OUTPUT_FORMAT
@ -63,7 +64,7 @@ import com.sadellie.unitto.feature.settings.navigation.unitsGroupRoute
@Composable
internal fun SettingsScreen(
viewModel: SettingsViewModel,
viewModel: SettingsViewModel = hiltViewModel(),
menuButtonClick: () -> Unit,
navControllerAction: (String) -> Unit
) {

View File

@ -18,80 +18,27 @@
package com.sadellie.unitto.feature.settings
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.data.unitgroups.UnitGroupsRepository
import com.sadellie.unitto.data.userprefs.UserPreferences
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.sadellie.themmo.MonetMode
import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository,
private val unitGroupsRepository: UnitGroupsRepository,
) : ViewModel() {
val userPrefs = userPrefsRepository.userPreferencesFlow
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000),
val userPrefs = userPrefsRepository.allPreferencesFlow
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
UserPreferences()
)
val shownUnitGroups = unitGroupsRepository.shownUnitGroups
val hiddenUnitGroups = unitGroupsRepository.hiddenUnitGroups
/**
* @see UserPreferencesRepository.updateThemingMode
*/
fun updateThemingMode(themingMode: ThemingMode) {
viewModelScope.launch {
userPrefsRepository.updateThemingMode(themingMode)
}
}
/**
* @see UserPreferencesRepository.updateDynamicTheme
*/
fun updateDynamicTheme(enabled: Boolean) {
viewModelScope.launch {
userPrefsRepository.updateDynamicTheme(enabled)
}
}
/**
* @see UserPreferencesRepository.updateAmoledTheme
*/
fun updateAmoledTheme(enabled: Boolean) {
viewModelScope.launch {
userPrefsRepository.updateAmoledTheme(enabled)
}
}
/**
* @see UserPreferencesRepository.updateCustomColor
*/
fun updateCustomColor(color: Color) {
viewModelScope.launch {
userPrefsRepository.updateCustomColor(color)
}
}
/**
* @see UserPreferencesRepository.updateMonetMode
*/
fun updateMonetMode(monetMode: MonetMode) {
viewModelScope.launch {
userPrefsRepository.updateMonetMode(monetMode)
}
}
/**
* @see UserPreferencesRepository.updateDigitsPrecision
@ -138,46 +85,6 @@ class SettingsViewModel @Inject constructor(
}
}
/**
* @see UnitGroupsRepository.markUnitGroupAsHidden
* @see UserPreferencesRepository.updateShownUnitGroups
*/
fun hideUnitGroup(unitGroup: UnitGroup) {
viewModelScope.launch {
unitGroupsRepository.markUnitGroupAsHidden(unitGroup)
userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
}
}
/**
* @see UnitGroupsRepository.markUnitGroupAsShown
* @see UserPreferencesRepository.updateShownUnitGroups
*/
fun returnUnitGroup(unitGroup: UnitGroup) {
viewModelScope.launch {
unitGroupsRepository.markUnitGroupAsShown(unitGroup)
userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
}
}
/**
* @see UnitGroupsRepository.moveShownUnitGroups
*/
fun onMove(from: ItemPosition, to: ItemPosition) {
viewModelScope.launch {
unitGroupsRepository.moveShownUnitGroups(from, to)
}
}
/**
* @see UserPreferencesRepository.updateShownUnitGroups
*/
fun onDragEnd() {
viewModelScope.launch {
userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
}
}
/**
* @see UserPreferencesRepository.updateToolsExperiment
*/
@ -204,20 +111,4 @@ class SettingsViewModel @Inject constructor(
userPrefsRepository.updateUnitConverterSorting(sorting)
}
}
/**
* Prevent from dragging over non-draggable items (headers and hidden)
*
* @param pos Position we are dragging over.
* @return True if can drag over given item.
*/
fun canDragOver(pos: ItemPosition) = shownUnitGroups.value.any { it == pos.key }
init {
viewModelScope.launch {
unitGroupsRepository.updateShownGroups(
userPrefsRepository.userPreferencesFlow.first().shownUnitGroups
)
}
}
}

View File

@ -27,10 +27,9 @@ import androidx.navigation.compose.navigation
import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.feature.settings.AboutScreen
import com.sadellie.unitto.feature.settings.SettingsScreen
import com.sadellie.unitto.feature.settings.SettingsViewModel
import com.sadellie.unitto.feature.settings.ThemesRoute
import com.sadellie.unitto.feature.settings.themes.ThemesRoute
import com.sadellie.unitto.feature.settings.ThirdPartyLicensesScreen
import com.sadellie.unitto.feature.settings.UnitGroupsScreen
import com.sadellie.unitto.feature.settings.unitgroups.UnitGroupsScreen
import io.github.sadellie.themmo.ThemmoController
private val settingsGraph: String by lazy { TopLevelDestinations.Settings.route }
@ -49,7 +48,6 @@ fun NavController.navigateToUnitGroups() {
}
fun NavGraphBuilder.settingGraph(
settingsViewModel: SettingsViewModel,
themmoController: ThemmoController,
navController: NavHostController,
menuButtonClick: () -> Unit
@ -57,37 +55,34 @@ fun NavGraphBuilder.settingGraph(
navigation(settingsRoute, settingsGraph) {
composable(settingsRoute) {
SettingsScreen(
viewModel = settingsViewModel,
menuButtonClick = menuButtonClick
) { route -> navController.navigate(route) }
menuButtonClick = menuButtonClick,
navControllerAction = navController::navigate
)
}
composable(themesRoute) {
ThemesRoute(
navigateUpAction = { navController.navigateUp() },
navigateUpAction = navController::navigateUp,
themmoController = themmoController,
viewModel = settingsViewModel
)
}
composable(thirdPartyRoute) {
ThirdPartyLicensesScreen(
navigateUpAction = { navController.navigateUp() }
navigateUpAction = navController::navigateUp,
)
}
composable(aboutRoute) {
AboutScreen(
navigateUpAction = { navController.navigateUp() },
navigateUpAction = navController::navigateUp,
navigateToThirdParty = { navController.navigate(thirdPartyRoute) },
viewModel = settingsViewModel
)
}
composable(unitsGroupRoute) {
UnitGroupsScreen(
viewModel = settingsViewModel,
navigateUpAction = { navController.navigateUp() }
navigateUpAction = navController::navigateUp,
)
}
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.settings
package com.sadellie.unitto.feature.settings.themes
import android.os.Build
import androidx.compose.animation.AnimatedVisibility
@ -49,6 +49,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.Header
import com.sadellie.unitto.core.ui.common.NavigateUpButton
@ -82,7 +83,7 @@ private val colorSchemes: List<Color> by lazy {
internal fun ThemesRoute(
navigateUpAction: () -> Unit = {},
themmoController: ThemmoController,
viewModel: SettingsViewModel
viewModel: ThemesViewModel = hiltViewModel()
) {
ThemesScreen(
navigateUpAction = navigateUpAction,

View File

@ -0,0 +1,80 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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 com.sadellie.unitto.feature.settings.themes
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.sadellie.themmo.MonetMode
import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ThemesViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository
) : ViewModel() {
/**
* @see UserPreferencesRepository.updateThemingMode
*/
fun updateThemingMode(themingMode: ThemingMode) {
viewModelScope.launch {
userPrefsRepository.updateThemingMode(themingMode)
}
}
/**
* @see UserPreferencesRepository.updateDynamicTheme
*/
fun updateDynamicTheme(enabled: Boolean) {
viewModelScope.launch {
userPrefsRepository.updateDynamicTheme(enabled)
}
}
/**
* @see UserPreferencesRepository.updateAmoledTheme
*/
fun updateAmoledTheme(enabled: Boolean) {
viewModelScope.launch {
userPrefsRepository.updateAmoledTheme(enabled)
}
}
/**
* @see UserPreferencesRepository.updateCustomColor
*/
fun updateCustomColor(color: Color) {
viewModelScope.launch {
userPrefsRepository.updateCustomColor(color)
}
}
/**
* @see UserPreferencesRepository.updateMonetMode
*/
fun updateMonetMode(monetMode: MonetMode) {
viewModelScope.launch {
userPrefsRepository.updateMonetMode(monetMode)
}
}
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.settings
package com.sadellie.unitto.feature.settings.unitgroups
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
@ -40,13 +40,14 @@ import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.Header
import com.sadellie.unitto.core.ui.common.NavigateUpButton
@ -59,21 +60,20 @@ import org.burnoutcrew.reorderable.reorderable
@Composable
internal fun UnitGroupsScreen(
viewModel: SettingsViewModel,
viewModel: UnitGroupsViewModel = hiltViewModel(),
navigateUpAction: () -> Unit
) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
UnittoScreenWithLargeTopBar(
title = stringResource(R.string.unit_groups_setting),
navigationIcon = { NavigateUpButton(navigateUpAction) }
) { paddingValues ->
val shownUnits = viewModel.shownUnitGroups.collectAsState()
val hiddenUnits = viewModel.hiddenUnitGroups.collectAsState()
val state = rememberReorderableLazyListState(
onMove = viewModel::onMove,
onMove = viewModel::moveShownUnitGroups,
canDragOver = { from, _ -> viewModel.canDragOver(from) },
onDragEnd = { _, _ -> viewModel.onDragEnd() }
onDragEnd = { _, _ -> viewModel.saveShownUnitGroups() }
)
LazyColumn(
@ -89,7 +89,7 @@ internal fun UnitGroupsScreen(
)
}
items(shownUnits.value, { it }) { item ->
items(uiState.value.shownGroups, { it }) { item ->
ReorderableItem(state, key = item) { isDragging ->
val transition = updateTransition(isDragging, label = "draggedTransition")
val background by transition.animateColor(label = "background") {
@ -104,7 +104,7 @@ internal fun UnitGroupsScreen(
modifier = Modifier
.padding(horizontal = itemPadding)
.clip(CircleShape)
.clickable { viewModel.hideUnitGroup(item) }
.clickable { viewModel.markUnitGroupAsHidden(item) }
.detectReorderAfterLongPress(state),
colors = ListItemDefaults.colors(
containerColor = background
@ -117,7 +117,7 @@ internal fun UnitGroupsScreen(
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(false),
onClick = { viewModel.hideUnitGroup(item) }
onClick = { viewModel.markUnitGroupAsHidden(item) }
)
)
},
@ -147,11 +147,11 @@ internal fun UnitGroupsScreen(
)
}
items(hiddenUnits.value, { it }) {
items(uiState.value.hiddenGroups, { it }) {
ListItem(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.clickable { viewModel.returnUnitGroup(it) }
.clickable { viewModel.markUnitGroupAsShown(it) }
.animateItemPlacement(),
headlineContent = { Text(stringResource(it.res)) },
trailingContent = {
@ -162,7 +162,7 @@ internal fun UnitGroupsScreen(
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(false),
onClick = { viewModel.returnUnitGroup(it) }
onClick = { viewModel.markUnitGroupAsShown(it) }
)
)
}

View File

@ -0,0 +1,26 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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 com.sadellie.unitto.feature.settings.unitgroups
import com.sadellie.unitto.data.model.UnitGroup
data class UnitGroupsUIState(
val shownGroups: List<UnitGroup>,
val hiddenGroups: List<UnitGroup>
)

View File

@ -0,0 +1,135 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* 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 com.sadellie.unitto.feature.settings.unitgroups
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
@HiltViewModel
class UnitGroupsViewModel @Inject constructor(
private val userPreferencesRepository: UserPreferencesRepository,
) : ViewModel() {
private var mutex: Mutex = Mutex()
/**
* Currently shown [UnitGroup]s.
*/
private val _shownUnitGroups = MutableStateFlow(listOf<UnitGroup>())
/**
* Currently hidden [UnitGroup]s.
*/
private val _hiddenUnitGroups = MutableStateFlow(listOf<UnitGroup>())
init {
viewModelScope.launch {
val shown = userPreferencesRepository.mainPreferencesFlow.first().shownUnitGroups
mutex.withLock {
_shownUnitGroups.update { shown }
_hiddenUnitGroups.update { ALL_UNIT_GROUPS - shown.toSet() }
}
}
}
val uiState = combine(_shownUnitGroups, _hiddenUnitGroups) { shown, hidden ->
return@combine UnitGroupsUIState(
shownGroups = shown,
hiddenGroups = hidden
)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
UnitGroupsUIState(emptyList(), emptyList())
)
/**
* Moves [UnitGroup] from [_shownUnitGroups] to [_hiddenUnitGroups]
*
* @param unitGroup [UnitGroup] to hide.
*/
fun markUnitGroupAsHidden(unitGroup: UnitGroup) = viewModelScope.launch {
mutex.withLock {
_shownUnitGroups.update { it - unitGroup }
// Newly hidden unit will appear at the top of the list
_hiddenUnitGroups.update { listOf(unitGroup) + it }
}
}
/**
* Moves [UnitGroup] from [_hiddenUnitGroups] to [_shownUnitGroups]
*
* @param unitGroup [UnitGroup] to show.
*/
fun markUnitGroupAsShown(unitGroup: UnitGroup) = viewModelScope.launch {
mutex.withLock {
_hiddenUnitGroups.update { it - unitGroup }
_shownUnitGroups.update { it + unitGroup }
}
}
/**
* Moves [UnitGroup] in [_shownUnitGroups] from one index to another (reorder).
*
* @param from Position from which we need to move from
* @param to Position where to put [UnitGroup]
*/
fun moveShownUnitGroups(from: ItemPosition, to: ItemPosition) = viewModelScope.launch {
mutex.withLock {
_shownUnitGroups.update { shown ->
shown.toMutableList().apply {
val initialIndex = shown.indexOfFirst { it == from.key }
/**
* No such item. Happens when dragging item and clicking "remove" while item is
* still being dragged.
*/
if (initialIndex == -1) return@launch
add(
shown.indexOfFirst { it == to.key },
removeAt(initialIndex)
)
}
}
}
}
fun canDragOver(pos: ItemPosition) = uiState.value.shownGroups.any { it == pos.key }
fun saveShownUnitGroups() = viewModelScope.launch {
userPreferencesRepository.updateShownUnitGroups(
uiState.value.shownGroups
)
}
}

View File

@ -26,9 +26,8 @@ import com.sadellie.unitto.data.database.UnitsEntity
import com.sadellie.unitto.data.database.UnitsRepository
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.unitgroups.UnitGroupsRepository
import com.sadellie.unitto.data.units.AllUnitsRepository
import com.sadellie.unitto.data.userprefs.UserPreferences
import com.sadellie.unitto.data.userprefs.MainPreferences
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@ -48,33 +47,30 @@ class UnitsListViewModel @Inject constructor(
private val allUnitsRepository: AllUnitsRepository,
private val mContext: Application,
private val userPrefsRepository: UserPreferencesRepository,
unitGroupsRepository: UnitGroupsRepository,
) : ViewModel() {
private val _userPrefs: StateFlow<UserPreferences> =
userPrefsRepository.userPreferencesFlow.stateIn(
private val _userPrefs: StateFlow<MainPreferences> =
userPrefsRepository.mainPreferencesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
UserPreferences()
MainPreferences()
)
private val _unitsToShow = MutableStateFlow(emptyMap<UnitGroup, List<AbstractUnit>>())
private val _searchQuery = MutableStateFlow("")
private val _chosenUnitGroup: MutableStateFlow<UnitGroup?> = MutableStateFlow(null)
private val _shownUnitGroups = unitGroupsRepository.shownUnitGroups
val mainFlow = combine(
_userPrefs,
_unitsToShow,
_searchQuery,
_chosenUnitGroup,
_shownUnitGroups,
) { userPrefs, unitsToShow, searchQuery, chosenUnitGroup, shownUnitGroups ->
) { userPrefs, unitsToShow, searchQuery, chosenUnitGroup ->
return@combine SecondScreenUIState(
favoritesOnly = userPrefs.unitConverterFavoritesOnly,
unitsToShow = unitsToShow,
searchQuery = searchQuery,
chosenUnitGroup = chosenUnitGroup,
shownUnitGroups = shownUnitGroups,
shownUnitGroups = userPrefs.shownUnitGroups,
formatterSymbols = AllFormatterSymbols.getById(userPrefs.separator)
)
}
@ -136,7 +132,7 @@ class UnitsListViewModel @Inject constructor(
chosenUnitGroup = _chosenUnitGroup.value,
favoritesOnly = _userPrefs.value.unitConverterFavoritesOnly,
searchQuery = _searchQuery.value,
allUnitsGroups = _shownUnitGroups.value,
allUnitsGroups = _userPrefs.value.shownUnitGroups,
sorting = _userPrefs.value.unitConverterSorting
)