diff --git a/modules/common-ui-components-settings/src/main/java/com/ankitsuda/rebound/ui/components/settings/PopupItemsSettingsItem.kt b/modules/common-ui-components-settings/src/main/java/com/ankitsuda/rebound/ui/components/settings/PopupItemsSettingsItem.kt new file mode 100644 index 00000000..4661dbb2 --- /dev/null +++ b/modules/common-ui-components-settings/src/main/java/com/ankitsuda/rebound/ui/components/settings/PopupItemsSettingsItem.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This 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 software 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. + */ + +package com.ankitsuda.rebound.ui.components.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.ankitsuda.base.util.isDark +import com.ankitsuda.rebound.ui.theme.LocalThemeState +import com.ankitsuda.rebound.ui.theme.ReboundTheme + +@Composable +fun PopupItemsSettingsItem( + modifier: Modifier = Modifier, + icon: ImageVector? = null, + text: String, + description: String = "", + selectedItem: A? = null, + items: List>, + onItemSelected: (A) -> Unit +) { + var isMenuExpanded by remember { + mutableStateOf(false) + } + + Box(modifier = modifier.clickable(onClick = { + isMenuExpanded = true + })) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = text, + tint = LocalThemeState.current.onBackgroundColor, + modifier = Modifier.padding(start = 2.dp, end = 18.dp) + ) + } + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = text, + color = LocalThemeState.current.onBackgroundColor + ) + if (description.isNotEmpty()) { + Text( + text = description, + style = ReboundTheme.typography.caption, + color = ReboundTheme.colors.onBackground.copy(alpha = 0.8f), + ) + } + } + Box { + DropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { isMenuExpanded = false }) { + for (item in items) { + val bgColor = if (selectedItem == item.first) { + if (MaterialTheme.colors.surface.isDark()) { + Color.White + } else { + Color.Black + }.copy(alpha = 0.1f) + } else { + Color.Transparent + } + + DropdownMenuItem( + modifier = Modifier.background(bgColor), + onClick = { + isMenuExpanded = false + onItemSelected(item.first) + }) { + Text(text = item.second) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/common-ui-resources/src/main/res/values/strings.xml b/modules/common-ui-resources/src/main/res/values/strings.xml index 3adaa110..65d68991 100644 --- a/modules/common-ui-resources/src/main/res/values/strings.xml +++ b/modules/common-ui-resources/src/main/res/values/strings.xml @@ -138,8 +138,10 @@ Defaults Weight Unit Metric (kg) + Imperial (lbs) Distance Unit Metric (m/km) + Metric (ft/miles) First Day of The Week Sunday Your Data diff --git a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/datastore/AppPreferences.kt b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/datastore/AppPreferences.kt index 0a416311..253d314d 100644 --- a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/datastore/AppPreferences.kt +++ b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/datastore/AppPreferences.kt @@ -19,6 +19,10 @@ import androidx.datastore.preferences.core.* import com.ankitsuda.base.ui.ThemeState import com.ankitsuda.base.util.NONE_WORKOUT_ID import com.ankitsuda.domain.models.Optional +import com.ankitsuda.rebound.domain.DistanceUnit +import com.ankitsuda.rebound.domain.DistanceUnitSerializer +import com.ankitsuda.rebound.domain.WeightUnit +import com.ankitsuda.rebound.domain.WeightUnitSerializer import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -39,29 +43,47 @@ class AppPreferences @Inject constructor(@ApplicationContext private val context companion object { val THEME_STATE_KEY = stringPreferencesKey(name = "theme_state") - - - // Current workout id val CURRENT_WORKOUT_ID_KEY = stringPreferencesKey(name = "current_workout_id") + val WEIGHT_UNIT_KEY = stringPreferencesKey(name = "weight_unit") + val DISTANCE_UNIT_KEY = stringPreferencesKey(name = "distance_unit") + val FIRST_DAY_OF_WEEK_KEY = intPreferencesKey(name = "first_day_of_week") } override val themeState: Flow get() = getValue(THEME_STATE_KEY, ThemeState.serializer(), ThemeState()) - override suspend fun setThemeState(value: ThemeState) { setValue(THEME_STATE_KEY, value, ThemeState.serializer()) } - override val currentWorkoutId: Flow get() = getValue(CURRENT_WORKOUT_ID_KEY, NONE_WORKOUT_ID) override suspend fun setCurrentWorkoutId(value: String) { setValue(CURRENT_WORKOUT_ID_KEY, value) } - // SMALL SHAPE ENDS + + override val weightUnit: Flow + get() = getValue(WEIGHT_UNIT_KEY, WeightUnitSerializer, WeightUnit.KG) + + override suspend fun setWeightUnit(value: WeightUnit) { + setValue(WEIGHT_UNIT_KEY, value, WeightUnitSerializer) + } + + override val distanceUnit: Flow + get() = getValue(DISTANCE_UNIT_KEY, DistanceUnitSerializer, DistanceUnit.KM) + + override suspend fun setDistanceUnit(value: DistanceUnit) { + setValue(DISTANCE_UNIT_KEY, value, DistanceUnitSerializer) + } + + override val firstDayOfWeek: Flow + get() = getValue(FIRST_DAY_OF_WEEK_KEY, 1) + + override suspend fun setFirstDayOfWeek(value: Int) { + setValue(FIRST_DAY_OF_WEEK_KEY, value) + } override suspend fun clearPreferenceStorage() { datastoreUtils.clearPreferenceStorage() diff --git a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/datastore/PrefStorage.kt b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/datastore/PrefStorage.kt index f13ab337..2bf1e0a9 100644 --- a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/datastore/PrefStorage.kt +++ b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/datastore/PrefStorage.kt @@ -15,6 +15,8 @@ package com.ankitsuda.rebound.data.datastore import com.ankitsuda.base.ui.ThemeState +import com.ankitsuda.rebound.domain.DistanceUnit +import com.ankitsuda.rebound.domain.WeightUnit import kotlinx.coroutines.flow.Flow interface PrefStorage { @@ -25,6 +27,14 @@ interface PrefStorage { val currentWorkoutId: Flow suspend fun setCurrentWorkoutId(value: String) + val weightUnit: Flow + suspend fun setWeightUnit(value: WeightUnit) + + val distanceUnit: Flow + suspend fun setDistanceUnit(value: DistanceUnit) + + val firstDayOfWeek: Flow + suspend fun setFirstDayOfWeek(value: Int) /*** * clears all the stored data diff --git a/modules/domain/build.gradle b/modules/domain/build.gradle index 5ecd8934..2dae786e 100644 --- a/modules/domain/build.gradle +++ b/modules/domain/build.gradle @@ -19,6 +19,8 @@ plugins { id "com.android.library" id "kotlin-android" id "kotlin-parcelize" + id "org.jetbrains.kotlin.plugin.serialization" + } android { diff --git a/modules/domain/src/main/java/com/ankitsuda/rebound/domain/DistanceUnit.kt b/modules/domain/src/main/java/com/ankitsuda/rebound/domain/DistanceUnit.kt new file mode 100644 index 00000000..e2861f25 --- /dev/null +++ b/modules/domain/src/main/java/com/ankitsuda/rebound/domain/DistanceUnit.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This 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 software 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. + */ + +package com.ankitsuda.rebound.domain + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable +enum class DistanceUnit(val value: String) { + KM("km"), + MILES("miles"); + + companion object { + fun fromValue(value: String): DistanceUnit { + return values().find { it.value == value } ?: KM + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = DistanceUnit::class) +object DistanceUnitSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("DistanceUnit", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: DistanceUnit) { + encoder.encodeString(value.value) + } + + override fun deserialize(decoder: Decoder): DistanceUnit { + return try { + val key = decoder.decodeString() + DistanceUnit.fromValue(key) + } catch (e: IllegalArgumentException) { + DistanceUnit.KM + } + } +} diff --git a/modules/domain/src/main/java/com/ankitsuda/rebound/domain/WeightUnit.kt b/modules/domain/src/main/java/com/ankitsuda/rebound/domain/WeightUnit.kt new file mode 100644 index 00000000..75319b4c --- /dev/null +++ b/modules/domain/src/main/java/com/ankitsuda/rebound/domain/WeightUnit.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This 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 software 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. + */ + +package com.ankitsuda.rebound.domain + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable +enum class WeightUnit(val value: String) { + KG("kg"), + LBS("lbs"); + + companion object { + fun fromValue(value: String): WeightUnit { + return values().find { it.value == value } ?: KG + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = WeightUnit::class) +object WeightUnitSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("WeightUnit", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: WeightUnit) { + encoder.encodeString(value.value) + } + + override fun deserialize(decoder: Decoder): WeightUnit { + return try { + val key = decoder.decodeString() + WeightUnit.fromValue(key) + } catch (e: IllegalArgumentException) { + WeightUnit.KG + } + } +} diff --git a/modules/ui-settings/build.gradle b/modules/ui-settings/build.gradle index 69a4377b..d15c5338 100644 --- a/modules/ui-settings/build.gradle +++ b/modules/ui-settings/build.gradle @@ -30,6 +30,7 @@ android { } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -52,8 +53,11 @@ dependencies { implementation project(":modules:common-ui-compose") implementation project(":modules:common-ui-theme") implementation project(":modules:common-ui-components") + implementation project(":modules:common-ui-components-settings") implementation project(":modules:navigation") + coreLibraryDesugaring Deps.Android.desugaring + implementation Deps.Dagger.hilt kapt Deps.Dagger.hiltCompiler } diff --git a/modules/ui-settings/src/main/java/com/ankitsuda/rebound/ui/settings/SettingsScreen.kt b/modules/ui-settings/src/main/java/com/ankitsuda/rebound/ui/settings/SettingsScreen.kt index d96b50d4..1a711513 100644 --- a/modules/ui-settings/src/main/java/com/ankitsuda/rebound/ui/settings/SettingsScreen.kt +++ b/modules/ui-settings/src/main/java/com/ankitsuda/rebound/ui/settings/SettingsScreen.kt @@ -15,36 +15,50 @@ package com.ankitsuda.rebound.ui.settings import androidx.compose.foundation.background -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.ankitsuda.navigation.LeafScreen import com.ankitsuda.navigation.LocalNavigator import com.ankitsuda.navigation.Navigator -import com.ankitsuda.rebound.ui.components.* +import com.ankitsuda.rebound.domain.DistanceUnit +import com.ankitsuda.rebound.domain.WeightUnit +import com.ankitsuda.rebound.ui.components.MoreItemCard +import com.ankitsuda.rebound.ui.components.MoreSectionHeader +import com.ankitsuda.rebound.ui.components.TopBar2 +import com.ankitsuda.rebound.ui.components.TopBarBackIconButton +import com.ankitsuda.rebound.ui.components.settings.PopupItemsSettingsItem import com.ankitsuda.rebound.ui.icons.Plates import me.onebone.toolbar.CollapsingToolbarScaffold import me.onebone.toolbar.ScrollStrategy import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.format.TextStyle +import java.util.Locale @Composable fun SettingsScreen( - navController: NavController, navigator: Navigator = LocalNavigator.current, + navController: NavController, + navigator: Navigator = LocalNavigator.current, + viewModel: SettingsScreenViewModel = hiltViewModel() ) { val collapsingState = rememberCollapsingToolbarScaffoldState() + val weightUnit by viewModel.weightUnit.collectAsState(initial = WeightUnit.KG) + val distanceUnit by viewModel.distanceUnit.collectAsState(initial = DistanceUnit.KM) + val firstDayOfWeek by viewModel.firstDayOfWeek.collectAsState(initial = 1) + CollapsingToolbarScaffold( scrollStrategy = ScrollStrategy.EnterAlwaysCollapsed, state = collapsingState, @@ -92,37 +106,71 @@ fun SettingsScreen( MoreSectionHeader(text = stringResource(R.string.defaults)) } item { - MoreItemCard( + val getStringByWeightUnit: @Composable ((WeightUnit) -> String) = { + when (it) { + WeightUnit.KG -> stringResource(R.string.metric_kg) + WeightUnit.LBS -> stringResource(R.string.imperial_lbs) + } + } + + PopupItemsSettingsItem( modifier = Modifier .fillMaxWidth(), icon = Icons.Outlined.FitnessCenter, text = stringResource(R.string.weight_unit), - description = stringResource(R.string.metric_kg), - onClick = { - - }) + description = getStringByWeightUnit(weightUnit), + selectedItem = weightUnit, + items = WeightUnit.values().map { + Pair( + it, getStringByWeightUnit(it) + ) + }, + onItemSelected = { + viewModel.setWeightUnit(it) + } + ) } item { - MoreItemCard( + val getStringByDistanceUnit: @Composable ((DistanceUnit) -> String) = { + when (it) { + DistanceUnit.KM -> stringResource(R.string.metric_m_km) + DistanceUnit.MILES -> stringResource(R.string.imperial_ft_miles) + } + } + + PopupItemsSettingsItem( modifier = Modifier .fillMaxWidth(), icon = Icons.Outlined.DirectionsRun, text = stringResource(R.string.distance_unit), - description = stringResource(R.string.metric_m_km), - onClick = { - - }) + description = getStringByDistanceUnit(distanceUnit), + selectedItem = distanceUnit, + items = DistanceUnit.values().map { + Pair( + it, getStringByDistanceUnit(it) + ) + }, + onItemSelected = { + viewModel.setDistanceUnit(it) + } + ) } item { - MoreItemCard( + fun getDayName(day: Int) = + DayOfWeek.of(day).getDisplayName(TextStyle.FULL, Locale.getDefault()) + + PopupItemsSettingsItem( modifier = Modifier .fillMaxWidth(), icon = Icons.Outlined.Event, text = stringResource(R.string.first_day_of_the_week), - description = stringResource(R.string.sunday), - onClick = { - - }) + description = getDayName(firstDayOfWeek), + selectedItem = firstDayOfWeek, + items = DayOfWeek.values().map { Pair(it.value, getDayName(it.value)) }, + onItemSelected = { + viewModel.setFirstDayOfWeek(it) + } + ) } item { MoreSectionHeader(text = stringResource(R.string.your_data)) diff --git a/modules/ui-settings/src/main/java/com/ankitsuda/rebound/ui/settings/SettingsScreenViewModel.kt b/modules/ui-settings/src/main/java/com/ankitsuda/rebound/ui/settings/SettingsScreenViewModel.kt new file mode 100644 index 00000000..4057eb73 --- /dev/null +++ b/modules/ui-settings/src/main/java/com/ankitsuda/rebound/ui/settings/SettingsScreenViewModel.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This 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 software 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. + */ + +package com.ankitsuda.rebound.ui.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ankitsuda.base.utils.extensions.lazyAsync +import com.ankitsuda.base.utils.extensions.shareWhileObserved +import com.ankitsuda.rebound.data.datastore.PrefStorage +import com.ankitsuda.rebound.domain.DistanceUnit +import com.ankitsuda.rebound.domain.WeightUnit +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsScreenViewModel @Inject constructor( + private val prefs: PrefStorage +) : ViewModel() { + val weightUnit = prefs.weightUnit.shareWhileObserved(viewModelScope) + val distanceUnit = prefs.distanceUnit.shareWhileObserved(viewModelScope) + val firstDayOfWeek = prefs.firstDayOfWeek.shareWhileObserved(viewModelScope) + + fun setWeightUnit(value: WeightUnit) { + viewModelScope.launch { + prefs.setWeightUnit(value) + } + } + + fun setDistanceUnit(value: DistanceUnit) { + viewModelScope.launch { + prefs.setDistanceUnit(value) + } + } + + fun setFirstDayOfWeek(value: Int) { + viewModelScope.launch { + prefs.setFirstDayOfWeek(value) + } + } +} \ No newline at end of file