diff --git a/app/build.gradle b/app/build.gradle index 417238fa..1710e58b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,7 +75,7 @@ aboutLibraries { dependencies { - def composeBom = platform('androidx.compose:compose-bom:2024.02.02') + def composeBom = platform('androidx.compose:compose-bom:2024.03.00') implementation composeBom androidTestImplementation composeBom diff --git a/app/src/main/java/com/starry/greenstash/MainActivity.kt b/app/src/main/java/com/starry/greenstash/MainActivity.kt index 59d919f0..e0f05025 100644 --- a/app/src/main/java/com/starry/greenstash/MainActivity.kt +++ b/app/src/main/java/com/starry/greenstash/MainActivity.kt @@ -41,8 +41,8 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.starry.greenstash.ui.navigation.NavGraph -import com.starry.greenstash.ui.screens.settings.viewmodels.SettingsViewModel -import com.starry.greenstash.ui.screens.settings.viewmodels.ThemeMode +import com.starry.greenstash.ui.screens.settings.SettingsViewModel +import com.starry.greenstash.ui.screens.settings.ThemeMode import com.starry.greenstash.ui.theme.GreenStashTheme import com.starry.greenstash.utils.Utils import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/com/starry/greenstash/di/MainModule.kt b/app/src/main/java/com/starry/greenstash/di/MainModule.kt index c6730469..bc78cb43 100644 --- a/app/src/main/java/com/starry/greenstash/di/MainModule.kt +++ b/app/src/main/java/com/starry/greenstash/di/MainModule.kt @@ -26,11 +26,6 @@ package com.starry.greenstash.di import android.content.Context -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.ui.ExperimentalComposeUiApi import com.starry.greenstash.backup.BackupManager import com.starry.greenstash.database.core.AppDatabase import com.starry.greenstash.database.goal.GoalDao @@ -43,15 +38,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.ExperimentalCoroutinesApi import javax.inject.Singleton -@ExperimentalCoroutinesApi -@ExperimentalMaterialApi -@ExperimentalFoundationApi -@ExperimentalComposeUiApi -@ExperimentalAnimationApi -@ExperimentalMaterial3Api + @InstallIn(SingletonComponent::class) @Module class MainModule { diff --git a/app/src/main/java/com/starry/greenstash/reminder/ReminderNotificationSender.kt b/app/src/main/java/com/starry/greenstash/reminder/ReminderNotificationSender.kt index ccbfba80..d5e6d8c5 100644 --- a/app/src/main/java/com/starry/greenstash/reminder/ReminderNotificationSender.kt +++ b/app/src/main/java/com/starry/greenstash/reminder/ReminderNotificationSender.kt @@ -72,16 +72,20 @@ class ReminderNotificationSender( val remainingAmount = (goal.targetAmount - goalItem.getCurrentlySavedAmount()) val defCurrency = preferenceUtil.getString(PreferenceUtil.DEFAULT_CURRENCY_STR, "")!! + val datePattern = preferenceUtil.getString(PreferenceUtil.DATE_FORMAT_STR, "")!! if (goal.deadline.isNotEmpty() && goal.deadline.isNotBlank()) { - val calculatedDays = GoalTextUtils(preferenceUtil).calcRemainingDays(goal) + val calculatedDays = GoalTextUtils.calcRemainingDays(goal, datePattern) when (goal.priority) { GoalPriority.High -> { val amountDay = remainingAmount / calculatedDays.remainingDays notification.addAction( R.drawable.ic_notification_deposit, "${context.getString(R.string.deposit_button)} ${ - Utils.formatCurrency(Utils.roundDecimal(amountDay), defCurrency) + Utils.formatCurrency( + amount = Utils.roundDecimal(amountDay), + currencyCode = defCurrency + ) }", createDepositIntent(goal.goalId, amountDay) ) @@ -92,7 +96,10 @@ class ReminderNotificationSender( notification.addAction( R.drawable.ic_notification_deposit, "${context.getString(R.string.deposit_button)} ${ - Utils.formatCurrency(Utils.roundDecimal(amountSemiWeek), defCurrency) + Utils.formatCurrency( + amount = Utils.roundDecimal(amountSemiWeek), + currencyCode = defCurrency + ) }", createDepositIntent(goal.goalId, amountSemiWeek) ) @@ -103,7 +110,10 @@ class ReminderNotificationSender( notification.addAction( R.drawable.ic_notification_deposit, "${context.getString(R.string.deposit_button)} ${ - Utils.formatCurrency(Utils.roundDecimal(amountWeek), defCurrency) + Utils.formatCurrency( + amount = Utils.roundDecimal(amountWeek), + currencyCode = defCurrency + ) }", createDepositIntent(goal.goalId, amountWeek) ) diff --git a/app/src/main/java/com/starry/greenstash/ui/common/CurrencyPicker.kt b/app/src/main/java/com/starry/greenstash/ui/common/CurrencyPicker.kt index efd384f8..577f319a 100644 --- a/app/src/main/java/com/starry/greenstash/ui/common/CurrencyPicker.kt +++ b/app/src/main/java/com/starry/greenstash/ui/common/CurrencyPicker.kt @@ -48,6 +48,7 @@ import androidx.compose.material3.RadioButtonDefaults import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -66,15 +67,46 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +/** + * Data class to hold the currency names and values for the currency picker. + * @param currencyNames Array of currency names. + * @param currencyValues Array of currency values. + */ +@Immutable +data class CurrencyPickerData( + val currencyNames: Array, + val currencyValues: Array +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CurrencyPickerData + + if (!currencyNames.contentEquals(other.currencyNames)) return false + if (!currencyValues.contentEquals(other.currencyValues)) return false + + return true + } + + override fun hashCode(): Int { + var result = currencyNames.contentHashCode() + result = 31 * result + currencyValues.contentHashCode() + return result + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun CurrencyPicker( defaultCurrencyValue: String, - currencyNames: Array, - currencyValues: Array, + currencyPickerData: CurrencyPickerData, showBottomSheet: MutableState, onCurrencySelected: (String) -> Unit ) { + val currencyNames = currencyPickerData.currencyNames + val currencyValues = currencyPickerData.currencyValues + val defaultCurrencyEntry = currencyNames[currencyValues.indexOf(defaultCurrencyValue)] val (selectedCurrencyOption, onCurrencyOptionSelected) = remember { mutableStateOf(defaultCurrencyEntry) diff --git a/app/src/main/java/com/starry/greenstash/ui/common/DateTimeCard.kt b/app/src/main/java/com/starry/greenstash/ui/common/DateTimeCard.kt new file mode 100644 index 00000000..5d73b94b --- /dev/null +++ b/app/src/main/java/com/starry/greenstash/ui/common/DateTimeCard.kt @@ -0,0 +1,113 @@ +/** + * MIT License + * + * Copyright (c) [2022 - Present] Stɑrry Shivɑm + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +package com.starry.greenstash.ui.common + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.starry.greenstash.R +import com.starry.greenstash.ui.screens.settings.DateStyle +import com.starry.greenstash.ui.theme.greenstashFont +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +fun DateTimeCard( + selectedDateTime: LocalDateTime, + dateTimeStyle: () -> DateStyle, + onClick: () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp, vertical = 8.dp) + .clickable { onClick() } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center + ) { + Row { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_dw_date), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Text( + text = selectedDateTime.format( + DateTimeFormatter.ofPattern(dateTimeStyle().pattern) + ), + fontFamily = greenstashFont, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(start = 8.dp, top = 2.dp) + ) + } + + Spacer(modifier = Modifier.width(24.dp)) + + Row { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_dw_time), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Text( + text = selectedDateTime.format(DateTimeFormatter.ofPattern("h:mm a")), + fontFamily = greenstashFont, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(start = 8.dp, top = 2.dp) + ) + } + } + } +} + +@Preview +@Composable +private fun DateTimeCardPreview() { + DateTimeCard( + selectedDateTime = LocalDateTime.now(), + dateTimeStyle = { DateStyle.DateMonthYear }, + onClick = {} + ) +} diff --git a/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt b/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt index 673a4028..504a52c4 100644 --- a/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt @@ -40,7 +40,7 @@ import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument -import com.starry.greenstash.ui.screens.backups.BackupScreen +import com.starry.greenstash.ui.screens.backups.composables.BackupScreen import com.starry.greenstash.ui.screens.dwscreen.composables.DWScreen import com.starry.greenstash.ui.screens.home.composables.HomeScreen import com.starry.greenstash.ui.screens.info.composables.GoalInfoScreen diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt similarity index 98% rename from app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupScreen.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt index 64987a11..7108ed5e 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt @@ -23,7 +23,7 @@ */ -package com.starry.greenstash.ui.screens.backups +package com.starry.greenstash.ui.screens.backups.composables import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -71,6 +71,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import coil.compose.AsyncImage import com.starry.greenstash.R +import com.starry.greenstash.ui.screens.backups.BackupViewModel import com.starry.greenstash.ui.theme.greenstashFont import kotlinx.coroutines.launch import java.io.InputStreamReader @@ -154,7 +155,7 @@ fun BackupScreen(navController: NavController) { } @Composable -fun BackupScreenContent( +private fun BackupScreenContent( paddingValues: PaddingValues, onBackupClicked: () -> Unit, onRestoreClicked: () -> Unit ) { Column( diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/input/viewmodels/DWViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/DWViewModel.kt similarity index 89% rename from app/src/main/java/com/starry/greenstash/ui/screens/input/viewmodels/DWViewModel.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/DWViewModel.kt index a10d767d..4c94a1c2 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/input/viewmodels/DWViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/DWViewModel.kt @@ -1,4 +1,4 @@ -package com.starry.greenstash.ui.screens.input.viewmodels +package com.starry.greenstash.ui.screens.dwscreen import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -9,7 +9,7 @@ import com.starry.greenstash.database.goal.GoalDao import com.starry.greenstash.database.transaction.Transaction import com.starry.greenstash.database.transaction.TransactionDao import com.starry.greenstash.database.transaction.TransactionType -import com.starry.greenstash.ui.screens.settings.viewmodels.DateStyle +import com.starry.greenstash.ui.screens.settings.DateStyle import com.starry.greenstash.utils.PreferenceUtil import com.starry.greenstash.utils.Utils import dagger.hilt.android.lifecycle.HiltViewModel @@ -33,9 +33,17 @@ class DWViewModel @Inject constructor( var state by mutableStateOf(DWScreenState()) - fun getDateStyleValue() = preferenceUtil.getString( - PreferenceUtil.DATE_FORMAT_STR, DateStyle.DateMonthYear.pattern - ) + fun getDateStyle(): DateStyle { + val dateStyleValue = preferenceUtil.getString( + PreferenceUtil.DATE_FORMAT_STR, + DateStyle.DateMonthYear.pattern + ) + return if (dateStyleValue == DateStyle.DateMonthYear.pattern) { + DateStyle.DateMonthYear + } else { + DateStyle.YearMonthDate + } + } fun convertTransactionType(type: String): TransactionType { return when (type) { diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/composables/DWScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/composables/DWScreen.kt index c8ddad89..8ebda39a 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/composables/DWScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/composables/DWScreen.kt @@ -25,17 +25,13 @@ package com.starry.greenstash.ui.screens.dwscreen.composables -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -43,7 +39,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Button -import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -86,9 +81,10 @@ import com.maxkeppeler.sheets.date_time.DateTimeDialog import com.maxkeppeler.sheets.date_time.models.DateTimeSelection import com.starry.greenstash.R import com.starry.greenstash.database.transaction.TransactionType +import com.starry.greenstash.ui.common.DateTimeCard import com.starry.greenstash.ui.navigation.DrawerScreens import com.starry.greenstash.ui.navigation.Screens -import com.starry.greenstash.ui.screens.input.viewmodels.DWViewModel +import com.starry.greenstash.ui.screens.dwscreen.DWViewModel import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.utils.Utils import com.starry.greenstash.utils.validateAmount @@ -98,7 +94,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.time.LocalDateTime -import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class) @@ -108,7 +103,7 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont val viewModel: DWViewModel = hiltViewModel() val selectedDateTime = remember { - mutableStateOf(LocalDateTime.now()) + mutableStateOf(LocalDateTime.now()) } val dateTimeDialogState = rememberUseCaseState(visible = false) @@ -121,8 +116,8 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont DateTimeDialog( state = dateTimeDialogState, selection = DateTimeSelection.DateTime( - selectedDate = selectedDateTime.value!!.toLocalDate(), - selectedTime = selectedDateTime.value!!.toLocalTime(), + selectedDate = selectedDateTime.value.toLocalDate(), + selectedTime = selectedDateTime.value.toLocalTime(), ) { newDateTime -> selectedDateTime.value = newDateTime }, @@ -157,39 +152,7 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont }) { paddingValues -> if (showTransactionAddedAnim.value) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val compositionResult: LottieCompositionResult = rememberLottieComposition( - spec = LottieCompositionSpec.RawRes(R.raw.transaction_added_lottie) - ) - val progressAnimation by animateLottieCompositionAsState( - compositionResult.value, - isPlaying = true, - iterations = 1, - speed = 1.4f - ) - - Spacer(modifier = Modifier.weight(1f)) - - LottieAnimation( - composition = compositionResult.value, - progress = progressAnimation, - modifier = Modifier.size(320.dp) - ) - - Text( - text = if (transactionType == TransactionType.Deposit) - stringResource(id = R.string.deposit_successful) - else stringResource(id = R.string.withdraw_successful), - fontWeight = FontWeight.SemiBold, - fontFamily = greenstashFont, - fontSize = 20.sp - ) - - Spacer(modifier = Modifier.weight(1.4f)) - } + TransactionAddedAnimation(transactionType) } else { Column( modifier = Modifier @@ -220,9 +183,9 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont enableMergePaths = true ) - DateTimePicker( - selectedDateTime = selectedDateTime, - dateTimeStyle = { viewModel.getDateStyleValue()!! }, + DateTimeCard( + selectedDateTime = selectedDateTime.value, + dateTimeStyle = { viewModel.getDateStyle() }, onClick = { dateTimeDialogState.show() } ) @@ -295,7 +258,7 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont TransactionType.Deposit -> { viewModel.deposit( goalId = goalId.toLong(), - dateTime = selectedDateTime.value!!, + dateTime = selectedDateTime.value, onGoalAchieved = { coroutineScope.launch { showTransactionAddedAnim.value = true @@ -317,7 +280,7 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont TransactionType.Withdraw -> { viewModel.withdraw( goalId = goalId.toLong(), - dateTime = selectedDateTime.value!!, + dateTime = selectedDateTime.value, onWithDrawOverflow = { coroutineScope.launch { snackBarHostState.showSnackbar( @@ -361,56 +324,41 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont } } + @Composable -fun DateTimePicker( - selectedDateTime: MutableState, - dateTimeStyle: () -> String, - onClick: () -> Unit -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 18.dp, vertical = 8.dp) - .clickable { onClick() } +private fun TransactionAddedAnimation(transactionType: TransactionType) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.Center - ) { - Row { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_dw_date), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Text( - text = selectedDateTime.value!!.format( - DateTimeFormatter.ofPattern(dateTimeStyle()) - ), - fontFamily = greenstashFont, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(start = 8.dp, top = 2.dp) - ) - } + val compositionResult: LottieCompositionResult = rememberLottieComposition( + spec = LottieCompositionSpec.RawRes(R.raw.transaction_added_lottie) + ) + val progressAnimation by animateLottieCompositionAsState( + compositionResult.value, + isPlaying = true, + iterations = 1, + speed = 1.4f + ) - Spacer(modifier = Modifier.width(24.dp)) + Spacer(modifier = Modifier.weight(1f)) - Row { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_dw_time), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Text( - text = selectedDateTime.value!!.format(DateTimeFormatter.ofPattern("h:mm a")), - fontFamily = greenstashFont, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(start = 8.dp, top = 2.dp) - ) - } - } + LottieAnimation( + composition = compositionResult.value, + progress = progressAnimation, + modifier = Modifier.size(320.dp) + ) + + Text( + text = if (transactionType == TransactionType.Deposit) + stringResource(id = R.string.deposit_successful) + else stringResource(id = R.string.withdraw_successful), + fontWeight = FontWeight.SemiBold, + fontFamily = greenstashFont, + fontSize = 20.sp + ) + + Spacer(modifier = Modifier.weight(1.4f)) } } diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/home/viewmodels/HomeViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/home/HomeViewModel.kt similarity index 95% rename from app/src/main/java/com/starry/greenstash/ui/screens/home/viewmodels/HomeViewModel.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/home/HomeViewModel.kt index 4fa1ac5b..6c571904 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/home/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/home/HomeViewModel.kt @@ -23,7 +23,7 @@ */ -package com.starry.greenstash.ui.screens.home.viewmodels +package com.starry.greenstash.ui.screens.home import androidx.compose.runtime.MutableState import androidx.compose.runtime.State @@ -34,10 +34,10 @@ import androidx.lifecycle.viewModelScope import com.starry.greenstash.database.goal.Goal import com.starry.greenstash.database.goal.GoalDao import com.starry.greenstash.reminder.ReminderManager -import com.starry.greenstash.utils.GoalTextUtils import com.starry.greenstash.utils.PreferenceUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch @@ -57,8 +57,6 @@ class HomeViewModel @Inject constructor( private val preferenceUtil: PreferenceUtil ) : ViewModel() { - val goalTextUtil = GoalTextUtils(preferenceUtil) - private val _filterFlowData: MutableState = mutableStateOf( FilterFlowData( FilterField.entries[preferenceUtil.getInt( @@ -74,6 +72,8 @@ class HomeViewModel @Inject constructor( val filterFlowData: State = _filterFlowData private val filterFlow = MutableStateFlow(filterFlowData.value) + + @OptIn(ExperimentalCoroutinesApi::class) private val goalsListFlow = filterFlow.flatMapLatest { ffData -> // Save the current filter combination in shared preferences @@ -151,6 +151,10 @@ class HomeViewModel @Inject constructor( return preferenceUtil.getString(PreferenceUtil.DEFAULT_CURRENCY_STR, "")!! } + fun getDateFormatPattern(): String { + return preferenceUtil.getString(PreferenceUtil.DATE_FORMAT_STR, "")!! + } + fun onboardingTapTargetsShown() { preferenceUtil.putBoolean(PreferenceUtil.HOME_SCREEN_ONBOARDING_BOOL, false) _showOnboardingTapTargets.value = false diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/GoalLazyItem.kt b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/GoalLazyItem.kt index 0a5a0809..4519916e 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/GoalLazyItem.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/GoalLazyItem.kt @@ -43,9 +43,10 @@ import com.starry.greenstash.R import com.starry.greenstash.database.core.GoalWithTransactions import com.starry.greenstash.database.transaction.TransactionType import com.starry.greenstash.ui.navigation.Screens -import com.starry.greenstash.ui.screens.home.viewmodels.GoalCardStyle -import com.starry.greenstash.ui.screens.home.viewmodels.HomeViewModel +import com.starry.greenstash.ui.screens.home.GoalCardStyle +import com.starry.greenstash.ui.screens.home.HomeViewModel import com.starry.greenstash.utils.Constants +import com.starry.greenstash.utils.GoalTextUtils import com.starry.greenstash.utils.ImageUtils import com.starry.greenstash.utils.Utils import com.starry.greenstash.utils.getActivity @@ -74,12 +75,18 @@ fun GoalLazyColumnItem( when (goalCardStyle) { GoalCardStyle.Classic -> { GoalItemClassic(title = item.goal.title, - primaryText = viewModel.goalTextUtil.buildPrimaryText( - context, - progressPercent, - item + primaryText = GoalTextUtils.buildPrimaryText( + context = context, + progressPercent = progressPercent, + goalItem = item, + currencyCode = viewModel.getDefaultCurrency() + ), + secondaryText = GoalTextUtils.buildSecondaryText( + context = context, + goalItem = item, + currencyCode = viewModel.getDefaultCurrency(), + datePattern = viewModel.getDateFormatPattern() ), - secondaryText = viewModel.goalTextUtil.buildSecondaryText(context, item), goalProgress = progressPercent.toFloat() / 100, goalImage = item.goal.goalImage, onDepositClicked = { @@ -147,7 +154,11 @@ fun GoalLazyColumnItem( item.getCurrentlySavedAmount(), viewModel.getDefaultCurrency() ), - daysLeftText = viewModel.goalTextUtil.getRemainingDaysText(context, item), + daysLeftText = GoalTextUtils.getRemainingDaysText( + context = context, + goalItem = item, + datePattern = viewModel.getDateFormatPattern() + ), goalProgress = progressPercent.toFloat() / 100, goalIcon = goalIcon, onDepositClicked = { diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeAppBars.kt b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeAppBars.kt index 2d90dbe0..84f9f620 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeAppBars.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeAppBars.kt @@ -55,12 +55,12 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.starry.greenstash.R -import com.starry.greenstash.ui.screens.home.viewmodels.SearchWidgetState +import com.starry.greenstash.ui.screens.home.SearchWidgetState import com.starry.greenstash.ui.theme.greenstashFont @Composable -fun MainAppBar( +fun HomeAppBar( onMenuClicked: () -> Unit, onFilterClicked: () -> Unit, onSearchClicked: () -> Unit, @@ -98,7 +98,7 @@ fun MainAppBar( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DefaultAppBar( +private fun DefaultAppBar( onMenuClicked: () -> Unit, onFilterClicked: () -> Unit, onSearchClicked: () -> Unit, @@ -140,7 +140,7 @@ fun DefaultAppBar( @Composable -fun SearchAppBar( +private fun SearchAppBar( text: String, onTextChange: (String) -> Unit, onCloseClicked: () -> Unit, diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeScreen.kt index 065ec9ba..afa0776e 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeScreen.kt @@ -111,10 +111,10 @@ import com.starry.greenstash.R import com.starry.greenstash.database.core.GoalWithTransactions import com.starry.greenstash.ui.navigation.DrawerScreens import com.starry.greenstash.ui.navigation.Screens -import com.starry.greenstash.ui.screens.home.viewmodels.FilterField -import com.starry.greenstash.ui.screens.home.viewmodels.FilterSortType -import com.starry.greenstash.ui.screens.home.viewmodels.HomeViewModel -import com.starry.greenstash.ui.screens.home.viewmodels.SearchWidgetState +import com.starry.greenstash.ui.screens.home.FilterField +import com.starry.greenstash.ui.screens.home.FilterSortType +import com.starry.greenstash.ui.screens.home.HomeViewModel +import com.starry.greenstash.ui.screens.home.SearchWidgetState import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.utils.Utils import com.starry.greenstash.utils.isScrollingUp @@ -123,9 +123,7 @@ import kotlinx.coroutines.launch import java.util.Locale -@OptIn( - ExperimentalMaterialApi::class -) +@OptIn(ExperimentalMaterialApi::class) @Composable fun HomeScreen(navController: NavController) { val context = LocalContext.current @@ -156,7 +154,7 @@ fun HomeScreen(navController: NavController) { @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable -fun HomeScreenContent( +private fun HomeScreenContent( context: Context, viewModel: HomeViewModel, navController: NavController, @@ -259,7 +257,7 @@ fun HomeScreenContent( Scaffold(modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(snackBarHostState) }, topBar = { - MainAppBar( + HomeAppBar( onMenuClicked = { coroutineScope.launch { drawerState.open() } }, onFilterClicked = { coroutineScope.launch { bottomSheetState.show() } @@ -344,40 +342,7 @@ fun HomeScreenContent( }) if (showNoGoalsAnimation) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val compositionResult: LottieCompositionResult = - rememberLottieComposition( - spec = LottieCompositionSpec.RawRes(R.raw.no_goal_set_lottie) - ) - val progressAnimation by animateLottieCompositionAsState( - compositionResult.value, - isPlaying = true, - iterations = LottieConstants.IterateForever, - speed = 1f - ) - - Spacer(modifier = Modifier.weight(1f)) - - LottieAnimation( - composition = compositionResult.value, - progress = progressAnimation, - modifier = Modifier.size(335.dp), - enableMergePaths = true - ) - - Text( - text = stringResource(id = R.string.no_goal_set), - fontWeight = FontWeight.Medium, - fontFamily = greenstashFont, - fontSize = 18.sp, - modifier = Modifier.padding(start = 12.dp, end = 12.dp), - ) - - Spacer(modifier = Modifier.weight(2f)) - } + NoGoalAnimation() } } else { if (searchTextState.isNotEmpty() && searchTextState.isNotBlank()) { @@ -487,7 +452,7 @@ fun HomeScreenContent( @Composable -fun FilterMenuSheet(viewModel: HomeViewModel) { +private fun FilterMenuSheet(viewModel: HomeViewModel) { Column( modifier = Modifier .fillMaxWidth() @@ -523,7 +488,7 @@ fun FilterMenuSheet(viewModel: HomeViewModel) { @Composable -fun FilterButton(text: String, isSelected: Boolean, onClick: () -> Unit) { +private fun FilterButton(text: String, isSelected: Boolean, onClick: () -> Unit) { val buttonColor: Color val textColor: Color if (isSelected) { @@ -563,6 +528,44 @@ fun FilterButton(text: String, isSelected: Boolean, onClick: () -> Unit) { } } +@Composable +private fun NoGoalAnimation() { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val compositionResult: LottieCompositionResult = + rememberLottieComposition( + spec = LottieCompositionSpec.RawRes(R.raw.no_goal_set_lottie) + ) + val progressAnimation by animateLottieCompositionAsState( + compositionResult.value, + isPlaying = true, + iterations = LottieConstants.IterateForever, + speed = 1f + ) + + Spacer(modifier = Modifier.weight(1f)) + + LottieAnimation( + composition = compositionResult.value, + progress = progressAnimation, + modifier = Modifier.size(335.dp), + enableMergePaths = true + ) + + Text( + text = stringResource(id = R.string.no_goal_set), + fontWeight = FontWeight.Medium, + fontFamily = greenstashFont, + fontSize = 18.sp, + modifier = Modifier.padding(start = 12.dp, end = 12.dp), + ) + + Spacer(modifier = Modifier.weight(2f)) + } +} + @Composable @Preview diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/info/viewmodels/InfoViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/InfoViewModel.kt similarity index 89% rename from app/src/main/java/com/starry/greenstash/ui/screens/info/viewmodels/InfoViewModel.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/info/InfoViewModel.kt index 2b166391..d808853d 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/info/viewmodels/InfoViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/InfoViewModel.kt @@ -23,7 +23,7 @@ */ -package com.starry.greenstash.ui.screens.info.viewmodels +package com.starry.greenstash.ui.screens.info import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,8 +35,7 @@ import com.starry.greenstash.database.goal.GoalDao import com.starry.greenstash.database.transaction.Transaction import com.starry.greenstash.database.transaction.TransactionDao import com.starry.greenstash.database.transaction.TransactionType -import com.starry.greenstash.ui.screens.settings.viewmodels.DateStyle -import com.starry.greenstash.utils.GoalTextUtils +import com.starry.greenstash.ui.screens.settings.DateStyle import com.starry.greenstash.utils.PreferenceUtil import com.starry.greenstash.utils.Utils import dagger.hilt.android.lifecycle.HiltViewModel @@ -63,7 +62,6 @@ class InfoViewModel @Inject constructor( private val preferenceUtil: PreferenceUtil ) : ViewModel() { - val goalTextUtils = GoalTextUtils(preferenceUtil) var state by mutableStateOf(InfoScreenState()) var editGoalState by mutableStateOf(EditGoalState()) @@ -109,9 +107,17 @@ class InfoViewModel @Inject constructor( PreferenceUtil.DEFAULT_CURRENCY_STR, "$" )!! - fun getDateStyleValue() = preferenceUtil.getString( - PreferenceUtil.DATE_FORMAT_STR, DateStyle.DateMonthYear.pattern - ) + fun getDateStyle(): DateStyle { + val dateStyleValue = preferenceUtil.getString( + PreferenceUtil.DATE_FORMAT_STR, + DateStyle.DateMonthYear.pattern + ) + return if (dateStyleValue == DateStyle.DateMonthYear.pattern) { + DateStyle.DateMonthYear + } else { + DateStyle.YearMonthDate + } + } fun shouldShowTransactionTip() = preferenceUtil.getBoolean( PreferenceUtil.INFO_TRANSACTION_SWIPE_TIP_BOOL, true diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/EditTransactionSheet.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/EditTransactionSheet.kt index e347dd42..92266b25 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/EditTransactionSheet.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/EditTransactionSheet.kt @@ -66,8 +66,8 @@ import com.maxkeppeler.sheets.date_time.models.DateTimeSelection import com.starry.greenstash.R import com.starry.greenstash.database.transaction.Transaction import com.starry.greenstash.database.transaction.TransactionType -import com.starry.greenstash.ui.screens.dwscreen.composables.DateTimePicker -import com.starry.greenstash.ui.screens.info.viewmodels.InfoViewModel +import com.starry.greenstash.ui.common.DateTimeCard +import com.starry.greenstash.ui.screens.info.InfoViewModel import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.utils.Utils import com.starry.greenstash.utils.toToast @@ -91,7 +91,7 @@ fun EditTransactionSheet( val selectedDateTime = remember { val instant = Instant.ofEpochMilli(transaction.timeStamp) - mutableStateOf( + mutableStateOf( LocalDateTime.ofInstant( instant, TimeZone.getDefault().toZoneId() @@ -106,8 +106,8 @@ fun EditTransactionSheet( DateTimeDialog( state = dateTimeDialogState, selection = DateTimeSelection.DateTime( - selectedDate = selectedDateTime.value!!.toLocalDate(), - selectedTime = selectedDateTime.value!!.toLocalTime(), + selectedDate = selectedDateTime.value.toLocalDate(), + selectedTime = selectedDateTime.value.toLocalTime(), ) { newDateTime -> selectedDateTime.value = newDateTime } ) @@ -166,9 +166,9 @@ fun EditTransactionSheet( } } - DateTimePicker( - selectedDateTime = selectedDateTime, - dateTimeStyle = { viewModel.getDateStyleValue()!! }, + DateTimeCard( + selectedDateTime = selectedDateTime.value, + dateTimeStyle = { viewModel.getDateStyle() }, onClick = { dateTimeDialogState.show() } ) @@ -241,7 +241,7 @@ fun EditTransactionSheet( } else { viewModel.updateTransaction( transaction = transaction, - transactionTime = selectedDateTime.value!!, + transactionTime = selectedDateTime.value, transactionType = selectedTransactionType, ) coroutineScope.launch { diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/GoalInfoScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/GoalInfoScreen.kt index 94362e5d..5841e37c 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/GoalInfoScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/GoalInfoScreen.kt @@ -25,9 +25,7 @@ package com.starry.greenstash.ui.screens.info.composables -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -43,7 +41,6 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.NotificationsActive @@ -70,7 +67,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -96,11 +92,11 @@ import com.starry.greenstash.database.goal.GoalPriority.Low import com.starry.greenstash.database.goal.GoalPriority.Normal import com.starry.greenstash.ui.common.DotIndicator import com.starry.greenstash.ui.common.ExpandableTextCard -import com.starry.greenstash.ui.screens.info.viewmodels.InfoViewModel +import com.starry.greenstash.ui.screens.info.InfoViewModel import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.ui.theme.greenstashNumberFont +import com.starry.greenstash.utils.GoalTextUtils import com.starry.greenstash.utils.Utils -import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalMaterial3Api::class) @@ -165,8 +161,10 @@ fun GoalInfoScreen(goalId: String, navController: NavController) { currencySymbol = currencySymbol, targetAmount = goalData.goal.targetAmount, savedAmount = goalData.getCurrentlySavedAmount(), - daysLeftText = viewModel.goalTextUtils.getRemainingDaysText( - context, goalData + daysLeftText = GoalTextUtils.getRemainingDaysText( + context = context, + goalItem = goalData, + datePattern = viewModel.getDateStyle().pattern ), progress = progressPercent.toFloat() / 100 ) @@ -400,12 +398,7 @@ fun GoalNotesCard(notesText: String) { ) } -@ExperimentalCoroutinesApi -@ExperimentalAnimationApi -@ExperimentalComposeUiApi -@ExperimentalFoundationApi -@ExperimentalMaterial3Api -@ExperimentalMaterialApi + @Composable @Preview fun GoalInfoPV() { diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItem.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItem.kt index efa41a62..96c6b3eb 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItem.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/composables/TransactionItem.kt @@ -76,8 +76,8 @@ import com.starry.greenstash.MainActivity import com.starry.greenstash.R import com.starry.greenstash.database.transaction.Transaction import com.starry.greenstash.database.transaction.TransactionType -import com.starry.greenstash.ui.screens.info.viewmodels.InfoViewModel -import com.starry.greenstash.ui.screens.settings.viewmodels.ThemeMode +import com.starry.greenstash.ui.screens.info.InfoViewModel +import com.starry.greenstash.ui.screens.settings.ThemeMode import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.utils.getActivity import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/viewmodels/InputViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/input/InputViewModel.kt similarity index 98% rename from app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/viewmodels/InputViewModel.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/input/InputViewModel.kt index d6be6632..4148ca0d 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/viewmodels/InputViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/input/InputViewModel.kt @@ -23,7 +23,7 @@ */ -package com.starry.greenstash.ui.screens.dwscreen.viewmodels +package com.starry.greenstash.ui.screens.input import android.content.Context import android.graphics.Bitmap @@ -41,7 +41,7 @@ import com.starry.greenstash.database.goal.Goal import com.starry.greenstash.database.goal.GoalDao import com.starry.greenstash.database.goal.GoalPriority import com.starry.greenstash.reminder.ReminderManager -import com.starry.greenstash.ui.screens.settings.viewmodels.DateStyle +import com.starry.greenstash.ui.screens.settings.DateStyle import com.starry.greenstash.utils.ImageUtils import com.starry.greenstash.utils.PreferenceUtil import com.starry.greenstash.utils.Utils diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/IconPickerDialog.kt b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/IconPickerDialog.kt index 54d253e0..60b002d1 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/IconPickerDialog.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/IconPickerDialog.kt @@ -70,9 +70,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import com.starry.greenstash.R -import com.starry.greenstash.ui.screens.dwscreen.viewmodels.IconItem -import com.starry.greenstash.ui.screens.dwscreen.viewmodels.IconsState -import com.starry.greenstash.ui.screens.dwscreen.viewmodels.InputViewModel +import com.starry.greenstash.ui.screens.input.IconItem +import com.starry.greenstash.ui.screens.input.IconsState +import com.starry.greenstash.ui.screens.input.InputViewModel import com.starry.greenstash.ui.theme.greenstashFont diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt index 7e0ac7bb..84bcb92d 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt @@ -32,9 +32,9 @@ import android.net.Uri import android.os.Build import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -57,7 +57,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check @@ -85,13 +84,13 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector @@ -120,6 +119,7 @@ import com.airbnb.lottie.compose.LottieCompositionResult import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition +import com.maxkeppeker.sheets.core.models.base.UseCaseState import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeler.sheets.calendar.CalendarDialog import com.maxkeppeler.sheets.calendar.models.CalendarConfig @@ -134,7 +134,7 @@ import com.starry.greenstash.R import com.starry.greenstash.database.goal.GoalPriority import com.starry.greenstash.ui.common.SelectableChipGroup import com.starry.greenstash.ui.navigation.DrawerScreens -import com.starry.greenstash.ui.screens.dwscreen.viewmodels.InputViewModel +import com.starry.greenstash.ui.screens.input.InputViewModel import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.utils.ImageUtils import com.starry.greenstash.utils.Utils @@ -143,18 +143,16 @@ import com.starry.greenstash.utils.hasNotificationPermission import com.starry.greenstash.utils.toToast import com.starry.greenstash.utils.validateAmount import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.time.LocalDate import java.time.format.DateTimeFormatter -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun InputScreen(editGoalId: String?, navController: NavController) { val context = LocalContext.current - val haptic = LocalHapticFeedback.current val viewModel: InputViewModel = hiltViewModel() val coroutineScope = rememberCoroutineScope() val snackBarHostState = remember { SnackbarHostState() } @@ -313,100 +311,34 @@ fun InputScreen(editGoalId: String?, navController: NavController) { .verticalScroll(scrollState, reverseScrolling = true), ) { Spacer(modifier = Modifier.height(12.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .height(220.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - contentAlignment = Alignment.Center - ) { - Box( - modifier = Modifier - .fillMaxWidth(0.82f) - .height(190.dp) - .border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(16.dp) - ) - .clip(RoundedCornerShape(16.dp)) - ) { - AsyncImage( - model = ImageRequest.Builder(context).data(goalImage) - .crossfade(enable = true).build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } - } - - ExtendedFloatingActionButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 24.dp) - .tapTarget( - precedence = 0, - title = TextDefinition( - text = stringResource(id = R.string.input_pick_image_onboarding_title), - textStyle = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - fontFamily = greenstashFont, - color = MaterialTheme.colorScheme.onSecondaryContainer - ), - description = TextDefinition( - text = stringResource(id = R.string.input_pick_image_onboarding_desc), - textStyle = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer, - fontFamily = greenstashFont - ), - tapTargetStyle = TapTargetStyle( - backgroundColor = MaterialTheme.colorScheme.secondaryContainer, - tapTargetHighlightColor = MaterialTheme.colorScheme.onSecondaryContainer, - backgroundAlpha = 1f, - ), - ), - onClick = { - photoPicker.launch( - PickVisualMediaRequest( - ActivityResultContracts.PickVisualMedia.ImageOnly - ) - ) - }, - elevation = FloatingActionButtonDefaults.elevation(4.dp), - containerColor = MaterialTheme.colorScheme.primary - ) { - Row { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_image), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(id = R.string.input_pick_image_fab), - modifier = Modifier.padding(top = 2.dp), - color = MaterialTheme.colorScheme.onPrimary, - fontFamily = greenstashFont - ) - } - } - } - Text( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp, bottom = 20.dp, start = 30.dp, end = 30.dp), - text = stringResource(id = R.string.input_page_quote), - textAlign = TextAlign.Center, - fontSize = 13.sp, - fontFamily = greenstashFont + GoalImagePicker( + goalImage = goalImage, photoPicker = photoPicker, + fabModifier = Modifier.tapTarget( + precedence = 0, + title = TextDefinition( + text = stringResource(id = R.string.input_pick_image_onboarding_title), + textStyle = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + fontFamily = greenstashFont, + color = MaterialTheme.colorScheme.onSecondaryContainer + ), + description = TextDefinition( + text = stringResource(id = R.string.input_pick_image_onboarding_desc), + textStyle = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + fontFamily = greenstashFont + ), + tapTargetStyle = TapTargetStyle( + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + tapTargetHighlightColor = MaterialTheme.colorScheme.onSecondaryContainer, + backgroundAlpha = 1f, + ), + ), ) + InputQuoteText() // New Goal Quote Text. + Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally @@ -437,8 +369,11 @@ fun InputScreen(editGoalId: String?, navController: NavController) { ), ) Spacer(modifier = Modifier.height(10.dp)) + GoalPriorityMenu(viewModel = viewModel) + Spacer(modifier = Modifier.height(10.dp)) + GoalReminderMenu( viewModel = viewModel, context = context, @@ -466,145 +401,12 @@ fun InputScreen(editGoalId: String?, navController: NavController) { ), ) ) - Spacer(modifier = Modifier.height(18.dp)) - - OutlinedTextField( - value = viewModel.state.goalTitleText, - onValueChange = { newText -> - viewModel.state = viewModel.state.copy(goalTitleText = newText) - }, - modifier = Modifier.fillMaxWidth(0.86f), - label = { - Text( - text = stringResource(id = R.string.input_text_title), - fontFamily = greenstashFont - ) - }, - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_title), - contentDescription = null - ) - }, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.onBackground, - ), - shape = RoundedCornerShape(14.dp), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - ) - - Spacer(modifier = Modifier.height(10.dp)) - - OutlinedTextField( - value = viewModel.state.targetAmount, - onValueChange = { newText -> - viewModel.state = - viewModel.state.copy( - targetAmount = Utils.getValidatedNumber( - newText - ) - ) - }, - modifier = Modifier.fillMaxWidth(0.86f), - label = { - Text( - text = stringResource(id = R.string.input_text_amount), - fontFamily = greenstashFont - ) - }, - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_amount), - contentDescription = null - ) - }, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.onBackground, - ), - shape = RoundedCornerShape(14.dp), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) - - Spacer(modifier = Modifier.height(10.dp)) - - val interactionSource = remember { MutableInteractionSource() } - - OutlinedTextField( - value = viewModel.state.deadline, - onValueChange = { newText -> - viewModel.state = viewModel.state.copy(deadline = newText) - }, - modifier = Modifier - .fillMaxWidth(0.86f) - .combinedClickable( - onClick = { calenderState.show() }, - onLongClick = { - haptic.performHapticFeedback( - HapticFeedbackType.LongPress - ) - if (viewModel.state.deadline.isNotEmpty()) { - showRemoveDeadlineDialog.value = true - } - }, - interactionSource = interactionSource, - indication = null - ), - label = { - Text( - text = stringResource(id = R.string.input_deadline), - fontFamily = greenstashFont - ) - }, - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_deadline), - contentDescription = null - ) - }, - colors = OutlinedTextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.onSurface, - disabledBorderColor = MaterialTheme.colorScheme.onBackground, - disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, - //For Icons - disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - shape = RoundedCornerShape(14.dp), - enabled = false, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - ) - - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(14.dp)) - OutlinedTextField( - value = viewModel.state.additionalNotes, - onValueChange = { newText -> - viewModel.state = viewModel.state.copy(additionalNotes = newText) - }, - modifier = Modifier.fillMaxWidth(0.86f), - label = { - Text( - text = stringResource(id = R.string.input_additional_notes), - fontFamily = greenstashFont - ) - }, - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_additional_notes), - contentDescription = null - ) - }, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.onBackground, - ), - shape = RoundedCornerShape(14.dp), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + InputTextFields( + viewModel = viewModel, + calenderState = calenderState, + showRemoveDeadlineDialog = showRemoveDeadlineDialog ) Spacer(modifier = Modifier.height(18.dp)) @@ -650,8 +452,93 @@ fun InputScreen(editGoalId: String?, navController: NavController) { } } + +@Composable +private fun GoalImagePicker( + goalImage: Any?, + photoPicker: ActivityResultLauncher, + fabModifier: Modifier // To be used for onboarding tap target. +) { + val context = LocalContext.current + Box( + modifier = Modifier + .fillMaxWidth() + .height(220.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .fillMaxWidth(0.82f) + .height(190.dp) + .border( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(16.dp) + ) + .clip(RoundedCornerShape(16.dp)) + ) { + AsyncImage( + model = ImageRequest.Builder(context).data(goalImage) + .crossfade(enable = true).build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } + } + + ExtendedFloatingActionButton( + modifier = fabModifier + .align(Alignment.BottomEnd) + .padding(end = 24.dp), + onClick = { + photoPicker.launch( + PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.ImageOnly + ) + ) + }, + elevation = FloatingActionButtonDefaults.elevation(4.dp), + containerColor = MaterialTheme.colorScheme.primary + ) { + Row { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_image), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.input_pick_image_fab), + modifier = Modifier.padding(top = 2.dp), + color = MaterialTheme.colorScheme.onPrimary, + fontFamily = greenstashFont + ) + } + } + } +} + @Composable -fun GoalIconPicker( +private fun InputQuoteText() { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 20.dp, start = 30.dp, end = 30.dp), + text = stringResource(id = R.string.input_page_quote), + textAlign = TextAlign.Center, + fontSize = 13.sp, + fontFamily = greenstashFont + ) +} + +@Composable +private fun GoalIconPicker( goalIcon: ImageVector, onClick: () -> Unit, // To be used for onboarding tap target. @@ -705,7 +592,7 @@ fun GoalIconPicker( @Composable -fun GoalPriorityMenu(viewModel: InputViewModel) { +private fun GoalPriorityMenu(viewModel: InputViewModel) { Card( modifier = Modifier.fillMaxWidth(0.86f), colors = CardDefaults.cardColors( @@ -748,7 +635,7 @@ fun GoalPriorityMenu(viewModel: InputViewModel) { @Composable -fun GoalReminderMenu( +private fun GoalReminderMenu( context: Context, viewModel: InputViewModel, snackBarHostState: SnackbarHostState, @@ -838,8 +725,158 @@ fun GoalReminderMenu( } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun InputTextFields( + viewModel: InputViewModel, + calenderState: UseCaseState, + showRemoveDeadlineDialog: MutableState +) { + val haptic = LocalHapticFeedback.current + val textFeildSpacing = 8.dp + + OutlinedTextField( + value = viewModel.state.goalTitleText, + onValueChange = { newText -> + viewModel.state = viewModel.state.copy(goalTitleText = newText) + }, + modifier = Modifier.fillMaxWidth(0.86f), + label = { + Text( + text = stringResource(id = R.string.input_text_title), + fontFamily = greenstashFont + ) + }, + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_title), + contentDescription = null + ) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.onBackground, + ), + shape = RoundedCornerShape(14.dp), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + ) + + Spacer(modifier = Modifier.height(textFeildSpacing)) + + OutlinedTextField( + value = viewModel.state.targetAmount, + onValueChange = { newText -> + viewModel.state = + viewModel.state.copy( + targetAmount = Utils.getValidatedNumber( + newText + ) + ) + }, + modifier = Modifier.fillMaxWidth(0.86f), + label = { + Text( + text = stringResource(id = R.string.input_text_amount), + fontFamily = greenstashFont + ) + }, + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_amount), + contentDescription = null + ) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.onBackground, + ), + shape = RoundedCornerShape(14.dp), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + + Spacer(modifier = Modifier.height(textFeildSpacing)) + + val interactionSource = remember { MutableInteractionSource() } + + OutlinedTextField( + value = viewModel.state.deadline, + onValueChange = { newText -> + viewModel.state = viewModel.state.copy(deadline = newText) + }, + modifier = Modifier + .fillMaxWidth(0.86f) + .combinedClickable( + onClick = { calenderState.show() }, + onLongClick = { + haptic.performHapticFeedback( + HapticFeedbackType.LongPress + ) + if (viewModel.state.deadline.isNotEmpty()) { + showRemoveDeadlineDialog.value = true + } + }, + interactionSource = interactionSource, + indication = null + ), + label = { + Text( + text = stringResource(id = R.string.input_deadline), + fontFamily = greenstashFont + ) + }, + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_deadline), + contentDescription = null + ) + }, + colors = OutlinedTextFieldDefaults.colors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledBorderColor = MaterialTheme.colorScheme.onBackground, + disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, + //For Icons + disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + shape = RoundedCornerShape(14.dp), + enabled = false, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + ) + + Spacer(modifier = Modifier.height(textFeildSpacing)) + + OutlinedTextField( + value = viewModel.state.additionalNotes, + onValueChange = { newText -> + viewModel.state = viewModel.state.copy(additionalNotes = newText) + }, + modifier = Modifier.fillMaxWidth(0.86f), + label = { + Text( + text = stringResource(id = R.string.input_additional_notes), + fontFamily = greenstashFont + ) + }, + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_additional_notes), + contentDescription = null + ) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.onBackground, + ), + shape = RoundedCornerShape(14.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + ) +} + @Composable -fun GoalAddedOREditedAnimation(editGoalId: String?) { +private fun GoalAddedOREditedAnimation(editGoalId: String?) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally @@ -877,12 +914,7 @@ fun GoalAddedOREditedAnimation(editGoalId: String?) { } } -@ExperimentalCoroutinesApi -@ExperimentalMaterialApi -@ExperimentalAnimationApi -@ExperimentalFoundationApi -@ExperimentalComposeUiApi -@ExperimentalMaterial3Api + @Preview(showBackground = true) @Composable fun InputScreenPV() { diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/settings/viewmodels/SettingsViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/settings/SettingsViewModel.kt similarity index 86% rename from app/src/main/java/com/starry/greenstash/ui/screens/settings/viewmodels/SettingsViewModel.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/settings/SettingsViewModel.kt index 921022d3..d56a611b 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/settings/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/settings/SettingsViewModel.kt @@ -23,7 +23,7 @@ */ -package com.starry.greenstash.ui.screens.settings.viewmodels +package com.starry.greenstash.ui.screens.settings import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme @@ -31,15 +31,26 @@ import androidx.compose.runtime.Composable import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.starry.greenstash.ui.screens.home.viewmodels.GoalCardStyle +import com.starry.greenstash.ui.screens.home.GoalCardStyle import com.starry.greenstash.utils.PreferenceUtil import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +/** + * Enum class for the theme mode of the app. + * [ThemeMode.Light] - Light theme + * [ThemeMode.Dark] - Dark theme + * [ThemeMode.Auto] - Follow system theme + */ enum class ThemeMode { Light, Dark, Auto } +/** + * Sealed class for the date style of the app. + * [DateStyle.DateMonthYear] - Date in the format dd/MM/yyyy + * [DateStyle.YearMonthDate] - Date in the format yyyy/MM/dd + */ sealed class DateStyle(val pattern: String) { data object DateMonthYear : DateStyle("dd/MM/yyyy") data object YearMonthDate : DateStyle("yyyy/MM/dd") @@ -118,6 +129,12 @@ class SettingsViewModel @Inject constructor( PreferenceUtil.APP_LOCK_BOOL, false ) + /** + * Get the current theme of the app, regardless of the system theme. + * This will always return either [ThemeMode.Light] or [ThemeMode.Dark]. + * If user has set the theme to Auto it will return the system theme, + * again Light or Dark instead of [ThemeMode.Auto]. + */ @Composable fun getCurrentTheme(): ThemeMode { return if (theme.value == ThemeMode.Auto) { diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/GoalCardStyle.kt b/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/GoalCardStyle.kt index e2617c77..c5ef9803 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/GoalCardStyle.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/GoalCardStyle.kt @@ -77,9 +77,9 @@ import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.starry.greenstash.MainActivity import com.starry.greenstash.R +import com.starry.greenstash.ui.screens.home.GoalCardStyle import com.starry.greenstash.ui.screens.home.composables.GoalItemClassic import com.starry.greenstash.ui.screens.home.composables.GoalItemCompact -import com.starry.greenstash.ui.screens.home.viewmodels.GoalCardStyle import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.utils.getActivity import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/SettingsScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/SettingsScreen.kt index f9805c4c..89fe69df 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/SettingsScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/SettingsScreen.kt @@ -78,10 +78,12 @@ import androidx.navigation.NavController import com.starry.greenstash.MainActivity import com.starry.greenstash.R import com.starry.greenstash.ui.common.CurrencyPicker +import com.starry.greenstash.ui.common.CurrencyPickerData import com.starry.greenstash.ui.navigation.Screens -import com.starry.greenstash.ui.screens.home.viewmodels.GoalCardStyle -import com.starry.greenstash.ui.screens.settings.viewmodels.DateStyle -import com.starry.greenstash.ui.screens.settings.viewmodels.ThemeMode +import com.starry.greenstash.ui.screens.home.GoalCardStyle +import com.starry.greenstash.ui.screens.settings.DateStyle +import com.starry.greenstash.ui.screens.settings.SettingsViewModel +import com.starry.greenstash.ui.screens.settings.ThemeMode import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.utils.Utils import com.starry.greenstash.utils.getActivity @@ -96,10 +98,6 @@ fun SettingsScreen(navController: NavController) { val viewModel = (context.getActivity() as MainActivity).settingsViewModel val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - lateinit var executor: Executor - lateinit var biometricPrompt: BiometricPrompt - lateinit var promptInfo: BiometricPrompt.PromptInfo - Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { @@ -126,382 +124,402 @@ fun SettingsScreen(navController: NavController) { ) { LazyColumn(modifier = Modifier.padding(it)) { /** Display Settings */ - item { + item { DisplaySettings(viewModel = viewModel, navController = navController) } - // Theme related values. - val themeValue = when (viewModel.getThemeValue()) { - ThemeMode.Light.ordinal -> stringResource(id = R.string.theme_dialog_option1) - ThemeMode.Dark.ordinal -> stringResource(id = R.string.theme_dialog_option2) - else -> stringResource(id = R.string.theme_dialog_option3) - } - val themeDialog = remember { mutableStateOf(false) } - val themeRadioOptions = listOf( - stringResource(id = R.string.theme_dialog_option1), - stringResource(id = R.string.theme_dialog_option2), - stringResource(id = R.string.theme_dialog_option3) - ) - val (selectedThemeOption, onThemeOptionSelected) = remember { - mutableStateOf(themeValue) - } + /** Locales Setting */ + item { LocaleSettings(viewModel = viewModel) } - // Material You related values. - val materialYouSwitch = remember { - mutableStateOf(viewModel.getMaterialYouValue()) - } + /** Security Settings. */ + item { SecuritySettings(viewModel = viewModel) } - val goalStyleValue = when (viewModel.getGoalCardStyleValue()) { - GoalCardStyle.Classic.ordinal -> stringResource(id = R.string.goal_card_option1) - GoalCardStyle.Compact.ordinal -> stringResource(id = R.string.goal_card_option2) - else -> stringResource(id = R.string.goal_card_option1) - } + /** About Setting */ + item { MiscSettings(navController = navController) } + } + } +} - Spacer(modifier = Modifier.height(8.dp)) - - SettingsContainer { - SettingsCategory(title = stringResource(id = R.string.display_settings_title)) - SettingsItem(title = stringResource(id = R.string.theme_setting), - description = themeValue, - icon = ImageVector.vectorResource(id = R.drawable.ic_settings_theme), - onClick = { themeDialog.value = true }) - - SettingsItem( - title = stringResource(id = R.string.material_you_setting), - description = stringResource( - id = R.string.material_you_setting_desc - ), - icon = ImageVector.vectorResource(id = R.drawable.ic_settings_material_you), - switchState = materialYouSwitch, - onCheckChange = { newValue -> - materialYouSwitch.value = newValue - - if (newValue) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - viewModel.setMaterialYou(true) - } else { - materialYouSwitch.value = false - context.getString(R.string.material_you_error) - .toToast(context) - } - } else { - viewModel.setMaterialYou(false) - } - } - ) +@Composable +private fun DisplaySettings(viewModel: SettingsViewModel, navController: NavController) { + val context = LocalContext.current - SettingsItem( - title = stringResource(id = R.string.goal_card_setting), - description = goalStyleValue, - icon = Icons.Filled.Style, - onClick = { navController.navigate(Screens.GoalCardStyle.route) } - ) + // Theme related values. + val themeValue = when (viewModel.getThemeValue()) { + ThemeMode.Light.ordinal -> stringResource(id = R.string.theme_dialog_option1) + ThemeMode.Dark.ordinal -> stringResource(id = R.string.theme_dialog_option2) + else -> stringResource(id = R.string.theme_dialog_option3) + } + val themeDialog = remember { mutableStateOf(false) } + val themeRadioOptions = listOf( + stringResource(id = R.string.theme_dialog_option1), + stringResource(id = R.string.theme_dialog_option2), + stringResource(id = R.string.theme_dialog_option3) + ) + val (selectedThemeOption, onThemeOptionSelected) = remember { + mutableStateOf(themeValue) + } + + // Material You related values. + val materialYouSwitch = remember { + mutableStateOf(viewModel.getMaterialYouValue()) + } - if (themeDialog.value) { - AlertDialog(onDismissRequest = { - themeDialog.value = false - }, title = { + val goalStyleValue = when (viewModel.getGoalCardStyleValue()) { + GoalCardStyle.Classic.ordinal -> stringResource(id = R.string.goal_card_option1) + GoalCardStyle.Compact.ordinal -> stringResource(id = R.string.goal_card_option2) + else -> stringResource(id = R.string.goal_card_option1) + } + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsContainer { + SettingsCategory(title = stringResource(id = R.string.display_settings_title)) + SettingsItem(title = stringResource(id = R.string.theme_setting), + description = themeValue, + icon = ImageVector.vectorResource(id = R.drawable.ic_settings_theme), + onClick = { themeDialog.value = true }) + + SettingsItem( + title = stringResource(id = R.string.material_you_setting), + description = stringResource( + id = R.string.material_you_setting_desc + ), + icon = ImageVector.vectorResource(id = R.drawable.ic_settings_material_you), + switchState = materialYouSwitch, + onCheckChange = { newValue -> + materialYouSwitch.value = newValue + + if (newValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + viewModel.setMaterialYou(true) + } else { + materialYouSwitch.value = false + context.getString(R.string.material_you_error) + .toToast(context) + } + } else { + viewModel.setMaterialYou(false) + } + } + ) + + SettingsItem( + title = stringResource(id = R.string.goal_card_setting), + description = goalStyleValue, + icon = Icons.Filled.Style, + onClick = { navController.navigate(Screens.GoalCardStyle.route) } + ) + + if (themeDialog.value) { + AlertDialog(onDismissRequest = { + themeDialog.value = false + }, title = { + Text( + text = stringResource(id = R.string.theme_dialog_title), + color = MaterialTheme.colorScheme.onSurface, + fontFamily = greenstashFont, + ) + }, text = { + Column( + modifier = Modifier.selectableGroup(), + verticalArrangement = Arrangement.Center, + ) { + themeRadioOptions.forEach { text -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(46.dp) + .selectable( + selected = (text == selectedThemeOption), + onClick = { onThemeOptionSelected(text) }, + role = Role.RadioButton, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = (text == selectedThemeOption), + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colorScheme.primary, + unselectedColor = MaterialTheme.colorScheme.inversePrimary, + disabledSelectedColor = Color.Black, + disabledUnselectedColor = Color.Black + ), + ) Text( - text = stringResource(id = R.string.theme_dialog_title), + text = text, + modifier = Modifier.padding(start = 16.dp), color = MaterialTheme.colorScheme.onSurface, fontFamily = greenstashFont, ) - }, text = { - Column( - modifier = Modifier.selectableGroup(), - verticalArrangement = Arrangement.Center, - ) { - themeRadioOptions.forEach { text -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(46.dp) - .selectable( - selected = (text == selectedThemeOption), - onClick = { onThemeOptionSelected(text) }, - role = Role.RadioButton, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton( - selected = (text == selectedThemeOption), - onClick = null, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.inversePrimary, - disabledSelectedColor = Color.Black, - disabledUnselectedColor = Color.Black - ), - ) - Text( - text = text, - modifier = Modifier.padding(start = 16.dp), - color = MaterialTheme.colorScheme.onSurface, - fontFamily = greenstashFont, - ) - } - } - } - }, confirmButton = { - FilledTonalButton( - onClick = { - themeDialog.value = false - when (selectedThemeOption) { - context.getString(R.string.theme_dialog_option1) -> { - viewModel.setTheme(ThemeMode.Light) - } - - context.getString(R.string.theme_dialog_option2) -> { - viewModel.setTheme(ThemeMode.Dark) - } - - context.getString(R.string.theme_dialog_option3) -> { - viewModel.setTheme(ThemeMode.Auto) - } - } - }, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - ) { - Text( - stringResource(id = R.string.theme_dialog_apply_button), - fontFamily = greenstashFont - ) - } - }, dismissButton = { - TextButton(onClick = { - themeDialog.value = false - }) { - Text( - stringResource(id = R.string.cancel), - fontFamily = greenstashFont - ) - } - }) + } } } - } + }, confirmButton = { + FilledTonalButton( + onClick = { + themeDialog.value = false + when (selectedThemeOption) { + context.getString(R.string.theme_dialog_option1) -> { + viewModel.setTheme(ThemeMode.Light) + } - /** Locales Setting */ - item { - // Date related values. - val dateValue = if (viewModel.getDateStyleValue() == DateStyle.YearMonthDate.pattern + context.getString(R.string.theme_dialog_option2) -> { + viewModel.setTheme(ThemeMode.Dark) + } + + context.getString(R.string.theme_dialog_option3) -> { + viewModel.setTheme(ThemeMode.Auto) + } + } + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) ) { - "YYYY/MM/DD" - } else { - "DD/MM/YYYY" + Text( + stringResource(id = R.string.theme_dialog_apply_button), + fontFamily = greenstashFont + ) } - - val dateDialog = remember { mutableStateOf(false) } - val dateRadioOptions = listOf("DD/MM/YYYY", "YYYY/MM/DD") - val (selectedDateOption, onDateOptionSelected) = remember { - mutableStateOf(dateValue) + }, dismissButton = { + TextButton(onClick = { + themeDialog.value = false + }) { + Text( + stringResource(id = R.string.cancel), + fontFamily = greenstashFont + ) } + }) + } + } +} - // Currency related values. - val currencyDialog = remember { mutableStateOf(false) } - val currencyNames = context.resources.getStringArray(R.array.currency_names) - val currencyValues = context.resources.getStringArray(R.array.currency_values) +@Composable +private fun LocaleSettings(viewModel: SettingsViewModel) { + val context = LocalContext.current + // Date related values. + val dateValue = if (viewModel.getDateStyleValue() == DateStyle.YearMonthDate.pattern + ) { + "YYYY/MM/DD" + } else { + "DD/MM/YYYY" + } - val selectedCurrencyName = remember { - mutableStateOf(currencyNames[currencyValues.indexOf(viewModel.getDefaultCurrencyValue())]) - } + val dateDialog = remember { mutableStateOf(false) } + val dateRadioOptions = listOf("DD/MM/YYYY", "YYYY/MM/DD") + val (selectedDateOption, onDateOptionSelected) = remember { + mutableStateOf(dateValue) + } + // Currency related values. + val currencyDialog = remember { mutableStateOf(false) } + val currencyNames = context.resources.getStringArray(R.array.currency_names) + val currencyValues = context.resources.getStringArray(R.array.currency_values) + + val selectedCurrencyName = remember { + mutableStateOf(currencyNames[currencyValues.indexOf(viewModel.getDefaultCurrencyValue())]) + } - SettingsContainer { - SettingsCategory(title = stringResource(id = R.string.locales_setting_title)) - SettingsItem(title = stringResource(id = R.string.date_format_setting), - description = dateValue, - icon = ImageVector.vectorResource(id = R.drawable.ic_settings_calender), - onClick = { dateDialog.value = true }) - SettingsItem(title = stringResource(id = R.string.preferred_currency_setting), - description = selectedCurrencyName.value, - icon = ImageVector.vectorResource(id = R.drawable.ic_settings_currency), - onClick = { currencyDialog.value = true }) + SettingsContainer { + SettingsCategory(title = stringResource(id = R.string.locales_setting_title)) + SettingsItem(title = stringResource(id = R.string.date_format_setting), + description = dateValue, + icon = ImageVector.vectorResource(id = R.drawable.ic_settings_calender), + onClick = { dateDialog.value = true }) - if (dateDialog.value) { - AlertDialog(onDismissRequest = { - dateDialog.value = false - }, title = { + SettingsItem(title = stringResource(id = R.string.preferred_currency_setting), + description = selectedCurrencyName.value, + icon = ImageVector.vectorResource(id = R.drawable.ic_settings_currency), + onClick = { currencyDialog.value = true }) + + if (dateDialog.value) { + AlertDialog(onDismissRequest = { + dateDialog.value = false + }, title = { + Text( + text = stringResource(id = R.string.date_format_dialog_title), + color = MaterialTheme.colorScheme.onSurface, + fontFamily = greenstashFont + ) + }, text = { + Column( + modifier = Modifier.selectableGroup(), + verticalArrangement = Arrangement.Center, + ) { + dateRadioOptions.forEach { text -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(46.dp) + .selectable( + selected = (text == selectedDateOption), + onClick = { onDateOptionSelected(text) }, + role = Role.RadioButton, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = (text == selectedDateOption), + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colorScheme.primary, + unselectedColor = MaterialTheme.colorScheme.inversePrimary, + disabledSelectedColor = Color.Black, + disabledUnselectedColor = Color.Black + ), + ) Text( - text = stringResource(id = R.string.date_format_dialog_title), + text = text, + modifier = Modifier.padding(start = 16.dp), color = MaterialTheme.colorScheme.onSurface, fontFamily = greenstashFont ) - }, text = { - Column( - modifier = Modifier.selectableGroup(), - verticalArrangement = Arrangement.Center, - ) { - dateRadioOptions.forEach { text -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(46.dp) - .selectable( - selected = (text == selectedDateOption), - onClick = { onDateOptionSelected(text) }, - role = Role.RadioButton, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton( - selected = (text == selectedDateOption), - onClick = null, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.inversePrimary, - disabledSelectedColor = Color.Black, - disabledUnselectedColor = Color.Black - ), - ) - Text( - text = text, - modifier = Modifier.padding(start = 16.dp), - color = MaterialTheme.colorScheme.onSurface, - fontFamily = greenstashFont - ) - } - } - } - }, confirmButton = { - FilledTonalButton( - onClick = { - dateDialog.value = false - when (selectedDateOption) { - "DD/MM/YYYY" -> { - viewModel.setDateStyle(DateStyle.DateMonthYear.pattern) - } - - "YYYY/MM/DD" -> { - viewModel.setDateStyle(DateStyle.YearMonthDate.pattern) - } - } - }, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - ) { - Text( - stringResource(id = R.string.confirm), - fontFamily = greenstashFont - ) - } - }, dismissButton = { - TextButton(onClick = { - dateDialog.value = false - }) { - Text( - stringResource(id = R.string.cancel), - fontFamily = greenstashFont - ) - } - }) - } - - CurrencyPicker( - defaultCurrencyValue = viewModel.getDefaultCurrencyValue() - ?: currencyValues.first(), - currencyNames = currencyNames, - currencyValues = currencyValues, - showBottomSheet = currencyDialog, - onCurrencySelected = { newValue -> - viewModel.setDefaultCurrency(newValue) - selectedCurrencyName.value = - currencyNames[currencyValues.indexOf(newValue)] } - ) + } } - } + }, confirmButton = { + FilledTonalButton( + onClick = { + dateDialog.value = false + when (selectedDateOption) { + "DD/MM/YYYY" -> { + viewModel.setDateStyle(DateStyle.DateMonthYear.pattern) + } - /** Security Settings. */ - item { - val appLockSwitch = remember { mutableStateOf(viewModel.getAppLockValue()) } - - SettingsContainer { - SettingsCategory(title = stringResource(id = R.string.security_settings_title)) - SettingsItem( - title = stringResource(id = R.string.app_lock_setting), - description = stringResource(id = R.string.app_lock_setting_desc), - icon = ImageVector.vectorResource(id = R.drawable.ic_settings_app_lock), - switchState = appLockSwitch, - onCheckChange = { newValue -> - appLockSwitch.value = newValue - - if (newValue) { - val mainActivity = context.getActivity() as MainActivity - executor = ContextCompat.getMainExecutor(context) - biometricPrompt = BiometricPrompt(mainActivity, executor, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError( - errorCode: Int, errString: CharSequence - ) { - super.onAuthenticationError(errorCode, errString) - context.getString(R.string.auth_error).format(errString) - .toToast(context) - // disable preference switch manually on auth error. - appLockSwitch.value = false - } - - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) - context.getString(R.string.auth_successful) - .toToast(context) - mainActivity.mainViewModel.appUnlocked = true - viewModel.setAppLock(true) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - context.getString(R.string.auth_failed).toToast(context) - // disable preference switch manually on auth error. - appLockSwitch.value = false - } - }) - - promptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(context.getString(R.string.bio_lock_title)) - .setSubtitle(context.getString(R.string.bio_lock_subtitle)) - .setAllowedAuthenticators(Utils.getAuthenticators()).build() - - biometricPrompt.authenticate(promptInfo) - } else { - viewModel.setAppLock(false) + "YYYY/MM/DD" -> { + viewModel.setDateStyle(DateStyle.YearMonthDate.pattern) } } + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Text( + stringResource(id = R.string.confirm), + fontFamily = greenstashFont ) } + }, dismissButton = { + TextButton(onClick = { + dateDialog.value = false + }) { + Text( + stringResource(id = R.string.cancel), + fontFamily = greenstashFont + ) + } + }) + } + + CurrencyPicker( + defaultCurrencyValue = viewModel.getDefaultCurrencyValue() + ?: currencyValues.first(), + currencyPickerData = CurrencyPickerData( + currencyNames = currencyNames, + currencyValues = currencyValues, + ), + showBottomSheet = currencyDialog, + onCurrencySelected = { newValue -> + viewModel.setDefaultCurrency(newValue) + selectedCurrencyName.value = + currencyNames[currencyValues.indexOf(newValue)] } + ) + } +} - /** About Setting */ - item { - SettingsContainer { - SettingsCategory(title = stringResource(id = R.string.misc_setting_title)) - SettingsItem( - title = stringResource(id = R.string.license_setting), - description = stringResource(id = R.string.license_setting_desc), - icon = ImageVector.vectorResource(id = R.drawable.ic_settings_osl), - onClick = { navController.navigate(Screens.OSLScreen.route) } - ) - SettingsItem( - title = stringResource(id = R.string.app_info_setting), - description = stringResource(id = R.string.app_info_setting_desc), - icon = ImageVector.vectorResource(id = R.drawable.ic_settings_about), - onClick = { navController.navigate(Screens.AboutScreen.route) } - ) +@Composable +private fun SecuritySettings(viewModel: SettingsViewModel) { + val context = LocalContext.current + val appLockSwitch = remember { mutableStateOf(viewModel.getAppLockValue()) } + + lateinit var executor: Executor + lateinit var biometricPrompt: BiometricPrompt + lateinit var promptInfo: BiometricPrompt.PromptInfo + + SettingsContainer { + SettingsCategory(title = stringResource(id = R.string.security_settings_title)) + SettingsItem( + title = stringResource(id = R.string.app_lock_setting), + description = stringResource(id = R.string.app_lock_setting_desc), + icon = ImageVector.vectorResource(id = R.drawable.ic_settings_app_lock), + switchState = appLockSwitch, + onCheckChange = { newValue -> + appLockSwitch.value = newValue + if (newValue) { + val mainActivity = context.getActivity() as MainActivity + executor = ContextCompat.getMainExecutor(context) + biometricPrompt = BiometricPrompt(mainActivity, executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, errString: CharSequence + ) { + super.onAuthenticationError(errorCode, errString) + context.getString(R.string.auth_error).format(errString) + .toToast(context) + // disable preference switch manually on auth error. + appLockSwitch.value = false + } + + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult + ) { + super.onAuthenticationSucceeded(result) + context.getString(R.string.auth_successful) + .toToast(context) + mainActivity.mainViewModel.appUnlocked = true + viewModel.setAppLock(true) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + context.getString(R.string.auth_failed).toToast(context) + // disable preference switch manually on auth error. + appLockSwitch.value = false + } + }) + + promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(context.getString(R.string.bio_lock_title)) + .setSubtitle(context.getString(R.string.bio_lock_subtitle)) + .setAllowedAuthenticators(Utils.getAuthenticators()).build() + + biometricPrompt.authenticate(promptInfo) + } else { + viewModel.setAppLock(false) } - Spacer(modifier = Modifier.height(2.dp)) } - } + ) + } +} + +@Composable +private fun MiscSettings(navController: NavController) { + SettingsContainer { + SettingsCategory(title = stringResource(id = R.string.misc_setting_title)) + SettingsItem( + title = stringResource(id = R.string.license_setting), + description = stringResource(id = R.string.license_setting_desc), + icon = ImageVector.vectorResource(id = R.drawable.ic_settings_osl), + onClick = { navController.navigate(Screens.OSLScreen.route) } + ) + SettingsItem( + title = stringResource(id = R.string.app_info_setting), + description = stringResource(id = R.string.app_info_setting_desc), + icon = ImageVector.vectorResource(id = R.drawable.ic_settings_about), + onClick = { navController.navigate(Screens.AboutScreen.route) } + ) } + Spacer(modifier = Modifier.height(2.dp)) // Last item padding. } @Composable -fun SettingsContainer(content: @Composable () -> Unit) { +private fun SettingsContainer(content: @Composable () -> Unit) { Card( modifier = Modifier.padding(vertical = 10.dp, horizontal = 12.dp), colors = CardDefaults.cardColors( @@ -517,7 +535,7 @@ fun SettingsContainer(content: @Composable () -> Unit) { } @Composable -fun SettingsCategory(title: String) { +private fun SettingsCategory(title: String) { Text( text = title, color = MaterialTheme.colorScheme.onBackground, diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/welcome/viewmodels/WelcomeViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/welcome/WelcomeViewModel.kt similarity index 97% rename from app/src/main/java/com/starry/greenstash/ui/screens/welcome/viewmodels/WelcomeViewModel.kt rename to app/src/main/java/com/starry/greenstash/ui/screens/welcome/WelcomeViewModel.kt index 96574002..d6dd67ed 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/welcome/viewmodels/WelcomeViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/welcome/WelcomeViewModel.kt @@ -23,7 +23,7 @@ */ -package com.starry.greenstash.ui.screens.welcome.viewmodels +package com.starry.greenstash.ui.screens.welcome import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/welcome/composables/WelcomeScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/welcome/composables/WelcomeScreen.kt index 88ab56e9..b65fb4b8 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/welcome/composables/WelcomeScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/welcome/composables/WelcomeScreen.kt @@ -67,8 +67,9 @@ import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.starry.greenstash.R import com.starry.greenstash.ui.common.CurrencyPicker +import com.starry.greenstash.ui.common.CurrencyPickerData import com.starry.greenstash.ui.navigation.DrawerScreens -import com.starry.greenstash.ui.screens.welcome.viewmodels.WelcomeViewModel +import com.starry.greenstash.ui.screens.welcome.WelcomeViewModel import com.starry.greenstash.ui.theme.greenstashFont @@ -186,8 +187,10 @@ fun WelcomeScreen(navController: NavController) { CurrencyPicker( defaultCurrencyValue = viewModel.getDefaultCurrencyValue() ?: currencyValues.first(), - currencyNames = currencyNames, - currencyValues = currencyValues, + currencyPickerData = CurrencyPickerData( + currencyNames = currencyNames, + currencyValues = currencyValues + ), showBottomSheet = currencyDialog, onCurrencySelected = { newValue -> viewModel.setDefaultCurrency(newValue) diff --git a/app/src/main/java/com/starry/greenstash/ui/theme/Theme.kt b/app/src/main/java/com/starry/greenstash/ui/theme/Theme.kt index a4dc24bd..387b5ee0 100644 --- a/app/src/main/java/com/starry/greenstash/ui/theme/Theme.kt +++ b/app/src/main/java/com/starry/greenstash/ui/theme/Theme.kt @@ -35,8 +35,8 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.LocalContext -import com.starry.greenstash.ui.screens.settings.viewmodels.SettingsViewModel -import com.starry.greenstash.ui.screens.settings.viewmodels.ThemeMode +import com.starry.greenstash.ui.screens.settings.SettingsViewModel +import com.starry.greenstash.ui.screens.settings.ThemeMode private val LightColors = lightColorScheme( diff --git a/app/src/main/java/com/starry/greenstash/utils/GoalTextUtils.kt b/app/src/main/java/com/starry/greenstash/utils/GoalTextUtils.kt index 7de937cf..7ba84908 100644 --- a/app/src/main/java/com/starry/greenstash/utils/GoalTextUtils.kt +++ b/app/src/main/java/com/starry/greenstash/utils/GoalTextUtils.kt @@ -29,13 +29,13 @@ import android.content.Context import com.starry.greenstash.R import com.starry.greenstash.database.core.GoalWithTransactions import com.starry.greenstash.database.goal.Goal -import com.starry.greenstash.ui.screens.settings.viewmodels.DateStyle +import com.starry.greenstash.ui.screens.settings.DateStyle import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit -class GoalTextUtils(private val preferenceUtil: PreferenceUtil) { +object GoalTextUtils { data class CalculatedDays( val remainingDays: Long, @@ -43,7 +43,10 @@ class GoalTextUtils(private val preferenceUtil: PreferenceUtil) { ) fun buildPrimaryText( - context: Context, progressPercent: Int, item: GoalWithTransactions + context: Context, + progressPercent: Int, + goalItem: GoalWithTransactions, + currencyCode: String, ): String { var text: String = when { progressPercent <= 25 -> { @@ -66,35 +69,36 @@ class GoalTextUtils(private val preferenceUtil: PreferenceUtil) { context.getString(R.string.progress_greet5) } } - val defCurrency = preferenceUtil.getString(PreferenceUtil.DEFAULT_CURRENCY_STR, "") text += if (progressPercent < 100) { "\n" + context.getString(R.string.currently_saved_incomplete) } else { "\n" + context.getString(R.string.currently_saved_complete) } text = text.format( - Utils.formatCurrency(item.getCurrentlySavedAmount(), defCurrency!!), - Utils.formatCurrency(item.goal.targetAmount, defCurrency) + Utils.formatCurrency(goalItem.getCurrentlySavedAmount(), currencyCode = currencyCode), + Utils.formatCurrency(goalItem.goal.targetAmount, currencyCode = currencyCode) ) return text } - fun buildSecondaryText(context: Context, item: GoalWithTransactions): String { - val remainingAmount = (item.goal.targetAmount - item.getCurrentlySavedAmount()) + fun buildSecondaryText( + context: Context, + goalItem: GoalWithTransactions, + currencyCode: String, + datePattern: String + ): String { + val remainingAmount = (goalItem.goal.targetAmount - goalItem.getCurrentlySavedAmount()) if ((remainingAmount > 0f)) { - if (item.goal.deadline.isNotEmpty() && item.goal.deadline.isNotBlank()) { - val calculatedDays = calcRemainingDays(item.goal) - val defCurrency = - preferenceUtil.getString(PreferenceUtil.DEFAULT_CURRENCY_STR, "")!! + if (goalItem.goal.deadline.isNotEmpty() && goalItem.goal.deadline.isNotBlank()) { + val calculatedDays = calcRemainingDays(goalItem.goal, datePattern) // build description string. var text = context.getString(R.string.goal_days_left) .format(calculatedDays.parsedEndDate, calculatedDays.remainingDays) + "\n" if (calculatedDays.remainingDays > 2) { text += context.getString(R.string.goal_approx_saving).format( Utils.formatCurrency( - Utils.roundDecimal( - remainingAmount / calculatedDays.remainingDays - ), defCurrency + Utils.roundDecimal(remainingAmount / calculatedDays.remainingDays), + currencyCode = currencyCode ) ) text += context.getString(R.string.goal_approx_saving_day) @@ -105,7 +109,8 @@ class GoalTextUtils(private val preferenceUtil: PreferenceUtil) { Utils.formatCurrency( Utils.roundDecimal( remainingAmount / weeks - ), defCurrency + ), + currencyCode = currencyCode ) }/${ context.getString( @@ -119,7 +124,8 @@ class GoalTextUtils(private val preferenceUtil: PreferenceUtil) { Utils.formatCurrency( Utils.roundDecimal( remainingAmount / months - ), defCurrency + ), + currencyCode = currencyCode ) }/${ context.getString( @@ -139,12 +145,16 @@ class GoalTextUtils(private val preferenceUtil: PreferenceUtil) { } - fun getRemainingDaysText(context: Context, goalItem: GoalWithTransactions): String { + fun getRemainingDaysText( + context: Context, + goalItem: GoalWithTransactions, + datePattern: String + ): String { return if (goalItem.getCurrentlySavedAmount() >= goalItem.goal.targetAmount) { context.getString(R.string.info_card_goal_achieved) } else { if (goalItem.goal.deadline.isNotEmpty() && goalItem.goal.deadline.isNotBlank()) { - val calculatedDays = calcRemainingDays(goalItem.goal) + val calculatedDays = calcRemainingDays(goalItem.goal, datePattern) context.getString(R.string.info_card_remaining_days) .format(calculatedDays.remainingDays) } else { @@ -154,13 +164,10 @@ class GoalTextUtils(private val preferenceUtil: PreferenceUtil) { } - fun calcRemainingDays(goal: Goal): CalculatedDays { + fun calcRemainingDays(goal: Goal, datePattern: String): CalculatedDays { // calculate remaining days between today and endDate (deadline). - val preferredDateFormat = preferenceUtil.getString( - PreferenceUtil.DATE_FORMAT_STR, DateStyle.DateMonthYear.pattern - ) val dateFormatter: DateTimeFormatter = - DateTimeFormatter.ofPattern(preferredDateFormat) + DateTimeFormatter.ofPattern(datePattern) val startDate = LocalDateTime.now().format(dateFormatter) /** @@ -173,11 +180,11 @@ class GoalTextUtils(private val preferenceUtil: PreferenceUtil) { val endDate = when { goal.deadline.split("/").first().length == 2 && - preferredDateFormat != DateStyle.DateMonthYear.pattern -> + datePattern != DateStyle.DateMonthYear.pattern -> reverseDate(goal.deadline) goal.deadline.split("/").first().length == 4 && - preferredDateFormat != DateStyle.YearMonthDate.pattern -> + datePattern != DateStyle.YearMonthDate.pattern -> reverseDate(goal.deadline) else -> goal.deadline diff --git a/app/src/main/java/com/starry/greenstash/utils/PreferenceUtil.kt b/app/src/main/java/com/starry/greenstash/utils/PreferenceUtil.kt index b25a5746..c5dce0b8 100644 --- a/app/src/main/java/com/starry/greenstash/utils/PreferenceUtil.kt +++ b/app/src/main/java/com/starry/greenstash/utils/PreferenceUtil.kt @@ -27,7 +27,7 @@ package com.starry.greenstash.utils import android.content.Context import android.content.SharedPreferences -import com.starry.greenstash.ui.screens.settings.viewmodels.DateStyle +import com.starry.greenstash.ui.screens.settings.DateStyle class PreferenceUtil(context: Context) { diff --git a/app/src/main/java/com/starry/greenstash/widget/GoalWidget.kt b/app/src/main/java/com/starry/greenstash/widget/GoalWidget.kt index 49782ebe..6b3aac89 100644 --- a/app/src/main/java/com/starry/greenstash/widget/GoalWidget.kt +++ b/app/src/main/java/com/starry/greenstash/widget/GoalWidget.kt @@ -100,6 +100,8 @@ class GoalWidget : AppWidgetProvider() { // Set Widget description. val defCurrency = preferenceUtil.getString(PreferenceUtil.DEFAULT_CURRENCY_STR, "")!! + val datePattern = preferenceUtil.getString(PreferenceUtil.DATE_FORMAT_STR, "")!! + val widgetDesc = context.getString(R.string.goal_widget_desc) .format( "${ @@ -112,7 +114,7 @@ class GoalWidget : AppWidgetProvider() { views.setCharSequence(R.id.widgetDesc, "setText", widgetDesc) // Calculate and display savings per day and week if applicable. - handleSavingsPerDuration(context, views, goalItem, defCurrency, preferenceUtil) + handleSavingsPerDuration(context, views, goalItem, defCurrency, datePattern) // Display appropriate views when the goal is achieved. handleGoalAchieved(views, goalItem) @@ -163,18 +165,18 @@ class GoalWidget : AppWidgetProvider() { views: RemoteViews, goalItem: GoalWithTransactions, defCurrency: String, - preferenceUtil: PreferenceUtil + datePattern: String ) { val remainingAmount = (goalItem.goal.targetAmount - goalItem.getCurrentlySavedAmount()) if (remainingAmount > 0f && goalItem.goal.deadline.isNotEmpty()) { - val calculatedDays = GoalTextUtils(preferenceUtil).calcRemainingDays(goalItem.goal) + val calculatedDays = GoalTextUtils.calcRemainingDays(goalItem.goal, datePattern) if (calculatedDays.remainingDays > 2) { val amountDays = "${ Utils.formatCurrency( - Utils.roundDecimal(remainingAmount / calculatedDays.remainingDays), - defCurrency + amount = Utils.roundDecimal(remainingAmount / calculatedDays.remainingDays), + currencyCode = defCurrency ) }/${context.getString(R.string.goal_approx_saving_day)}" views.setCharSequence(R.id.widgetAmountDay, "setText", amountDays) @@ -184,8 +186,8 @@ class GoalWidget : AppWidgetProvider() { if (calculatedDays.remainingDays > 7) { val amountWeeks = "${ Utils.formatCurrency( - Utils.roundDecimal(remainingAmount / (calculatedDays.remainingDays / 7)), - defCurrency + amount = Utils.roundDecimal(remainingAmount / (calculatedDays.remainingDays / 7)), + currencyCode = defCurrency ) }/${context.getString(R.string.goal_approx_saving_week)}" views.setCharSequence(R.id.widgetAmountWeek, "setText", amountWeeks) diff --git a/app/src/main/java/com/starry/greenstash/widget/configuration/WidgetConfigActivity.kt b/app/src/main/java/com/starry/greenstash/widget/configuration/WidgetConfigActivity.kt index 74be435a..8ed43bd3 100644 --- a/app/src/main/java/com/starry/greenstash/widget/configuration/WidgetConfigActivity.kt +++ b/app/src/main/java/com/starry/greenstash/widget/configuration/WidgetConfigActivity.kt @@ -86,14 +86,16 @@ import com.airbnb.lottie.compose.rememberLottieComposition import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.starry.greenstash.MainActivity import com.starry.greenstash.R -import com.starry.greenstash.ui.screens.settings.viewmodels.SettingsViewModel -import com.starry.greenstash.ui.screens.settings.viewmodels.ThemeMode +import com.starry.greenstash.ui.screens.settings.SettingsViewModel +import com.starry.greenstash.ui.screens.settings.ThemeMode import com.starry.greenstash.ui.theme.GreenStashTheme import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.widget.GoalWidget +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay +@AndroidEntryPoint class WidgetConfigActivity : AppCompatActivity() { private val viewModel: WidgetConfigViewModel by viewModels()