diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 89c279c8..24891849 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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"))) diff --git a/app/src/main/java/com/sadellie/unitto/MainActivity.kt b/app/src/main/java/com/sadellie/unitto/MainActivity.kt index 490a109c..b68879ed 100644 --- a/app/src/main/java/com/sadellie/unitto/MainActivity.kt +++ b/app/src/main/java/com/sadellie/unitto/MainActivity.kt @@ -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) } } diff --git a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt index c06d4c74..59ce7f13 100644 --- a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt +++ b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt @@ -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() } } ) } diff --git a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt index c11bec89..8e5fcf8d 100644 --- a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt +++ b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt @@ -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 diff --git a/data/unitgroups/src/main/java/com/sadellie/unitto/data/unitgroups/UnitGroupsRepository.kt b/data/unitgroups/src/main/java/com/sadellie/unitto/data/unitgroups/UnitGroupsRepository.kt deleted file mode 100644 index 44fbe968..00000000 --- a/data/unitgroups/src/main/java/com/sadellie/unitto/data/unitgroups/UnitGroupsRepository.kt +++ /dev/null @@ -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 . - */ - -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()) - private set - - /** - * Currently hidden [UnitGroup]s. - */ - var hiddenUnitGroups = MutableStateFlow(listOf()) - 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) { - 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) - ) - } - } - } -} diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt index a2c46562..a137de91 100644 --- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt +++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt @@ -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 = 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 = dataStore.data + val uiPreferencesFlow: Flow = 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 = 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 * diff --git a/feature/calculator/build.gradle.kts b/feature/calculator/build.gradle.kts index b69ba212..d0055baf 100644 --- a/feature/calculator/build.gradle.kts +++ b/feature/calculator/build.gradle.kts @@ -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"))) diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt index 79c15e10..80bc810f 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt @@ -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 = - userPrefsRepository.userPreferencesFlow.stateIn( + private val _userPrefs: StateFlow = + userPrefsRepository.mainPreferencesFlow.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000L), - UserPreferences() + MainPreferences() ) private val _input: MutableStateFlow = MutableStateFlow(TextFieldValue()) diff --git a/feature/converter/build.gradle.kts b/feature/converter/build.gradle.kts index 22ab1a5b..91b5c875 100644 --- a/feature/converter/build.gradle.kts +++ b/feature/converter/build.gradle.kts @@ -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) diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt index 746e0596..15bc07a3 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt @@ -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 { diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/AboutScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/AboutScreen.kt index 00ad327e..08d4b585 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/AboutScreen.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/AboutScreen.kt @@ -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() diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt index 122588e6..1064c342 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt @@ -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 ) { diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt index 488ab511..b0aef490 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt @@ -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 - ) - } - } } diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt index cc2a874e..dc1e855b 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt @@ -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, ) } } diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThemesScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesScreen.kt similarity index 98% rename from feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThemesScreen.kt rename to feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesScreen.kt index 72a49460..6448c2bc 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThemesScreen.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesScreen.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -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 by lazy { internal fun ThemesRoute( navigateUpAction: () -> Unit = {}, themmoController: ThemmoController, - viewModel: SettingsViewModel + viewModel: ThemesViewModel = hiltViewModel() ) { ThemesScreen( navigateUpAction = navigateUpAction, diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesViewModel.kt new file mode 100644 index 00000000..d28bb490 --- /dev/null +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesViewModel.kt @@ -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 . + */ + +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) + } + } +} diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/UnitGroupsScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsScreen.kt similarity index 89% rename from feature/settings/src/main/java/com/sadellie/unitto/feature/settings/UnitGroupsScreen.kt rename to feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsScreen.kt index 39e6e148..520631ee 100644 --- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/UnitGroupsScreen.kt +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsScreen.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -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) } ) ) } diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsUIState.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsUIState.kt new file mode 100644 index 00000000..3051e863 --- /dev/null +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsUIState.kt @@ -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 . + */ + +package com.sadellie.unitto.feature.settings.unitgroups + +import com.sadellie.unitto.data.model.UnitGroup + +data class UnitGroupsUIState( + val shownGroups: List, + val hiddenGroups: List +) diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsViewModel.kt new file mode 100644 index 00000000..3b6fd155 --- /dev/null +++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsViewModel.kt @@ -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 . + */ + +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()) + + /** + * Currently hidden [UnitGroup]s. + */ + private val _hiddenUnitGroups = MutableStateFlow(listOf()) + + 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 + ) + } +} diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/UnitsListViewModel.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/UnitsListViewModel.kt index 483a2351..377517ea 100644 --- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/UnitsListViewModel.kt +++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/UnitsListViewModel.kt @@ -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 = - userPrefsRepository.userPreferencesFlow.stateIn( + private val _userPrefs: StateFlow = + userPrefsRepository.mainPreferencesFlow.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000L), - UserPreferences() + MainPreferences() ) private val _unitsToShow = MutableStateFlow(emptyMap>()) private val _searchQuery = MutableStateFlow("") private val _chosenUnitGroup: MutableStateFlow = 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 )