diff --git a/app/schemas/com.starry.greenstash.database.core.AppDatabase/7.json b/app/schemas/com.starry.greenstash.database.core.AppDatabase/7.json new file mode 100644 index 00000000..024b5124 --- /dev/null +++ b/app/schemas/com.starry.greenstash.database.core.AppDatabase/7.json @@ -0,0 +1,190 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "ee83f4215d1464f23a88d9e4f44a2300", + "entities": [ + { + "tableName": "saving_goal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `targetAmount` REAL NOT NULL, `deadline` TEXT NOT NULL, `goalImage` BLOB, `additionalNotes` TEXT NOT NULL, `priority` INTEGER NOT NULL DEFAULT 2, `reminder` INTEGER NOT NULL DEFAULT 0, `goalIconId` TEXT DEFAULT 'Image', `archived` INTEGER NOT NULL DEFAULT 0, `goalId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "targetAmount", + "columnName": "targetAmount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "goalImage", + "columnName": "goalImage", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "additionalNotes", + "columnName": "additionalNotes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "2" + }, + { + "fieldPath": "reminder", + "columnName": "reminder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "goalIconId", + "columnName": "goalIconId", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'Image'" + }, + { + "fieldPath": "archived", + "columnName": "archived", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "goalId", + "columnName": "goalId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "goalId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "transaction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ownerGoalId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `timeStamp` INTEGER NOT NULL, `amount` REAL NOT NULL, `notes` TEXT NOT NULL, `transactionId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`ownerGoalId`) REFERENCES `saving_goal`(`goalId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "ownerGoalId", + "columnName": "ownerGoalId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeStamp", + "columnName": "timeStamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "transactionId", + "columnName": "transactionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "transactionId" + ] + }, + "indices": [ + { + "name": "index_transaction_ownerGoalId", + "unique": false, + "columnNames": [ + "ownerGoalId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_transaction_ownerGoalId` ON `${TABLE_NAME}` (`ownerGoalId`)" + } + ], + "foreignKeys": [ + { + "table": "saving_goal", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "ownerGoalId" + ], + "referencedColumns": [ + "goalId" + ] + } + ] + }, + { + "tableName": "widget_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appWidgetId` INTEGER NOT NULL, `goalId` INTEGER NOT NULL, PRIMARY KEY(`appWidgetId`))", + "fields": [ + { + "fieldPath": "appWidgetId", + "columnName": "appWidgetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "goalId", + "columnName": "goalId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "appWidgetId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ee83f4215d1464f23a88d9e4f44a2300')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/starry/greenstash/database/core/AppDatabase.kt b/app/src/main/java/com/starry/greenstash/database/core/AppDatabase.kt index 8a297391..ac0ae7a8 100644 --- a/app/src/main/java/com/starry/greenstash/database/core/AppDatabase.kt +++ b/app/src/main/java/com/starry/greenstash/database/core/AppDatabase.kt @@ -39,14 +39,15 @@ import com.starry.greenstash.database.widget.WidgetData @Database( entities = [Goal::class, Transaction::class, WidgetData::class], - version = 6, + version = 7, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(from = 3, to = 4), AutoMigration(from = 4, to = 5), - AutoMigration(from = 5, to = 6) + AutoMigration(from = 5, to = 6), + AutoMigration(from = 6, to = 7) ] ) @TypeConverters(Converters::class) diff --git a/app/src/main/java/com/starry/greenstash/database/goal/Goal.kt b/app/src/main/java/com/starry/greenstash/database/goal/Goal.kt index 63a17c77..d2308007 100644 --- a/app/src/main/java/com/starry/greenstash/database/goal/Goal.kt +++ b/app/src/main/java/com/starry/greenstash/database/goal/Goal.kt @@ -46,13 +46,13 @@ data class Goal( @ColumnInfo(defaultValue = "2") val priority: GoalPriority, // Added in database schema v4 - @ColumnInfo(defaultValue = "false") + @ColumnInfo(defaultValue = "0") val reminder: Boolean, // Added in database schema v5 @ColumnInfo(defaultValue = "Image") val goalIconId: String?, // Added in database schema v6 - @ColumnInfo(defaultValue = "false") + @ColumnInfo(defaultValue = "0") val archived: Boolean = false ) { @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/archive/composables/ArchiveScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/archive/composables/ArchiveScreen.kt index a8ffb3ef..94ea73af 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/archive/composables/ArchiveScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/archive/composables/ArchiveScreen.kt @@ -25,6 +25,9 @@ package com.starry.greenstash.ui.screens.archive.composables +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -39,18 +42,36 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Face2 import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -58,24 +79,89 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionResult +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.starry.greenstash.R import com.starry.greenstash.ui.screens.archive.ArchiveViewModel import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.ui.theme.greenstashNumberFont +import com.starry.greenstash.utils.weakHapticFeedback +import kotlinx.coroutines.delay +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ArchiveScreen(navController: NavController) { + val view = LocalView.current val viewModel: ArchiveViewModel = hiltViewModel() - ArchivedGoalItem( - title = "Home Decorations", - icon = Icons.Filled.Face2, - savedAmount = "₹10,000", - onRestoreClicked = {}, - onDeleteClicked = {} - ) + + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + val archivedGoals by viewModel.archivedGoals.collectAsState(initial = listOf()) + + Scaffold(modifier = Modifier.fillMaxSize(), + snackbarHost = { SnackbarHost(snackBarHostState) }, + topBar = { + LargeTopAppBar( + title = { + Text( + stringResource(id = R.string.archive_screen_header), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontFamily = greenstashFont + ) + }, + navigationIcon = { + IconButton(onClick = { + view.weakHapticFeedback() + navController.navigateUp() + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surface, + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + if (archivedGoals.isEmpty()) { + var showNoGoalsAnimation by remember { mutableStateOf(false) } + LaunchedEffect(key1 = true, block = { + delay(200) + showNoGoalsAnimation = true + }) + AnimatedVisibility( + visible = showNoGoalsAnimation, + enter = fadeIn(), + exit = fadeOut() + ) { + NoArchivedGoals() + } + } else { + // TODO: Add a message for empty archived goals + } + } + } } @Composable -fun ArchivedGoalItem( +private fun ArchivedGoalItem( title: String, icon: ImageVector?, savedAmount: String, @@ -108,7 +194,7 @@ fun ArchivedGoalItem( modifier = Modifier .size(40.dp) .background( - color = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.primary, shape = CircleShape ) .clip(CircleShape) @@ -120,6 +206,7 @@ fun ArchivedGoalItem( modifier = Modifier .fillMaxSize() .padding(10.dp), + tint = MaterialTheme.colorScheme.onPrimary ) } Spacer(modifier = Modifier.width(16.dp)) @@ -128,7 +215,7 @@ fun ArchivedGoalItem( modifier = Modifier .size(40.dp) .background( - color = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.primary, shape = CircleShape ) .clip(CircleShape) @@ -140,6 +227,7 @@ fun ArchivedGoalItem( modifier = Modifier .fillMaxSize() .padding(10.dp), + tint = MaterialTheme.colorScheme.onPrimary ) } } @@ -166,6 +254,44 @@ fun ArchivedGoalItem( } } +@Composable +private fun NoArchivedGoals() { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val compositionResult: LottieCompositionResult = + rememberLottieComposition( + spec = LottieCompositionSpec.RawRes(R.raw.no_goal_found_lottie) + ) + val progressAnimation by animateLottieCompositionAsState( + compositionResult.value, + isPlaying = true, + iterations = 1, + speed = 1f + ) + + Spacer(modifier = Modifier.weight(1f)) + + LottieAnimation( + composition = compositionResult.value, + progress = { progressAnimation }, + modifier = Modifier.size(320.dp), + enableMergePaths = true + ) + + Text( + text = stringResource(id = R.string.archive_empty), + fontWeight = FontWeight.Medium, + fontFamily = greenstashFont, + fontSize = 18.sp, + modifier = Modifier.padding(start = 12.dp, end = 12.dp), + ) + + Spacer(modifier = Modifier.weight(2f)) + } +} + @Preview @Composable fun ArchiveItemPV() { diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/home/HomeViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/home/HomeViewModel.kt index 6c571904..9d5f4376 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/home/HomeViewModel.kt @@ -138,6 +138,16 @@ class HomeViewModel @Inject constructor( } + fun archiveGoal(goal: Goal) { + viewModelScope.launch(Dispatchers.IO) { + val updatedGoal = goal.copy(archived = true) + goalDao.updateGoal(updatedGoal) + if (reminderManager.isReminderSet(goal.goalId)) { + reminderManager.stopReminder(goal.goalId) + } + } + } + fun deleteGoal(goal: Goal) { viewModelScope.launch(Dispatchers.IO) { goalDao.deleteGoal(goal.goalId) diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/GoalItems.kt b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/GoalItems.kt index e80f22b9..a5e73246 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/GoalItems.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/GoalItems.kt @@ -94,11 +94,13 @@ fun GoalItemClassic( secondaryText: String, goalProgress: Float, goalImage: Bitmap?, + isGoalCompleted: Boolean, onDepositClicked: () -> Unit, onWithdrawClicked: () -> Unit, onInfoClicked: () -> Unit, onEditClicked: () -> Unit, onDeleteClicked: () -> Unit, + onArchivedClicked: () -> Unit ) { val progress by animateFloatAsState(targetValue = goalProgress, label = "goal progress") @@ -166,17 +168,30 @@ fun GoalItemClassic( /** Goal Buttons */ Row(modifier = Modifier.padding(3.dp)) { - - TextButton( - onClick = { onDepositClicked() }, - modifier = Modifier.padding(end = 2.dp) - ) { - Text( - text = stringResource(id = R.string.deposit_button).uppercase(), - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - fontFamily = greenstashFont - ) + if (isGoalCompleted) { + TextButton( + onClick = { onArchivedClicked() }, + modifier = Modifier.padding(end = 2.dp) + ) { + Text( + text = stringResource(id = R.string.archive_button).uppercase(), + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + fontFamily = greenstashFont + ) + } + } else { + TextButton( + onClick = { onDepositClicked() }, + modifier = Modifier.padding(end = 2.dp) + ) { + Text( + text = stringResource(id = R.string.deposit_button).uppercase(), + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + fontFamily = greenstashFont + ) + } } TextButton( onClick = { onWithdrawClicked() }, @@ -233,11 +248,13 @@ fun GoalItemCompact( daysLeftText: String, goalProgress: Float, goalIcon: ImageVector, + isGoalCompleted: Boolean, onDepositClicked: () -> Unit, onWithdrawClicked: () -> Unit, onInfoClicked: () -> Unit, onEditClicked: () -> Unit, onDeleteClicked: () -> Unit, + onArchivedClicked: () -> Unit ) { val coroutineScope = rememberCoroutineScope() val swipeState = rememberSwipeToDismissBoxState( @@ -381,19 +398,36 @@ fun GoalItemCompact( } Row { - IconButton( - onClick = { onDepositClicked() }, - modifier = Modifier - .padding(top = 4.dp) - .offset((10).dp) - .size(54.dp) - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_deposit_plus), - contentDescription = stringResource(id = R.string.deposit_button), - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) + if (isGoalCompleted) { + IconButton( + onClick = { onArchivedClicked() }, + modifier = Modifier + .padding(top = 4.dp) + .offset((10).dp) + .size(54.dp) + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_compact_goal_archve), + contentDescription = stringResource(id = R.string.archive_button), + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } else { + IconButton( + onClick = { onDepositClicked() }, + modifier = Modifier + .padding(top = 4.dp) + .offset((10).dp) + .size(54.dp) + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_compact_goal_deposit), + contentDescription = stringResource(id = R.string.deposit_button), + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + } } IconButton( onClick = { onWithdrawClicked() }, modifier = Modifier @@ -403,7 +437,7 @@ fun GoalItemCompact( ) { Icon( modifier = Modifier.size(20.dp), - imageVector = ImageVector.vectorResource(R.drawable.ic_withdraw_minus), + imageVector = ImageVector.vectorResource(R.drawable.ic_compact_goal_withdraw), contentDescription = stringResource(id = R.string.withdraw_button), tint = MaterialTheme.colorScheme.onSecondaryContainer ) @@ -463,12 +497,14 @@ fun GoalItemsPV() { secondaryText = "You have until 26/05/2023 (85) days left.\nYou need to save around $58.83/day, $416.67/week, $2,500.00/month.", goalProgress = 0.6f, goalImage = null, + isGoalCompleted = false, onDepositClicked = { }, onWithdrawClicked = { }, onInfoClicked = { }, - onEditClicked = { }) { - - } + onEditClicked = { }, + onDeleteClicked = { }, + onArchivedClicked = { }, + ) Spacer(modifier = Modifier.height(10.dp)) @@ -478,11 +514,13 @@ fun GoalItemsPV() { daysLeftText = "Goal Achieved! 🎉", goalProgress = 0.8f, goalIcon = ImageVector.vectorResource(id = R.drawable.ic_nav_rating), + isGoalCompleted = true, onDepositClicked = {}, onWithdrawClicked = {}, onInfoClicked = {}, onEditClicked = {}, - onDeleteClicked = {} + onDeleteClicked = {}, + onArchivedClicked = {}, ) } } \ No newline at end of file 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 b574c9a3..63721ad9 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 @@ -25,7 +25,6 @@ package com.starry.greenstash.ui.screens.home.composables -import android.content.Context import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.material3.SnackbarHostState @@ -34,8 +33,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -53,27 +52,32 @@ import com.starry.greenstash.utils.Utils import com.starry.greenstash.utils.getActivity import com.starry.greenstash.utils.strongHapticFeedback import com.starry.greenstash.utils.weakHapticFeedback +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable fun GoalLazyColumnItem( - context: Context, viewModel: HomeViewModel, item: GoalWithTransactions, snackBarHostState: SnackbarHostState, + coroutineScope: CoroutineScope, navController: NavController, currentIndex: Int ) { + val context = LocalContext.current val settingsVM = (context.getActivity() as MainActivity).settingsViewModel val goalCardStyle = settingsVM.goalCardStyle.observeAsState().value!! - val coroutineScope = rememberCoroutineScope() + val isGoalCompleted = remember(item.goal.goalId) { + item.getCurrentlySavedAmount() >= item.goal.targetAmount + } val progressPercent = remember(item.goal.goalId) { ((item.getCurrentlySavedAmount() / item.goal.targetAmount) * 100).toInt() } val openDeleteDialog = remember { mutableStateOf(false) } + val openArchiveDialog = remember { mutableStateOf(false) } val localView = LocalView.current when (goalCardStyle) { @@ -93,6 +97,7 @@ fun GoalLazyColumnItem( ), goalProgress = progressPercent.toFloat() / 100, goalImage = item.goal.goalImage, + isGoalCompleted = isGoalCompleted, onDepositClicked = { localView.weakHapticFeedback() if (item.getCurrentlySavedAmount() >= item.goal.targetAmount) { @@ -142,6 +147,10 @@ fun GoalLazyColumnItem( onDeleteClicked = { localView.strongHapticFeedback() openDeleteDialog.value = true + }, + onArchivedClicked = { + localView.weakHapticFeedback() + openArchiveDialog.value = true } ) @@ -172,6 +181,7 @@ fun GoalLazyColumnItem( ), goalProgress = progressPercent.toFloat() / 100, goalIcon = goalIcon, + isGoalCompleted = isGoalCompleted, onDepositClicked = { localView.weakHapticFeedback() if (item.getCurrentlySavedAmount() >= item.goal.targetAmount) { @@ -221,6 +231,10 @@ fun GoalLazyColumnItem( onDeleteClicked = { localView.strongHapticFeedback() openDeleteDialog.value = true + }, + onArchivedClicked = { + localView.weakHapticFeedback() + openArchiveDialog.value = true } ) } @@ -228,11 +242,18 @@ fun GoalLazyColumnItem( HomeDialogs( openDeleteDialog = openDeleteDialog, + openArchiveDialog = openArchiveDialog, onDeleteConfirmed = { viewModel.deleteGoal(item.goal) - /*coroutineScope.launch { + coroutineScope.launch { snackBarHostState.showSnackbar(context.getString(R.string.goal_delete_success)) - }*/ + } + }, + onArchiveConfirmed = { + viewModel.archiveGoal(item.goal) + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.goal_archive_success)) + } } ) } diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeDialogs.kt b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeDialogs.kt index fb71eedb..c202ebb7 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeDialogs.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/home/composables/HomeDialogs.kt @@ -25,8 +25,6 @@ package com.starry.greenstash.ui.screens.home.composables -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledTonalButton @@ -36,7 +34,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.sp import com.starry.greenstash.R import com.starry.greenstash.ui.theme.greenstashFont @@ -44,7 +45,9 @@ import com.starry.greenstash.ui.theme.greenstashFont @Composable fun HomeDialogs( openDeleteDialog: MutableState, + openArchiveDialog: MutableState, onDeleteConfirmed: () -> Unit, + onArchiveConfirmed: () -> Unit ) { if (openDeleteDialog.value) { @@ -55,6 +58,7 @@ fun HomeDialogs( text = stringResource(id = R.string.goal_delete_confirmation), color = MaterialTheme.colorScheme.onSurface, fontFamily = greenstashFont, + fontSize = 18.sp ) }, confirmButton = { FilledTonalButton( @@ -77,7 +81,50 @@ fun HomeDialogs( } }, icon = { - Icon(imageVector = Icons.Rounded.Delete, contentDescription = null) + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_goal_delete), + contentDescription = null + ) + } + ) + } + + if (openArchiveDialog.value) { + + AlertDialog(onDismissRequest = { + openArchiveDialog.value = false + }, title = { + Text( + text = stringResource(id = R.string.goal_archive_confirmation), + color = MaterialTheme.colorScheme.onSurface, + fontFamily = greenstashFont, + fontSize = 18.sp + ) + }, confirmButton = { + FilledTonalButton( + onClick = { + openArchiveDialog.value = false + onArchiveConfirmed() + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) { + Text(stringResource(id = R.string.confirm), fontFamily = greenstashFont) + } + }, dismissButton = { + TextButton(onClick = { + openArchiveDialog.value = false + }) { + Text(stringResource(id = R.string.cancel), fontFamily = greenstashFont) + } + }, + icon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_compact_goal_archve), + contentDescription = null + ) } ) } 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 5825fe04..f047c257 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 @@ -110,6 +110,7 @@ import com.starry.greenstash.ui.theme.greenstashFont import com.starry.greenstash.utils.getActivity import com.starry.greenstash.utils.isScrollingUp import com.starry.greenstash.utils.weakHapticFeedback +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.Locale @@ -243,7 +244,8 @@ fun HomeScreen(navController: NavController) { searchTextState = searchTextState, viewModel = viewModel, navController = navController, - snackBarHostState = snackBarHostState + snackBarHostState = snackBarHostState, + coroutineScope = coroutineScope ) } else { AllGoalsList( @@ -251,7 +253,8 @@ fun HomeScreen(navController: NavController) { allGoalState = allGoalState, viewModel = viewModel, navController = navController, - snackBarHostState = snackBarHostState + snackBarHostState = snackBarHostState, + coroutineScope = coroutineScope ) } } @@ -270,19 +273,15 @@ private fun GoalSearchResults( searchTextState: String, viewModel: HomeViewModel, navController: NavController, - snackBarHostState: SnackbarHostState + snackBarHostState: SnackbarHostState, + coroutineScope: CoroutineScope ) { val allGoals = allGoalState.value - val context = LocalContext.current - val filteredList: ArrayList = ArrayList() - - for (goalItem in allGoals) { - if (goalItem.goal.title.lowercase(Locale.getDefault()) - .contains(searchTextState.lowercase(Locale.getDefault())) - ) { - filteredList.add(goalItem) - } + val filteredList = allGoals.filter { goalItem -> + goalItem.goal.title.lowercase(Locale.getDefault()) + .contains(searchTextState.lowercase(Locale.getDefault())) } + if (allGoals.isNotEmpty() && filteredList.isEmpty()) { Column( modifier = Modifier.fillMaxSize(), @@ -333,10 +332,10 @@ private fun GoalSearchResults( val item = filteredList[idx] Box(modifier = Modifier.animateItemPlacement()) { GoalLazyColumnItem( - context = context, viewModel = viewModel, item = item, snackBarHostState = snackBarHostState, + coroutineScope= coroutineScope, navController = navController, currentIndex = idx ) @@ -355,10 +354,10 @@ private fun AllGoalsList( allGoalState: State>, viewModel: HomeViewModel, navController: NavController, - snackBarHostState: SnackbarHostState + snackBarHostState: SnackbarHostState, + coroutineScope: CoroutineScope ) { val allGoals = allGoalState.value - val context = LocalContext.current LazyColumn( modifier = Modifier .fillMaxSize() @@ -373,10 +372,10 @@ private fun AllGoalsList( val item = allGoals[idx] Box(modifier = Modifier.animateItemPlacement()) { GoalLazyColumnItem( - context = context, viewModel = viewModel, item = item, snackBarHostState = snackBarHostState, + coroutineScope = coroutineScope, navController = navController, currentIndex = idx ) 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 77e173f5..ecbff699 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 @@ -160,12 +160,13 @@ fun GoalCardStyle(navController: NavController) { secondaryText = "You have until 26/05/2023 (85) days left.\nYou need to save around $58.83/day, $416.67/week, $2,500.00/month.", goalProgress = 0.6f, goalImage = null, + isGoalCompleted = false, onDepositClicked = { }, onWithdrawClicked = { }, onInfoClicked = { }, - onEditClicked = { }) { - - } + onEditClicked = { }, + onDeleteClicked = { }, + onArchivedClicked = { }) } GoalCardStyle.Compact -> { @@ -175,11 +176,13 @@ fun GoalCardStyle(navController: NavController) { daysLeftText = "12 days left", goalProgress = 0.8f, goalIcon = ImageVector.vectorResource(id = R.drawable.ic_nav_rating), + isGoalCompleted = false, onDepositClicked = {}, onWithdrawClicked = {}, onInfoClicked = {}, onEditClicked = {}, - onDeleteClicked = {} + onDeleteClicked = {}, + onArchivedClicked = { } ) } } diff --git a/app/src/main/res/drawable/ic_compact_goal_archve.xml b/app/src/main/res/drawable/ic_compact_goal_archve.xml new file mode 100644 index 00000000..2895c855 --- /dev/null +++ b/app/src/main/res/drawable/ic_compact_goal_archve.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_deposit_plus.xml b/app/src/main/res/drawable/ic_compact_goal_deposit.xml similarity index 100% rename from app/src/main/res/drawable/ic_deposit_plus.xml rename to app/src/main/res/drawable/ic_compact_goal_deposit.xml diff --git a/app/src/main/res/drawable/ic_withdraw_minus.xml b/app/src/main/res/drawable/ic_compact_goal_withdraw.xml similarity index 100% rename from app/src/main/res/drawable/ic_withdraw_minus.xml rename to app/src/main/res/drawable/ic_compact_goal_withdraw.xml diff --git a/app/src/main/res/drawable/ic_transaction_deposit.xml b/app/src/main/res/drawable/ic_transaction_deposit.xml deleted file mode 100644 index 6d77011a..00000000 --- a/app/src/main/res/drawable/ic_transaction_deposit.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_transaction_withdraw.xml b/app/src/main/res/drawable/ic_transaction_withdraw.xml deleted file mode 100644 index 73163371..00000000 --- a/app/src/main/res/drawable/ic_transaction_withdraw.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c9ea5367..f625d647 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -141,6 +141,10 @@ Do you want to remove deadline from this goal? Tip! You can remove the deadline from existing goals by long-pressing on the deadline field. + + Archived Goals + No archived goals yet. + Backup & Restore Backup app data including all of your saving goals, current progress, transactions etc and easily restore it whenever you want.