From 21b3f83b0f8bf7ae1cbb9ebaf8c0841150bb1739 Mon Sep 17 00:00:00 2001 From: starry-shivam Date: Fri, 29 Mar 2024 13:28:00 +0530 Subject: [PATCH] Improve onboarding process Signed-off-by: starry-shivam --- app/proguard-rules.pro | 11 +- .../com/starry/greenstash/MainViewModel.kt | 2 +- .../greenstash/ui/common/CurrencyPicker.kt | 180 +++++ .../ui/screens/home/composables/HomeScreen.kt | 9 +- .../screens/home/viewmodels/HomeViewModel.kt | 4 +- .../screens/input/composables/InputScreen.kt | 719 ++++++++++-------- .../input/viewmodels/InputViewModel.kt | 15 +- .../settings/composables/SettingsScreen.kt | 2 +- .../settings/viewmodels/SettingsViewModel.kt | 2 +- .../welcome/composables/WelcomeScreen.kt | 181 ++--- .../welcome/viewmodels/WelcomeViewModel.kt | 2 +- .../starry/greenstash/utils/PreferenceUtil.kt | 3 +- app/src/main/res/values-es/strings.xml | 9 +- app/src/main/res/values-tr/strings.xml | 9 +- app/src/main/res/values-zh-rCN/strings.xml | 9 +- app/src/main/res/values/arrays.xml | 2 +- app/src/main/res/values/strings.xml | 11 +- 17 files changed, 744 insertions(+), 426 deletions(-) create mode 100644 app/src/main/java/com/starry/greenstash/ui/common/CurrencyPicker.kt diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b7b20232..919e55e3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -28,4 +28,13 @@ -keep class * implements com.google.gson.JsonDeserializer # Prevent R8 from removing icons used in goal icon picker. --keep class androidx.compose.material.icons.filled.** { *; } \ No newline at end of file +-keep class androidx.compose.material.icons.filled.** { *; } + +# remove Log statements in release builds +-assumenosideeffects class android.util.Log { + public static *** d(...); + public static *** v(...); + public static *** i(...); + public static *** w(...); + public static *** e(...); +} \ No newline at end of file diff --git a/app/src/main/java/com/starry/greenstash/MainViewModel.kt b/app/src/main/java/com/starry/greenstash/MainViewModel.kt index 552419e7..38f9c92a 100644 --- a/app/src/main/java/com/starry/greenstash/MainViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/MainViewModel.kt @@ -82,7 +82,7 @@ class MainViewModel @Inject constructor( _startDestination.value = Screens.WelcomeScreen.route } - delay(100) + delay(150) _isLoading.value = false } } 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 new file mode 100644 index 00000000..969bad8b --- /dev/null +++ b/app/src/main/java/com/starry/greenstash/ui/common/CurrencyPicker.kt @@ -0,0 +1,180 @@ +/** + * 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.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import com.starry.greenstash.R +import com.starry.greenstash.ui.theme.greenstashFont +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@ExperimentalMaterial3Api +@Composable +fun CurrencyPicker( + defaultCurrencyValue: String, + currencyNames: Array, + currencyValues: Array, + showBottomSheet: MutableState, + onCurrencySelected: (String) -> Unit +) { + val defaultCurrencyEntry = currencyNames[currencyValues.indexOf(defaultCurrencyValue)] + val (selectedCurrencyOption, onCurrencyOptionSelected) = remember { + mutableStateOf(defaultCurrencyEntry) + } + val (searchText, onSearchTextChanged) = remember { mutableStateOf("") } + val filteredCurrencies = currencyNames.filter { it.contains(searchText, ignoreCase = true) } + + val coroutineScope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState() + + if (showBottomSheet.value) { + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { + coroutineScope.launch { + sheetState.hide() + delay(300) + withContext(Dispatchers.Main) { + showBottomSheet.value = false + val choice = currencyValues[currencyNames.indexOf(selectedCurrencyOption)] + onCurrencySelected(choice) + } + } + }, + content = { + Column { + OutlinedTextField( + value = searchText, + onValueChange = onSearchTextChanged, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + placeholder = { Text("Search currency") }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search" + ) + }, shape = RoundedCornerShape(12.dp) + ) + Column( + modifier = Modifier + .selectableGroup() + .verticalScroll(rememberScrollState()) + ) { + filteredCurrencies.forEach { text -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(46.dp) + .selectable( + selected = (text == selectedCurrencyOption), + onClick = { + onCurrencyOptionSelected(text) + }, + role = Role.RadioButton + ), + verticalAlignment = Alignment.CenterVertically + ) { + Row(modifier = Modifier.padding(horizontal = 18.dp)) { + RadioButton( + selected = (text == selectedCurrencyOption), + 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 + ) + } + } + } + } + } + Button( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + onClick = { + coroutineScope.launch { + sheetState.hide() + delay(300) + withContext(Dispatchers.Main) { + showBottomSheet.value = false + val choice = + currencyValues[currencyNames.indexOf(selectedCurrencyOption)] + onCurrencySelected(choice) + } + } + } + ) { + Text(stringResource(id = R.string.confirm), fontFamily = greenstashFont) + } + } + ) + } +} \ No newline at end of file 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 7a74fd8d..bbaf0d25 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 @@ -87,8 +87,10 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight @@ -538,13 +540,18 @@ fun FilterButton(text: String, isSelected: Boolean, onClick: () -> Unit) { textColor = MaterialTheme.colorScheme.onSecondaryContainer } + val haptic = LocalHapticFeedback.current + Card( modifier = Modifier .height(60.dp) .padding(6.dp), colors = CardDefaults.cardColors(containerColor = buttonColor), shape = RoundedCornerShape(14.dp), - onClick = onClick + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onClick() + } ) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( 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/viewmodels/HomeViewModel.kt index cff6afac..6534372c 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/viewmodels/HomeViewModel.kt @@ -117,7 +117,7 @@ class HomeViewModel @Inject constructor( private val _showOnboardingTapTargets: MutableState = mutableStateOf( value = preferenceUtil.getBoolean( - PreferenceUtil.SHOW_ONBOARDING_TAP_TARGETS_BOOL, + PreferenceUtil.HOME_SCREEN_ONBOARDING_BOOL, true ) ) @@ -163,7 +163,7 @@ class HomeViewModel @Inject constructor( } fun onboardingTapTargetsShown() { - preferenceUtil.putBoolean(PreferenceUtil.SHOW_ONBOARDING_TAP_TARGETS_BOOL, false) + preferenceUtil.putBoolean(PreferenceUtil.HOME_SCREEN_ONBOARDING_BOOL, false) _showOnboardingTapTargets.value = false } 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 1597f06d..4ca36d51 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 @@ -123,6 +123,9 @@ import com.maxkeppeler.sheets.calendar.CalendarDialog import com.maxkeppeler.sheets.calendar.models.CalendarConfig import com.maxkeppeler.sheets.calendar.models.CalendarSelection import com.maxkeppeler.sheets.calendar.models.CalendarStyle +import com.psoffritti.taptargetcompose.TapTargetCoordinator +import com.psoffritti.taptargetcompose.TapTargetStyle +import com.psoffritti.taptargetcompose.TextDefinition import com.starry.greenstash.BuildConfig import com.starry.greenstash.MainActivity import com.starry.greenstash.R @@ -247,344 +250,403 @@ fun InputScreen(editGoalId: String?, navController: NavController) { showRemoveDeadlineDialog.value = false viewModel.removeDeadLine() }) { - Text(stringResource(id = R.string.confirm), fontFamily = greenstashFont) + Text(stringResource(id = R.string.confirm), fontFamily = greenstashFont) } }, dismissButton = { TextButton(onClick = { showRemoveDeadlineDialog.value = false }) { - Text(stringResource(id = R.string.cancel), fontFamily = greenstashFont) + Text(stringResource(id = R.string.cancel), fontFamily = greenstashFont) } }) } - // Input Screen UI. - Scaffold(modifier = Modifier - .fillMaxSize() - .imePadding(), - snackbarHost = { SnackbarHost(snackBarHostState) }, - topBar = { - TopAppBar( - modifier = Modifier.fillMaxWidth(), - title = { - Text( - text = topBarText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontFamily = greenstashFont - ) - }, navigationIcon = { - IconButton(onClick = { navController.navigateUp() }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null - ) - } - } - ) - }) { paddingValues -> - if (showGoalAddedAnim.value) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val compositionResult: LottieCompositionResult = rememberLottieComposition( - spec = LottieCompositionSpec.RawRes(R.raw.goal_saved_lottie) - ) - val progressAnimation by animateLottieCompositionAsState( - compositionResult.value, - isPlaying = true, - iterations = 1, - speed = 1.4f - ) + // Input Screen UI. ================================================ - Spacer(modifier = Modifier.weight(1f)) + val showTapTargets = remember { mutableStateOf(false) } + LaunchedEffect(key1 = viewModel.showOnboardingTapTargets.value) { + delay(300) // Delay to prevent flickering + showTapTargets.value = viewModel.showOnboardingTapTargets.value + } - LottieAnimation( - composition = compositionResult.value, - progress = progressAnimation, - modifier = Modifier.size(320.dp) - ) - val textStr = if (editGoalId == null) { - stringResource(id = R.string.goal_saved_success) - } else { - stringResource(id = R.string.goad_edit_success) - } - Text( - text = textStr, - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp, - fontFamily = greenstashFont + TapTargetCoordinator( + showTapTargets = showTapTargets.value, + onComplete = { viewModel.onboardingTapTargetsShown() }, + ) { + Scaffold( + modifier = Modifier + .fillMaxSize() + .imePadding(), + snackbarHost = { SnackbarHost(snackBarHostState) }, + topBar = { + TopAppBar( + modifier = Modifier.fillMaxWidth(), + title = { + Text( + text = topBarText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontFamily = greenstashFont + ) + }, navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + } ) - - Spacer(modifier = Modifier.weight(1.4f)) - } - } else { - // Scroll to top when screen is loaded. - val scrollState = rememberScrollState() - LaunchedEffect(key1 = true) { - scrollState.scrollTo(scrollState.maxValue) } + ) { paddingValues -> + if (showGoalAddedAnim.value) { + GoalAddedOREditedAnimation(editGoalId = editGoalId) + } else { + // Scroll to top when screen is loaded. + val scrollState = rememberScrollState() + LaunchedEffect(key1 = true) { + scrollState.scrollTo(scrollState.maxValue) + } - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(paddingValues) - .verticalScroll(scrollState, reverseScrolling = true), - ) { - Spacer(modifier = Modifier.height(12.dp)) - Box( + Column( modifier = Modifier - .fillMaxWidth() - .height(220.dp) + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(paddingValues) + .verticalScroll(scrollState, reverseScrolling = true), ) { + Spacer(modifier = Modifier.height(12.dp)) Box( modifier = Modifier .fillMaxWidth() - .padding(top = 16.dp), - contentAlignment = Alignment.Center + .height(220.dp) ) { Box( modifier = Modifier - .fillMaxWidth(0.82f) - .height(190.dp) - .border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(16.dp) + .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 + ) ) - .clip(RoundedCornerShape(16.dp)) + }, + elevation = FloatingActionButtonDefaults.elevation(4.dp), + containerColor = MaterialTheme.colorScheme.primary ) { - AsyncImage( - model = ImageRequest.Builder(context).data(goalImage) - .crossfade(enable = true).build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) + 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 + ) + } } } - - ExtendedFloatingActionButton( + Text( modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 24.dp), - onClick = { - photoPicker.launch( - PickVisualMediaRequest( - ActivityResultContracts.PickVisualMedia.ImageOnly - ) - ) - }, - elevation = FloatingActionButtonDefaults.elevation(4.dp), - containerColor = MaterialTheme.colorScheme.primary + .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 + ) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - 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), - modifier = Modifier.padding(top = 2.dp), - color = MaterialTheme.colorScheme.onPrimary, - fontFamily = greenstashFont + GoalIconPicker( + goalIcon = goalIcon, + onClick = { showIconPickerDialog.value = true }, + modifier = Modifier.tapTarget( + precedence = 1, + title = TextDefinition( + text = stringResource(id = R.string.input_pick_icon_onboarding_title), + textStyle = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + fontFamily = greenstashFont, + color = MaterialTheme.colorScheme.onSecondaryContainer + ), + description = TextDefinition( + text = stringResource(id = R.string.input_pick_icon_onboarding_desc), + textStyle = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + fontFamily = greenstashFont + ), + tapTargetStyle = TapTargetStyle( + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + tapTargetHighlightColor = MaterialTheme.colorScheme.onSecondaryContainer, + backgroundAlpha = 1f, + ), + ), + ) + Spacer(modifier = Modifier.height(10.dp)) + GoalPriorityMenu(viewModel = viewModel) + Spacer(modifier = Modifier.height(10.dp)) + GoalReminderMenu( + viewModel = viewModel, + context = context, + snackBarHostState = snackBarHostState, + coroutineScope = coroutineScope, + modifier = Modifier.tapTarget( + precedence = 2, + title = TextDefinition( + text = stringResource(id = R.string.input_goal_reminders_onboarding_title), + textStyle = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + fontFamily = greenstashFont, + color = MaterialTheme.colorScheme.onSecondaryContainer + ), + description = TextDefinition( + text = stringResource(id = R.string.input_goal_reminders_onboarding_desc), + textStyle = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + fontFamily = greenstashFont + ), + tapTargetStyle = TapTargetStyle( + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + tapTargetHighlightColor = MaterialTheme.colorScheme.onSecondaryContainer, + backgroundAlpha = 1f, + ), ) - } - } - } + ) + 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), + ) - 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 - ) + Spacer(modifier = Modifier.height(10.dp)) - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - GoalIconPicker(goalIcon = goalIcon, onClick = { - showIconPickerDialog.value = true - }) - Spacer(modifier = Modifier.height(10.dp)) - GoalPriorityMenu(viewModel = viewModel) - Spacer(modifier = Modifier.height(10.dp)) - GoalReminderMenu( - viewModel = viewModel, - context = context, - snackbarHostState = snackBarHostState, - coroutineScope = coroutineScope - ) - 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), - ) + 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)) - - 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)) - Spacer(modifier = Modifier.height(10.dp)) + val interactionSource = remember { MutableInteractionSource() } - 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), + ) - 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 + Spacer(modifier = Modifier.height(10.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, ), - 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), - ) + shape = RoundedCornerShape(14.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + ) - Spacer(modifier = Modifier.height(10.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), - ) + Spacer(modifier = Modifier.height(22.dp)) - Spacer(modifier = Modifier.height(22.dp)) - - Button( - onClick = { - if (viewModel.state.goalTitleText.isEmpty() || viewModel.state.goalTitleText.isBlank()) { - context.getString(R.string.title_empty_err).toToast(context) - } else if (!viewModel.state.targetAmount.validateAmount()) { - context.getString(R.string.amount_empty_err).toToast(context) - } else { - if (editGoalId != null) { - viewModel.editSavingGoal(editGoalId.toLong(), context) + Button( + onClick = { + if (viewModel.state.goalTitleText.isEmpty() || viewModel.state.goalTitleText.isBlank()) { + context.getString(R.string.title_empty_err).toToast(context) + } else if (!viewModel.state.targetAmount.validateAmount()) { + context.getString(R.string.amount_empty_err).toToast(context) } else { - viewModel.addSavingGoal(context) - } + if (editGoalId != null) { + viewModel.editSavingGoal(editGoalId.toLong(), context) + } else { + viewModel.addSavingGoal(context) + } - coroutineScope.launch { - showGoalAddedAnim.value = true - delay(1050) - navController.popBackStack(DrawerScreens.Home.route, true) - navController.navigate(DrawerScreens.Home.route) + coroutineScope.launch { + showGoalAddedAnim.value = true + delay(1050) + navController.popBackStack(DrawerScreens.Home.route, true) + navController.navigate(DrawerScreens.Home.route) + } } - } - }, - modifier = Modifier - .fillMaxWidth(0.86f) - .height(55.dp), - shape = RoundedCornerShape(14.dp), - ) { - Text( - text = buttonText, - color = MaterialTheme.colorScheme.onPrimary, - fontFamily = greenstashFont - ) - } + }, + modifier = Modifier + .fillMaxWidth(0.86f) + .height(55.dp), + shape = RoundedCornerShape(14.dp), + ) { + Text( + text = buttonText, + color = MaterialTheme.colorScheme.onPrimary, + fontFamily = greenstashFont + ) + } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) + } } } } @@ -592,7 +654,12 @@ fun InputScreen(editGoalId: String?, navController: NavController) { } @Composable -fun GoalIconPicker(goalIcon: ImageVector, onClick: () -> Unit) { +fun GoalIconPicker( + goalIcon: ImageVector, + onClick: () -> Unit, + // To be used for onboarding tap target. + modifier: Modifier = Modifier, +) { Card( onClick = onClick, modifier = Modifier.fillMaxWidth(0.86f), @@ -609,7 +676,7 @@ fun GoalIconPicker(goalIcon: ImageVector, onClick: () -> Unit) { .padding(16.dp) ) { Box( - modifier = Modifier + modifier = modifier .size(48.dp) .background( color = MaterialTheme.colorScheme.surface, @@ -697,9 +764,12 @@ fun GoalPriorityMenu(viewModel: InputViewModel) { fun GoalReminderMenu( context: Context, viewModel: InputViewModel, - snackbarHostState: SnackbarHostState, - coroutineScope: CoroutineScope + snackBarHostState: SnackbarHostState, + coroutineScope: CoroutineScope, + // To be used for onboarding tap target. + modifier: Modifier = Modifier ) { + val haptic = LocalHapticFeedback.current var hasNotificationPermission by remember { mutableStateOf(context.hasNotificationPermission()) } val launcher = rememberLauncherForActivityResult( @@ -716,7 +786,7 @@ fun GoalReminderMenu( ) { coroutineScope.launch { val snackBarResult = - snackbarHostState.showSnackbar( + snackBarHostState.showSnackbar( message = context.getString(R.string.notification_permission_error), actionLabel = "Open Settings" ) @@ -745,11 +815,16 @@ fun GoalReminderMenu( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - Text(text = "Saving Reminders", fontSize = 18.sp, fontFamily = greenstashFont) + Text( + text = stringResource(id = R.string.input_goal_reminder), + fontSize = 18.sp, + fontFamily = greenstashFont + ) Spacer(modifier = Modifier.width(14.dp)) Switch( checked = viewModel.state.reminder, onCheckedChange = { newValue -> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) viewModel.state = viewModel.state.copy(reminder = newValue) // Ask for notification permission if android ver > 13. if (newValue && @@ -758,12 +833,52 @@ fun GoalReminderMenu( ) { launcher.launch(Manifest.permission.POST_NOTIFICATIONS) } - } + }, + modifier = modifier ) } } } +@Composable +fun GoalAddedOREditedAnimation(editGoalId: String?) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val compositionResult: LottieCompositionResult = rememberLottieComposition( + spec = LottieCompositionSpec.RawRes(R.raw.goal_saved_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) + ) + val textStr = if (editGoalId == null) { + stringResource(id = R.string.goal_saved_success) + } else { + stringResource(id = R.string.goad_edit_success) + } + Text( + text = textStr, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + fontFamily = greenstashFont + ) + + Spacer(modifier = Modifier.weight(1.4f)) + } +} + @ExperimentalCoroutinesApi @ExperimentalMaterialApi @ExperimentalAnimationApi diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/input/viewmodels/InputViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/input/viewmodels/InputViewModel.kt index e487d9df..a4c5bb94 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/input/viewmodels/InputViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/input/viewmodels/InputViewModel.kt @@ -32,6 +32,7 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -105,6 +106,14 @@ class InputViewModel @Inject constructor( private var iconSearchJob: Job? = null + private val _showOnboardingTapTargets: MutableState = mutableStateOf( + value = preferenceUtil.getBoolean( + PreferenceUtil.INPUT_SCREEN_ONBOARDING_BOOL, + true + ) + ) + val showOnboardingTapTargets: State = _showOnboardingTapTargets + fun addSavingGoal(context: Context) { viewModelScope.launch(Dispatchers.IO) { val goal = Goal( @@ -133,7 +142,6 @@ class InputViewModel @Inject constructor( goalId: Long, onEditDataSet: (goalImage: Bitmap?, goalIconId: String?) -> Unit ) { - println("Goal ID: $goalId") viewModelScope.launch(Dispatchers.IO) { val goal = goalDao.getGoalById(goalId)!! withContext(Dispatchers.Main) { @@ -235,4 +243,9 @@ class InputViewModel @Inject constructor( return lines } + fun onboardingTapTargetsShown() { + preferenceUtil.putBoolean(PreferenceUtil.INPUT_SCREEN_ONBOARDING_BOOL, false) + _showOnboardingTapTargets.value = false + } + } \ No newline at end of file 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 db74345c..44e9828a 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 @@ -303,7 +303,7 @@ fun SettingsScreen(navController: NavController) { mutableStateOf(dateValue) } - val currencyEntries = context.resources.getStringArray(R.array.currency_entries) + val currencyEntries = context.resources.getStringArray(R.array.currency_names) val currencyValues = context.resources.getStringArray(R.array.currency_values) val currencyValue = currencyEntries[ 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/viewmodels/SettingsViewModel.kt index ea2b881e..921022d3 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/viewmodels/SettingsViewModel.kt @@ -111,7 +111,7 @@ class SettingsViewModel @Inject constructor( ) fun getDefaultCurrencyValue() = preferenceUtil.getString( - PreferenceUtil.DEFAULT_CURRENCY_STR, "$" + PreferenceUtil.DEFAULT_CURRENCY_STR, "USD" ) fun getAppLockValue() = preferenceUtil.getBoolean( 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 38abb8b7..cbc8c55e 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 @@ -25,6 +25,7 @@ package com.starry.greenstash.ui.screens.welcome.composables +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -37,28 +38,24 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.RadioButton -import androidx.compose.material3.RadioButtonDefaults import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -71,22 +68,24 @@ import com.airbnb.lottie.compose.LottieCompositionSpec 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.navigation.DrawerScreens import com.starry.greenstash.ui.screens.welcome.viewmodels.WelcomeViewModel import com.starry.greenstash.ui.theme.greenstashFont +@ExperimentalMaterial3Api @Composable fun WelcomeScreen(navController: NavController) { val context = LocalContext.current val viewModel: WelcomeViewModel = hiltViewModel() - val currencyEntries = context.resources.getStringArray(R.array.currency_entries) + val currencyDialog = remember { mutableStateOf(false) } + val currencyNames = context.resources.getStringArray(R.array.currency_names) val currencyValues = context.resources.getStringArray(R.array.currency_values) - val currencyValue = currencyEntries[currencyValues.indexOf(viewModel.getDefaultCurrencyValue())] - val currencyDialog = remember { mutableStateOf(false) } - val (selectedCurrencyOption, onCurrencyOptionSelected) = remember { - mutableStateOf(currencyValue) + + val selectedCurrencyName = remember { + mutableStateOf(currencyNames[currencyValues.indexOf(viewModel.getDefaultCurrencyValue())]) } Column( @@ -94,6 +93,7 @@ fun WelcomeScreen(navController: NavController) { .fillMaxSize() .background(MaterialTheme.colorScheme.background) .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center ) { val compositionResult: LottieCompositionResult = rememberLottieComposition( @@ -106,57 +106,76 @@ fun WelcomeScreen(navController: NavController) { speed = 1f ) - Spacer(modifier = Modifier.weight(1f)) - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { LottieAnimation( composition = compositionResult.value, progress = progressAnimation, - modifier = Modifier.size(300.dp), + modifier = Modifier.size(320.dp), enableMergePaths = true ) } - Text( - text = stringResource(id = R.string.welcome_screen_text), - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - fontFamily = greenstashFont, - textAlign = TextAlign.Center, + + Card( modifier = Modifier .fillMaxWidth() - .padding(start = 40.dp, end = 35.dp) - ) - - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + .padding(horizontal = 24.dp, vertical = 14.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + 3.dp + ) + ) ) { - OutlinedButton( - onClick = { currencyDialog.value = true }, + + Column( modifier = Modifier - .padding(top = 50.dp, bottom = 16.dp) - .height(50.dp) - .fillMaxWidth(0.8f), - shape = RoundedCornerShape(12.dp), + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = currencyValue, - fontSize = 16.sp, - fontFamily = greenstashFont, + text = stringResource(id = R.string.welcome_screen_text), fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onPrimaryContainer + fontSize = 14.sp, + fontFamily = greenstashFont, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 12.dp) ) + + Row { + + } + + OutlinedButton( + onClick = { currencyDialog.value = true }, + modifier = Modifier.animateContentSize(), + shape = RoundedCornerShape(12.dp), + ) { + Text( + text = selectedCurrencyName.value, + fontSize = 16.sp, + fontFamily = greenstashFont, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + Spacer(modifier = Modifier.height(8.dp)) } - Button( + } + + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + FilledTonalButton( onClick = { viewModel.saveOnBoardingState(completed = true) navController.popBackStack() navController.navigate(DrawerScreens.Home.route) }, modifier = Modifier - .height(50.dp) - .fillMaxWidth(0.8f), + .fillMaxWidth() + .padding(horizontal = 26.dp), shape = RoundedCornerShape(12.dp) ) { Text( @@ -168,72 +187,18 @@ fun WelcomeScreen(navController: NavController) { } } - Spacer(modifier = Modifier.weight(1f)) + CurrencyPicker( + defaultCurrencyValue = viewModel.getDefaultCurrencyValue() + ?: currencyValues.first(), + currencyNames = currencyNames, + currencyValues = currencyValues, + showBottomSheet = currencyDialog, + onCurrencySelected = { newValue -> + println("Selected currency: $newValue") + viewModel.setDefaultCurrency(newValue) + selectedCurrencyName.value = currencyNames[currencyValues.indexOf(newValue)] + } + ) - if (currencyDialog.value) { - AlertDialog(onDismissRequest = { - currencyDialog.value = false - }, title = { - Text( - text = stringResource(id = R.string.currency_dialog_title), - color = MaterialTheme.colorScheme.onSurface, - fontFamily = greenstashFont, - ) - }, text = { - Column( - modifier = Modifier - .selectableGroup() - .verticalScroll( - rememberScrollState() - ), - verticalArrangement = Arrangement.Center, - ) { - currencyEntries.forEach { text -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(46.dp) - .selectable( - selected = (text == selectedCurrencyOption), - onClick = { onCurrencyOptionSelected(text) }, - role = Role.RadioButton, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton( - selected = (text == selectedCurrencyOption), - 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 = { - TextButton(onClick = { - currencyDialog.value = false - val choice = currencyValues[currencyEntries.indexOf(selectedCurrencyOption)] - viewModel.setDefaultCurrency(choice) - }) { - Text(stringResource(id = R.string.confirm), fontFamily = greenstashFont) - } - }, dismissButton = { - TextButton(onClick = { - currencyDialog.value = false - }) { - Text(stringResource(id = R.string.cancel), fontFamily = greenstashFont) - } - }) - } } } \ No newline at end of file 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/viewmodels/WelcomeViewModel.kt index 63399fc8..96574002 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/viewmodels/WelcomeViewModel.kt @@ -51,6 +51,6 @@ class WelcomeViewModel @Inject constructor( } fun getDefaultCurrencyValue() = preferenceUtil.getString( - PreferenceUtil.DEFAULT_CURRENCY_STR, "$" + PreferenceUtil.DEFAULT_CURRENCY_STR, "USD" ) } \ No newline at end of file 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 e6c0444f..3b390b80 100644 --- a/app/src/main/java/com/starry/greenstash/utils/PreferenceUtil.kt +++ b/app/src/main/java/com/starry/greenstash/utils/PreferenceUtil.kt @@ -47,7 +47,8 @@ class PreferenceUtil(context: Context) { const val GOAL_FILTER_SORT_TYPE_INT = "goal_filter_sort_type" // Onboarding preferences - const val SHOW_ONBOARDING_TAP_TARGETS_BOOL = "show_onboarding_tap_targets" + const val HOME_SCREEN_ONBOARDING_BOOL = "show_home_screen_onboarding" + const val INPUT_SCREEN_ONBOARDING_BOOL = "show_input_onboarding" } private var prefs: SharedPreferences diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6ec4276f..dc6dcefd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -94,9 +94,16 @@ ¡Es momento de ahorrar! Editar Meta de Ahorro Selecciona ícono para tu objetivo. + …O si el minimalismo es tu estilo, agrega un icono para tu objetivo. + Los iconos de objetivo se usarán en las tarjetas de objetivo con estilo compacto, Configuración > Estilo de Tarjeta de Objetivo Selecciona un ícono - Imagen de la meta + Imagen de la meta + ¡Agrega imágenes a tus objetivos para darles un aspecto personalizado! + Haz clic en el botón de acción flotante para agregar una imagen a tu objetivo. Selecciona la prioridad para este objetivo. + Recordatorios de ahorro + ¡Habilita los recordatorios para mantenerte actualizado sobre tus objetivos! + Se enviarán recordatorios diarios, semanalmente o según la prioridad del objetivo. Título del Ahorro ¡Oops! el título está vacío. Meta diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 30d1a68a..56c6701e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -94,10 +94,17 @@ Birikim yapma zamanı! Birikim Hedefini Düzenle Hedefiniz için bir simge seçin. + …Ya da minimalizm tarzınızsa, hedefiniz için bir simge ekleyin. + Hedef simgeleri, kompakt stil hedef kartlarında kullanılacaktır, Ayarlar > Hedef Kartı Stili Bir simge seçin - Hedef Resmi + Hedef Resmi + Hedeflerinize kişisel bir görünüm kazandırmak için resimler ekleyin! + Hedefinize bir resim eklemek için hareketli işlem düğmesine tıklayın. Merdivenin tamamını görmek zorunda değilsin, sadece ilk adımı at. – Martin Luther King, Jr. Bu hedef için öncelik seçin. + Tasarruf Hatırlatıcıları + Hedeflerinizle Güncel Kalabilmek için Hatırlatıcıları Etkinleştirin! + Hatırlatıcılar, hedef önceliğine bağlı olarak günlük, haftalık veya yarı haftalık olarak gönderilecektir. Hedef Başlığı Hoop! başlık boş. Hedef Tutar diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 37fc3b3f..a4816689 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -94,10 +94,17 @@ 省钱时间到! 编辑省钱目标 为您的目标选择一个图标。 + …或者如果简约风格是您的风格,请为您的目标添加图标。 + 目标图标将用于紧凑样式的目标卡上,设置 > 目标卡样式 选择图标 - 目标图片 + 目标图片 + 为您的目标添加图片,赋予它们个性化的外观! + 点击浮动操作按钮为您的目标添加图片。 千里之行,始于足下 为此目标选择优先级。 + 储蓄提醒 + 启用提醒,及时了解您的目标进展! + 根据目标优先级,将每日、半周或每周发送提醒。 目标标题 哇!尚无标题。 目标金额 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 6e47282c..37d35d77 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,6 +1,6 @@ - + US Dollar ($) Canadian Dollar (CA$) Euro (€) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4489c57..0e293483 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,7 +21,7 @@ Hello! Please select your preferred currency to get started. - Get Started + Let\'s Get Started! Saving Goals @@ -94,10 +94,17 @@ It\'s time to save! Edit Saving Goal Select icon for your goal. + …Or if minimalism is your style, add an icon for your goal. + Goal icons will be used on compact syled goal cards, Settings > Goal Card Style Select an icon - Goal Image + Goal Image + Add images to your goals to give them a personalized look! + Click on the floating action button to add an image to your goal. You don’t have to see the whole staircase, just take the first step. – Martin Luther King, Jr. Select priority for this goal. + Saving Reminders + Enable Reminders to Stay Updated on Your Goals! + Reminders will be sent daily, semiweekly, or weekly, depending on goal priority. Goal Title Woops! title is empty. Target Amount