diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 251f6f11..b268ef36 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,14 +4,6 @@
-
-
-
-
-
-
-
-
diff --git a/.idea/other.xml b/.idea/other.xml
index 0d3a1fbb..4604c446 100644
--- a/.idea/other.xml
+++ b/.idea/other.xml
@@ -179,17 +179,6 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/build.gradle b/app/build.gradle
index e182b715..dca6f808 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -11,14 +11,14 @@ apply plugin: 'com.mikepenz.aboutlibraries.plugin'
android {
namespace 'com.starry.greenstash'
- compileSdk 34
+ compileSdk 35
defaultConfig {
applicationId "com.starry.greenstash"
minSdk 24
- targetSdk 34
- versionCode 380
- versionName "3.8.0"
+ targetSdk 35
+ versionCode 381
+ versionName "3.8.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -86,10 +86,10 @@ dependencies {
// Android core components.
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.3'
implementation 'androidx.activity:activity-compose:1.9.0'
- implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.2"
- implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.3"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3"
implementation "androidx.navigation:navigation-compose:2.7.7"
// Jetpack compose.
implementation "androidx.compose.ui:ui"
@@ -138,8 +138,8 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
// Testing components.
testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4"
debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest"
diff --git a/app/src/main/java/com/starry/greenstash/backup/BackupManager.kt b/app/src/main/java/com/starry/greenstash/backup/BackupManager.kt
index dad8a20f..0a06244a 100644
--- a/app/src/main/java/com/starry/greenstash/backup/BackupManager.kt
+++ b/app/src/main/java/com/starry/greenstash/backup/BackupManager.kt
@@ -27,20 +27,19 @@ package com.starry.greenstash.backup
import android.content.Context
import android.content.Intent
-import android.graphics.Bitmap
import android.util.Log
-import androidx.annotation.Keep
import androidx.core.content.FileProvider
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
import com.starry.greenstash.BuildConfig
-import com.starry.greenstash.database.core.GoalWithTransactions
+import com.starry.greenstash.backup.BackupType.CSV
+import com.starry.greenstash.backup.BackupType.JSON
import com.starry.greenstash.database.goal.GoalDao
import com.starry.greenstash.utils.updateText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.time.LocalDateTime
+import java.util.Locale
+import java.util.UUID
/**
* Handles all backup & restore related functionalities.
@@ -51,43 +50,18 @@ import java.time.LocalDateTime
*/
class BackupManager(private val context: Context, private val goalDao: GoalDao) {
- /**
- * Instance of [Gson] with custom type adaptor applied for serializing
- * and deserializing [Bitmap] fields.
- */
- private val gsonInstance = GsonBuilder()
- .registerTypeAdapter(Bitmap::class.java, BitmapTypeAdapter())
- .setDateFormat(ISO8601_DATE_FORMAT)
- .create()
-
companion object {
- /** Backup schema version. */
- const val BACKUP_SCHEMA_VERSION = 1
/** Authority for using file provider API. */
private const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider"
- /** An ISO-8601 date format for Gson */
- private const val ISO8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
-
/** Backup folder name inside cache directory. */
private const val BACKUP_FOLDER_NAME = "backups"
}
- /**
- * Model for backup json data, containing current schema version
- * and timestamp when backup was created.
- *
- * @param version backup schema version.
- * @param timestamp timestamp when backup was created.
- * @param data list of [GoalWithTransactions] to be backed up.
- */
- @Keep
- data class BackupJsonModel(
- val version: Int = BACKUP_SCHEMA_VERSION,
- val timestamp: Long,
- val data: List
- )
+ // Converters for different backup types.
+ private val goalToJsonConverter = GoalToJSONConverter()
+ private val goalToCsvConverter = GoalToCSVConverter()
/**
* Logger function with pre-applied tag.
@@ -103,51 +77,64 @@ class BackupManager(private val context: Context, private val goalDao: GoalDao)
*
* @return a chooser [Intent] for newly created backup file.
*/
- suspend fun createDatabaseBackup(): Intent = withContext(Dispatchers.IO) {
- log("Fetching goals from database and serialising into json...")
+ suspend fun createDatabaseBackup(backupType: BackupType): Intent = withContext(Dispatchers.IO) {
+ log("Fetching goals from database and serialising into ${backupType.name}...")
val goalsWithTransactions = goalDao.getAllGoals()
- val jsonString = gsonInstance.toJson(
- BackupJsonModel(
- timestamp = System.currentTimeMillis(),
- data = goalsWithTransactions
- )
- )
-
- log("Creating backup json file inside cache directory...")
- val fileName = "GreenStash-Backup(${System.currentTimeMillis()}).json"
+ val backupString = when (backupType) {
+ BackupType.JSON -> goalToJsonConverter.convertToJson(goalsWithTransactions)
+ BackupType.CSV -> goalToCsvConverter.convertToCSV(goalsWithTransactions)
+ }
+
+ log("Creating a ${backupType.name} file inside cache directory...")
+ val fileName = "GreenStash-(${UUID.randomUUID()}).${backupType.name.lowercase(Locale.US)}"
val file = File(File(context.cacheDir, BACKUP_FOLDER_NAME).apply { mkdir() }, fileName)
- file.updateText(jsonString)
+ file.updateText(backupString)
val uri = FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file)
log("Building and returning chooser intent for backup file.")
+ val intentType = when (backupType) {
+ BackupType.JSON -> "application/json"
+ BackupType.CSV -> "text/csv"
+ }
return@withContext Intent(Intent.ACTION_SEND).apply {
- type = "application/json"
+ type = intentType
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_STREAM, uri)
putExtra(Intent.EXTRA_SUBJECT, "Greenstash Backup")
putExtra(Intent.EXTRA_TEXT, "Created at ${LocalDateTime.now()}")
}.let { intent -> Intent.createChooser(intent, fileName) }
-
}
/**
- * Restores a database backup by deserializing the backup json string
+ * Restores a database backup by deserializing the backup json or csv string
* and saving goals and transactions back into the database.
*
- * @param jsonString a valid backup json as sting.
+ * @param backupString a valid backup json or csv string.
* @param onFailure callback to be called if [BackupManager] failed parse the json string.
* @param onSuccess callback to be called after backup was successfully restored.
*/
suspend fun restoreDatabaseBackup(
- jsonString: String,
+ backupString: String,
+ backupType: BackupType = BackupType.JSON,
onFailure: () -> Unit,
- onSuccess: () -> Unit
+ onSuccess: () -> Unit,
) = withContext(Dispatchers.IO) {
+ log("Parsing backup file...")
+ when (backupType) {
+ BackupType.JSON -> restoreJsonBackup(backupString, onFailure, onSuccess)
+ BackupType.CSV -> restoreCsvBackup(backupString, onFailure, onSuccess)
+ }
+ }
- // Parse json string.
- log("Parsing backup json file...")
- val backupData: BackupJsonModel? = try {
- gsonInstance.fromJson(jsonString, BackupJsonModel::class.java)
+ // Restores json backup by converting json string into [BackupJsonModel] and
+ // then inserting goals and transactions into the database.
+ private suspend fun restoreJsonBackup(
+ backupString: String,
+ onFailure: () -> Unit,
+ onSuccess: () -> Unit
+ ) {
+ val backupData = try {
+ goalToJsonConverter.convertFromJson(backupString)
} catch (exc: Exception) {
log("Failed to parse backup json file! Err: ${exc.message}")
exc.printStackTrace()
@@ -156,12 +143,47 @@ class BackupManager(private val context: Context, private val goalDao: GoalDao)
if (backupData?.data == null) {
withContext(Dispatchers.Main) { onFailure() }
- return@withContext
+ return
+ }
+
+ log("Inserting goals & transactions into the database...")
+ goalDao.insertGoalWithTransactions(backupData.data)
+ withContext(Dispatchers.Main) { onSuccess() }
+ }
+
+ // Restores csv backup by converting csv string into [GoalWithTransactions] list.
+ private suspend fun restoreCsvBackup(
+ backupString: String,
+ onFailure: () -> Unit,
+ onSuccess: () -> Unit
+ ) {
+ val backupData = try {
+ goalToCsvConverter.convertFromCSV(backupString)
+ } catch (exc: Exception) {
+ log("Failed to parse backup csv file! Err: ${exc.message}")
+ exc.printStackTrace()
+ null
+ }
+
+ if (backupData?.data == null) {
+ withContext(Dispatchers.Main) { onFailure() }
+ return
}
- // Insert goal & transaction data into database.
log("Inserting goals & transactions into the database...")
- goalDao.insertGoalWithTransaction(backupData.data)
+ goalDao.insertGoalWithTransactions(backupData.data)
withContext(Dispatchers.Main) { onSuccess() }
}
-}
\ No newline at end of file
+
+}
+
+
+/**
+ * Type of backup file.
+ *
+ * @property JSON for JSON backup file.
+ * @property CSV for CSV backup file.
+ *
+ * @see [BackupManager]
+ */
+enum class BackupType { JSON, CSV }
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/backup/GoalToCSVConverter.kt b/app/src/main/java/com/starry/greenstash/backup/GoalToCSVConverter.kt
new file mode 100644
index 00000000..5f0e6514
--- /dev/null
+++ b/app/src/main/java/com/starry/greenstash/backup/GoalToCSVConverter.kt
@@ -0,0 +1,199 @@
+/**
+ * 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.backup
+
+import androidx.annotation.Keep
+import com.starry.greenstash.database.core.GoalWithTransactions
+import com.starry.greenstash.database.goal.Goal
+import com.starry.greenstash.database.goal.GoalPriority
+import com.starry.greenstash.database.transaction.Transaction
+import com.starry.greenstash.database.transaction.TransactionType
+import java.io.StringWriter
+
+/**
+ * Converts [GoalWithTransactions] data to CSV format and vice versa.
+ */
+class GoalToCSVConverter {
+
+ companion object {
+ /** Backup schema version. */
+ const val BACKUP_SCHEMA_VERSION = 1
+
+ /** CSV delimiter. */
+ const val CSV_DELIMITER = ","
+ }
+
+ /**
+ * Model for backup CSV data, containing current schema version
+ * and timestamp when backup was created.
+ *
+ * @param version backup schema version.
+ * @param timestamp timestamp when backup was created.
+ * @param data list of [GoalWithTransactions] to be backed up.
+ */
+ @Keep
+ data class BackupCSVModel(
+ val version: Int = BACKUP_SCHEMA_VERSION,
+ val timestamp: Long,
+ val data: List
+ )
+
+ /**
+ * Converts the given [GoalWithTransactions] list into a CSV string.
+ *
+ * @param goalWithTransactions List of [GoalWithTransactions] to convert to CSV.
+ * @return A CSV-formatted string.
+ */
+ fun convertToCSV(goalWithTransactions: List): String {
+ val writer = StringWriter()
+ writer.appendLine("Schema Version,$BACKUP_SCHEMA_VERSION")
+ writer.appendLine("Timestamp,${System.currentTimeMillis()}")
+ writer.appendLine(
+ // Goal columns
+ "Goal ID," +
+ "Title," +
+ "Target Amount," +
+ "Deadline," +
+ "Priority," +
+ "Reminder," +
+ "Goal Icon ID," +
+ "Archived," +
+ "Additional Notes," +
+ // Transaction columns
+ "Transaction ID," +
+ "Type," +
+ "Timestamp," +
+ "Amount," +
+ "Notes"
+ )
+ goalWithTransactions.forEach { goalWithTransaction ->
+ val goal = goalWithTransaction.goal
+ val transactions = goalWithTransaction.transactions
+
+ println("Goal: ${goal.title}")
+ println("Transactions: ${transactions.size}")
+
+ if (transactions.isEmpty()) {
+ writer.appendLine(
+ listOf(
+ goal.goalId,
+ goal.title,
+ goal.targetAmount,
+ goal.deadline,
+ goal.priority.name,
+ goal.reminder,
+ goal.goalIconId ?: "",
+ goal.archived,
+ goal.additionalNotes,
+ "",
+ "",
+ "",
+ "",
+ ""
+ ).joinToString(separator = CSV_DELIMITER)
+ )
+ } else {
+ transactions.forEach { transaction ->
+ writer.appendLine(
+ listOf(
+ goal.goalId,
+ goal.title,
+ goal.targetAmount,
+ goal.deadline,
+ goal.priority.name,
+ goal.reminder,
+ goal.goalIconId ?: "",
+ goal.archived,
+ goal.additionalNotes,
+ transaction.transactionId,
+ transaction.type.name,
+ transaction.timeStamp,
+ transaction.amount,
+ transaction.notes
+ ).joinToString(separator = CSV_DELIMITER)
+ )
+ }
+ }
+ }
+
+ return writer.toString()
+ }
+
+ /**
+ * Converts a CSV string back into a list of [GoalWithTransactions].
+ *
+ * @param csv The CSV string to convert.
+ * @return A [BackupCSVModel] containing the version, timestamp, and data.
+ */
+ fun convertFromCSV(csv: String): BackupCSVModel {
+ val lines = csv.lines()
+ val version = lines[0].split(CSV_DELIMITER)[1].toInt()
+ val timestamp = lines[1].split(CSV_DELIMITER)[1].toLong()
+ val data = mutableListOf()
+
+ lines.drop(3).forEach { line ->
+ if (line.isBlank()) return@forEach
+ val columns = line.split(CSV_DELIMITER)
+
+ val goal = Goal(
+ title = columns[1],
+ targetAmount = columns[2].toDouble(),
+ deadline = columns[3],
+ priority = GoalPriority.valueOf(columns[4]),
+ reminder = columns[5].toBoolean(),
+ goalIconId = columns[6].ifEmpty { null },
+ archived = columns[7].toBoolean(),
+ additionalNotes = columns[8],
+ goalImage = null
+ ).apply { goalId = columns[0].toLong() }
+
+ val transaction = if (columns[9].isNotEmpty()) {
+ Transaction(
+ ownerGoalId = columns[0].toLong(),
+ type = TransactionType.valueOf(columns[10]),
+ timeStamp = columns[11].toLong(),
+ amount = columns[12].toDouble(),
+ notes = columns[13]
+ ).apply { transactionId = columns[9].toLong() }
+ } else null
+
+ val existingGoalWithTransactions = data.find { it.goal.goalId == goal.goalId }
+ if (existingGoalWithTransactions != null && transaction != null) {
+ val mutableTransactions = existingGoalWithTransactions.transactions.toMutableList()
+ mutableTransactions.add(transaction)
+ data[data.indexOf(existingGoalWithTransactions)] =
+ GoalWithTransactions(
+ goal = existingGoalWithTransactions.goal,
+ transactions = mutableTransactions
+ )
+ } else {
+ data.add(GoalWithTransactions(goal, transaction?.let { listOf(it) } ?: emptyList()))
+ }
+ }
+
+ return BackupCSVModel(version, timestamp, data)
+ }
+}
diff --git a/app/src/main/java/com/starry/greenstash/backup/GoalToJSONConverter.kt b/app/src/main/java/com/starry/greenstash/backup/GoalToJSONConverter.kt
new file mode 100644
index 00000000..2bbd6836
--- /dev/null
+++ b/app/src/main/java/com/starry/greenstash/backup/GoalToJSONConverter.kt
@@ -0,0 +1,78 @@
+/**
+ * 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.backup
+
+import android.graphics.Bitmap
+import androidx.annotation.Keep
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.starry.greenstash.database.core.GoalWithTransactions
+
+/**
+ * Converts [GoalWithTransactions] data to JSON format and vice versa.
+ */
+class GoalToJSONConverter {
+
+ /**
+ * Instance of [Gson] with custom type adaptor applied for serializing
+ * and deserializing [Bitmap] fields.
+ */
+ private val gsonInstance = GsonBuilder()
+ .registerTypeAdapter(Bitmap::class.java, BitmapTypeAdapter())
+ .setDateFormat(ISO8601_DATE_FORMAT)
+ .create()
+
+ companion object {
+ /** Backup schema version. */
+ const val BACKUP_SCHEMA_VERSION = 1
+
+ /** An ISO-8601 date format for Gson */
+ private const val ISO8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
+ }
+
+ /**
+ * Model for backup json data, containing current schema version
+ * and timestamp when backup was created.
+ *
+ * @param version backup schema version.
+ * @param timestamp timestamp when backup was created.
+ * @param data list of [GoalWithTransactions] to be backed up.
+ */
+ @Keep
+ data class BackupJsonModel(
+ val version: Int = BACKUP_SCHEMA_VERSION,
+ val timestamp: Long,
+ val data: List
+ )
+
+ fun convertToJson(goalWithTransactions: List): String =
+ gsonInstance.toJson(
+ BackupJsonModel(timestamp = System.currentTimeMillis(), data = goalWithTransactions)
+ )
+
+ fun convertFromJson(json: String): BackupJsonModel =
+ gsonInstance.fromJson(json, BackupJsonModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt b/app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt
index 56a570fd..32fe1b0f 100644
--- a/app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt
+++ b/app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt
@@ -53,7 +53,7 @@ interface GoalDao {
* @param goalsWithTransactions List of GoalWithTransactions.
*/
@Transaction
- suspend fun insertGoalWithTransaction(goalsWithTransactions: List) {
+ suspend fun insertGoalWithTransactions(goalsWithTransactions: List) {
goalsWithTransactions.forEach { goalWithTransactions ->
// Set placeholder id.
goalWithTransactions.goal.goalId = 0L
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 c8a6d77a..e086c7b0 100644
--- a/app/src/main/java/com/starry/greenstash/reminder/ReminderNotificationSender.kt
+++ b/app/src/main/java/com/starry/greenstash/reminder/ReminderNotificationSender.kt
@@ -37,8 +37,8 @@ import com.starry.greenstash.database.goal.GoalPriority
import com.starry.greenstash.reminder.receivers.ReminderDepositReceiver
import com.starry.greenstash.reminder.receivers.ReminderDismissReceiver
import com.starry.greenstash.utils.GoalTextUtils
+import com.starry.greenstash.utils.NumberUtils
import com.starry.greenstash.utils.PreferenceUtil
-import com.starry.greenstash.utils.Utils
/**
@@ -94,8 +94,8 @@ class ReminderNotificationSender(
notification.addAction(
R.drawable.ic_notification_deposit,
"${context.getString(R.string.deposit_button)} ${
- Utils.formatCurrency(
- amount = Utils.roundDecimal(amountDay),
+ NumberUtils.formatCurrency(
+ amount = NumberUtils.roundDecimal(amountDay),
currencyCode = defCurrency
)
}",
@@ -108,8 +108,8 @@ class ReminderNotificationSender(
notification.addAction(
R.drawable.ic_notification_deposit,
"${context.getString(R.string.deposit_button)} ${
- Utils.formatCurrency(
- amount = Utils.roundDecimal(amountSemiWeek),
+ NumberUtils.formatCurrency(
+ amount = NumberUtils.roundDecimal(amountSemiWeek),
currencyCode = defCurrency
)
}",
@@ -122,8 +122,8 @@ class ReminderNotificationSender(
notification.addAction(
R.drawable.ic_notification_deposit,
"${context.getString(R.string.deposit_button)} ${
- Utils.formatCurrency(
- amount = Utils.roundDecimal(amountWeek),
+ NumberUtils.formatCurrency(
+ amount = NumberUtils.roundDecimal(amountWeek),
currencyCode = defCurrency
)
}",
@@ -153,7 +153,12 @@ class ReminderNotificationSender(
.setContentTitle(context.getString(R.string.notification_deposited_title))
.setContentText(
context.getString(R.string.notification_deposited_desc)
- .format(Utils.formatCurrency(Utils.roundDecimal(amount), defCurrency!!))
+ .format(
+ NumberUtils.formatCurrency(
+ NumberUtils.roundDecimal(amount),
+ defCurrency!!
+ )
+ )
)
.setStyle(NotificationCompat.BigTextStyle())
.setContentIntent(createActivityIntent())
diff --git a/app/src/main/java/com/starry/greenstash/reminder/receivers/ReminderDepositReceiver.kt b/app/src/main/java/com/starry/greenstash/reminder/receivers/ReminderDepositReceiver.kt
index 585a6937..e65ff784 100644
--- a/app/src/main/java/com/starry/greenstash/reminder/receivers/ReminderDepositReceiver.kt
+++ b/app/src/main/java/com/starry/greenstash/reminder/receivers/ReminderDepositReceiver.kt
@@ -36,7 +36,7 @@ import com.starry.greenstash.database.transaction.TransactionDao
import com.starry.greenstash.database.transaction.TransactionType
import com.starry.greenstash.reminder.ReminderManager
import com.starry.greenstash.reminder.ReminderNotificationSender
-import com.starry.greenstash.utils.Utils
+import com.starry.greenstash.utils.NumberUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -85,7 +85,7 @@ class ReminderDepositReceiver : BroadcastReceiver() {
ownerGoalId = it.goal.goalId,
type = TransactionType.Deposit,
timeStamp = System.currentTimeMillis(),
- amount = Utils.roundDecimal(depositAmount),
+ amount = NumberUtils.roundDecimal(depositAmount),
notes = ""
)
)
diff --git a/app/src/main/java/com/starry/greenstash/ui/common/TipCard.kt b/app/src/main/java/com/starry/greenstash/ui/common/TipCard.kt
index a117431e..c06ad31b 100644
--- a/app/src/main/java/com/starry/greenstash/ui/common/TipCard.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/common/TipCard.kt
@@ -54,6 +54,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.starry.greenstash.ui.theme.greenstashFont
+
@Composable
fun TipCard(
modifier: Modifier = Modifier,
@@ -61,6 +62,43 @@ fun TipCard(
description: String,
showTipCard: Boolean,
onDismissRequest: () -> Unit
+) {
+ TipCard(
+ modifier = modifier,
+ icon = icon,
+ description = description,
+ showTipCard = showTipCard,
+ showDismissButton = true,
+ onDismissRequest = onDismissRequest
+ )
+}
+
+
+@Composable
+fun TipCardNoDismiss(
+ modifier: Modifier = Modifier,
+ icon: ImageVector = Icons.Filled.Lightbulb,
+ description: String,
+ showTipCard: Boolean,
+) {
+ TipCard(
+ modifier = modifier,
+ icon = icon,
+ description = description,
+ showTipCard = showTipCard,
+ showDismissButton = false,
+ onDismissRequest = {}
+ )
+}
+
+@Composable
+private fun TipCard(
+ modifier: Modifier = Modifier,
+ icon: ImageVector = Icons.Filled.Lightbulb,
+ description: String,
+ showTipCard: Boolean,
+ showDismissButton: Boolean = true,
+ onDismissRequest: () -> Unit
) {
Column(
modifier = Modifier
@@ -102,11 +140,13 @@ fun TipCard(
)
}
Spacer(modifier = Modifier.height(16.dp))
- Button(
- onClick = { onDismissRequest() },
- modifier = Modifier.align(Alignment.End)
- ) {
- Text(text = "OK")
+ if (showDismissButton) {
+ Button(
+ onClick = { onDismissRequest() },
+ modifier = Modifier.align(Alignment.End)
+ ) {
+ Text(text = "OK")
+ }
}
}
}
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 d2d72a19..b7680f23 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
@@ -103,7 +103,7 @@ import com.starry.greenstash.ui.theme.greenstashFont
import com.starry.greenstash.ui.theme.greenstashNumberFont
import com.starry.greenstash.utils.Constants
import com.starry.greenstash.utils.ImageUtils
-import com.starry.greenstash.utils.Utils
+import com.starry.greenstash.utils.NumberUtils
import com.starry.greenstash.utils.weakHapticFeedback
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -257,7 +257,7 @@ private fun ArchivedLazyItem(
ArchivedGoalItem(
title = goalItem.goal.title,
icon = goalIcon,
- savedAmount = Utils.formatCurrency(
+ savedAmount = NumberUtils.formatCurrency(
goalItem.getCurrentlySavedAmount(),
defaultCurrency
),
diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupViewModel.kt
index 39dd899b..1c008cff 100644
--- a/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupViewModel.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupViewModel.kt
@@ -4,6 +4,7 @@ import android.content.Intent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.starry.greenstash.backup.BackupManager
+import com.starry.greenstash.backup.BackupType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -15,17 +16,23 @@ class BackupViewModel @Inject constructor(
private val backupManager: BackupManager
) : ViewModel() {
- fun takeBackup(onComplete: (Intent) -> Unit) {
+ fun takeBackup(backupType: BackupType, onComplete: (Intent) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
- val backupIntent = backupManager.createDatabaseBackup()
+ val backupIntent = backupManager.createDatabaseBackup(backupType)
withContext(Dispatchers.Main) { onComplete(backupIntent) }
}
}
- fun restoreBackup(jsonString: String, onSuccess: () -> Unit, onFailure: () -> Unit) {
+ fun restoreBackup(
+ backupType: BackupType,
+ backupString: String,
+ onSuccess: () -> Unit,
+ onFailure: () -> Unit
+ ) {
viewModelScope.launch(Dispatchers.IO) {
backupManager.restoreDatabaseBackup(
- jsonString = jsonString,
+ backupType = backupType,
+ backupString = backupString,
onSuccess = onSuccess,
onFailure = onFailure
)
diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt
index 9c25a185..fecb5ab8 100644
--- a/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt
@@ -42,6 +42,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -50,32 +52,44 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.surfaceColorAtElevation
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.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.starry.greenstash.R
+import com.starry.greenstash.backup.BackupType
+import com.starry.greenstash.ui.common.TipCardNoDismiss
import com.starry.greenstash.ui.screens.backups.BackupViewModel
import com.starry.greenstash.ui.theme.greenstashFont
import com.starry.greenstash.utils.weakHapticFeedback
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.InputStreamReader
import java.io.Reader
@@ -123,7 +137,11 @@ fun BackupScreen(navController: NavController) {
)
)
}, content = {
- val backupLauncher =
+ val ftpButtonText = remember { mutableStateOf("") }
+ val selectedBackupType = remember { mutableStateOf(BackupType.JSON) }
+ val showFileTypePicker = remember { mutableStateOf(false) }
+
+ val backupRestoreLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let { fileUri ->
context.contentResolver.openInputStream(fileUri)?.let { ips ->
@@ -139,7 +157,9 @@ fun BackupScreen(navController: NavController) {
out.appendRange(buffer, 0, numRead)
}
- viewModel.restoreBackup(jsonString = out.toString(),
+ viewModel.restoreBackup(
+ backupType = selectedBackupType.value,
+ backupString = out.toString(),
onSuccess = {
coroutineScope.launch {
snackBarHostState.showSnackbar(context.getString(R.string.backup_restore_success))
@@ -156,9 +176,56 @@ fun BackupScreen(navController: NavController) {
}
+
+ if (showFileTypePicker.value) {
+ BackupFileTypePicker(
+ showFileTypePicker = showFileTypePicker,
+ buttonText = ftpButtonText.value,
+ onConfirm = { backupType ->
+ // Used for restoring backup inside file picker launcher.
+ selectedBackupType.value = backupType
+
+ if (ftpButtonText.value.isEmpty()) {
+ return@BackupFileTypePicker
+ }
+
+ if (ftpButtonText.value == context.getString(R.string.backup_ftp_create_button)) {
+ viewModel.takeBackup(backupType) { intent ->
+ context.startActivity(intent)
+ }
+ } else {
+ // Restore backup
+ when (backupType) {
+ BackupType.JSON -> {
+ backupRestoreLauncher.launch(arrayOf("application/json"))
+ }
+
+ BackupType.CSV -> {
+ backupRestoreLauncher.launch(
+ arrayOf(
+ "text/csv",
+ "text/comma-separated-values",
+ "application/vnd.ms-excel"
+ )
+ )
+ }
+ }
+ }
+ }
+ )
+ }
+
BackupScreenContent(paddingValues = it,
- onBackupClicked = { viewModel.takeBackup { intent -> context.startActivity(intent) } },
- onRestoreClicked = { backupLauncher.launch(arrayOf("application/json")) }
+ onBackupClicked = {
+ ftpButtonText.value =
+ context.getString(R.string.backup_ftp_create_button)
+ showFileTypePicker.value = true
+ },
+ onRestoreClicked = {
+ ftpButtonText.value =
+ context.getString(R.string.backup_ftp_restore_button)
+ showFileTypePicker.value = true
+ }
)
})
}
@@ -222,7 +289,6 @@ private fun BackupScreenContent(
Spacer(modifier = Modifier.height(14.dp))
-
Row(
modifier = Modifier.fillMaxWidth()
) {
@@ -249,10 +315,137 @@ private fun BackupScreenContent(
shape = RoundedCornerShape(12.dp),
) {
Text(
- text = stringResource(id = R.string.restore_button),
+ text = stringResource(id = R.string.backup_restore_button),
+ fontFamily = greenstashFont
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun BackupFileTypePicker(
+ showFileTypePicker: MutableState,
+ buttonText: String,
+ onConfirm: (BackupType) -> Unit
+) {
+ val sheetState = rememberModalBottomSheetState()
+ val coroutineScope = rememberCoroutineScope()
+
+ ModalBottomSheet(
+ onDismissRequest = {
+ coroutineScope.launch {
+ sheetState.hide()
+ delay(300)
+ showFileTypePicker.value = false
+ }
+ },
+ sheetState = sheetState
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp)
+ ) {
+ val (selectedBackupType, onBackupTypeSelected) = remember {
+ mutableStateOf(BackupType.JSON)
+ }
+
+ Text(
+ text = stringResource(id = R.string.backup_select_file_type),
+ fontFamily = greenstashFont,
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 6.dp)
+ )
+
+ SingleChoiceSegmentedButtonRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 18.dp, vertical = 6.dp)
+ ) {
+ SegmentedButton(
+ selected = selectedBackupType == BackupType.JSON,
+ onClick = { onBackupTypeSelected(BackupType.JSON) },
+ shape = RoundedCornerShape(topStart = 14.dp, bottomStart = 14.dp),
+ label = {
+ Text(
+ text = BackupType.JSON.name, fontFamily = greenstashFont
+ )
+ },
+ icon = {
+ if (selectedBackupType == BackupType.JSON) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ },
+ colors = SegmentedButtonDefaults.colors(
+ activeContentColor = MaterialTheme.colorScheme.onPrimary,
+ activeContainerColor = MaterialTheme.colorScheme.primary,
+ )
+ )
+
+ SegmentedButton(
+ selected = selectedBackupType == BackupType.CSV,
+ onClick = { onBackupTypeSelected(BackupType.CSV) },
+ shape = RoundedCornerShape(topEnd = 14.dp, bottomEnd = 14.dp),
+ label = {
+ Text(
+ text = BackupType.CSV.name,
+ fontFamily = greenstashFont
+ )
+ },
+ icon = {
+ if (selectedBackupType == BackupType.CSV) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ },
+ colors = SegmentedButtonDefaults.colors(
+ activeContentColor = MaterialTheme.colorScheme.onPrimary,
+ activeContainerColor = MaterialTheme.colorScheme.primary,
+ )
+ )
+ }
+
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ TipCardNoDismiss(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
+ icon = ImageVector.vectorResource(id = R.drawable.ic_backup_csv),
+ description = stringResource(id = R.string.backup_csv_note),
+ showTipCard = selectedBackupType == BackupType.CSV,
+ )
+
+ Button(
+ onClick = {
+ onConfirm(selectedBackupType)
+ coroutineScope.launch {
+ sheetState.hide()
+ delay(300)
+ showFileTypePicker.value = false
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ shape = RoundedCornerShape(14.dp),
+ ) {
+ Text(
+ text = buttonText,
fontFamily = greenstashFont
)
}
+
+ Spacer(modifier = Modifier.height(12.dp))
}
}
-}
\ No newline at end of file
+}
+
diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/DWViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/DWViewModel.kt
index 4c94a1c2..2875e15a 100644
--- a/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/DWViewModel.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/screens/dwscreen/DWViewModel.kt
@@ -10,6 +10,7 @@ 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.DateStyle
+import com.starry.greenstash.utils.NumberUtils
import com.starry.greenstash.utils.PreferenceUtil
import com.starry.greenstash.utils.Utils
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -111,7 +112,7 @@ class DWViewModel @Inject constructor(
}
}
- private fun amountToDouble(amount: String) = Utils.roundDecimal(amount.toDouble())
+ private fun amountToDouble(amount: String) = NumberUtils.roundDecimal(amount.toDouble())
private suspend fun getGoalById(goalId: Long) = goalDao.getGoalById(goalId)
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 a079230f..10e32bdb 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
@@ -79,6 +79,7 @@ import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.date_time.DateTimeDialog
+import com.maxkeppeler.sheets.date_time.models.DateTimeConfig
import com.maxkeppeler.sheets.date_time.models.DateTimeSelection
import com.starry.greenstash.R
import com.starry.greenstash.database.transaction.TransactionType
@@ -87,7 +88,7 @@ import com.starry.greenstash.ui.navigation.DrawerScreens
import com.starry.greenstash.ui.navigation.Screens
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.NumberUtils
import com.starry.greenstash.utils.validateAmount
import com.starry.greenstash.utils.weakHapticFeedback
import kotlinx.coroutines.CoroutineScope
@@ -96,6 +97,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.LocalDateTime
+import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@@ -124,6 +126,7 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont
) { newDateTime ->
selectedDateTime.value = newDateTime
},
+ config = DateTimeConfig(locale = Locale.US)
)
Scaffold(
@@ -181,7 +184,7 @@ fun DWScreen(goalId: String, transactionTypeName: String, navController: NavCont
notesValue = viewModel.state.notes,
onAmountChange = { amount ->
viewModel.state =
- viewModel.state.copy(amount = Utils.getValidatedNumber(amount))
+ viewModel.state.copy(amount = NumberUtils.getValidatedNumber(amount))
},
onNotesChange = { notes ->
viewModel.state = viewModel.state.copy(notes = notes)
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 253f46bd..00ef6cc1 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
@@ -49,7 +49,7 @@ 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.NumberUtils
import com.starry.greenstash.utils.getActivity
import com.starry.greenstash.utils.strongHapticFeedback
import com.starry.greenstash.utils.weakHapticFeedback
@@ -171,7 +171,7 @@ fun GoalLazyColumnItem(
GoalItemCompact(
title = item.goal.title,
- savedAmount = Utils.formatCurrency(
+ savedAmount = NumberUtils.formatCurrency(
item.getCurrentlySavedAmount(),
viewModel.getDefaultCurrency()
),
diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/info/InfoViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/info/InfoViewModel.kt
index d808853d..63783b43 100644
--- a/app/src/main/java/com/starry/greenstash/ui/screens/info/InfoViewModel.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/screens/info/InfoViewModel.kt
@@ -36,6 +36,7 @@ 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.DateStyle
+import com.starry.greenstash.utils.NumberUtils
import com.starry.greenstash.utils.PreferenceUtil
import com.starry.greenstash.utils.Utils
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -95,7 +96,7 @@ class InfoViewModel @Inject constructor(
val newTransaction = transaction.copy(
type = TransactionType.valueOf(transactionType),
timeStamp = Utils.getEpochTime(transactionTime),
- amount = Utils.roundDecimal(editGoalState.amount.toDouble()),
+ amount = NumberUtils.roundDecimal(editGoalState.amount.toDouble()),
notes = editGoalState.notes
)
newTransaction.transactionId = transaction.transactionId
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 1f0e0a67..d7af4de5 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
@@ -62,6 +62,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.date_time.DateTimeDialog
+import com.maxkeppeler.sheets.date_time.models.DateTimeConfig
import com.maxkeppeler.sheets.date_time.models.DateTimeSelection
import com.starry.greenstash.R
import com.starry.greenstash.database.transaction.Transaction
@@ -69,13 +70,14 @@ import com.starry.greenstash.database.transaction.TransactionType
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.NumberUtils
import com.starry.greenstash.utils.toToast
import com.starry.greenstash.utils.validateAmount
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.LocalDateTime
+import java.util.Locale
import java.util.TimeZone
@@ -109,7 +111,8 @@ fun EditTransactionSheet(
selection = DateTimeSelection.DateTime(
selectedDate = selectedDateTime.value.toLocalDate(),
selectedTime = selectedDateTime.value.toLocalTime(),
- ) { newDateTime -> selectedDateTime.value = newDateTime }
+ ) { newDateTime -> selectedDateTime.value = newDateTime },
+ config = DateTimeConfig(locale = Locale.US)
)
if (showEditTransaction.value) {
@@ -198,7 +201,7 @@ fun EditTransactionSheet(
onValueChange = { newText ->
viewModel.editGoalState =
viewModel.editGoalState.copy(
- amount = Utils.getValidatedNumber(
+ amount = NumberUtils.getValidatedNumber(
newText
)
)
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 9a42ce3d..839c3a58 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
@@ -99,7 +99,7 @@ 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 com.starry.greenstash.utils.NumberUtils
import com.starry.greenstash.utils.weakHapticFeedback
import kotlinx.coroutines.delay
@@ -238,9 +238,9 @@ fun GoalInfoCard(
progress: Float
) {
val formattedTargetAmount =
- Utils.formatCurrency(Utils.roundDecimal(targetAmount), currencySymbol)
+ NumberUtils.formatCurrency(NumberUtils.roundDecimal(targetAmount), currencySymbol)
val formattedSavedAmount =
- Utils.formatCurrency(Utils.roundDecimal(savedAmount), currencySymbol)
+ NumberUtils.formatCurrency(NumberUtils.roundDecimal(savedAmount), currencySymbol)
val animatedProgress = animateFloatAsState(targetValue = progress, label = "progress")
Card(
diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/input/InputViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/input/InputViewModel.kt
index 8baaa51d..a10ef08f 100644
--- a/app/src/main/java/com/starry/greenstash/ui/screens/input/InputViewModel.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/screens/input/InputViewModel.kt
@@ -43,8 +43,8 @@ import com.starry.greenstash.database.goal.GoalPriority
import com.starry.greenstash.reminder.ReminderManager
import com.starry.greenstash.ui.screens.settings.DateStyle
import com.starry.greenstash.utils.ImageUtils
+import com.starry.greenstash.utils.NumberUtils
import com.starry.greenstash.utils.PreferenceUtil
-import com.starry.greenstash.utils.Utils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -107,7 +107,7 @@ class InputViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
val goal = Goal(
title = state.goalTitleText,
- targetAmount = Utils.roundDecimal(state.targetAmount.toDouble()),
+ targetAmount = NumberUtils.roundDecimal(state.targetAmount.toDouble()),
deadline = state.deadline,
goalImage = if (state.goalImageUri != null) ImageUtils.uriToBitmap(
uri = state.goalImageUri!!, context = context, maxSize = 1024
@@ -152,7 +152,7 @@ class InputViewModel @Inject constructor(
val goal = goalDao.getGoalById(goalId)!!
val newGoal = Goal(
title = state.goalTitleText,
- targetAmount = Utils.roundDecimal(state.targetAmount.toDouble()),
+ targetAmount = NumberUtils.roundDecimal(state.targetAmount.toDouble()),
deadline = state.deadline,
goalImage = if (state.goalImageUri != null) ImageUtils.uriToBitmap(
uri = state.goalImageUri!!, context = context, maxSize = 1024
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 932213b6..144aa0a7 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
@@ -144,7 +144,7 @@ import com.starry.greenstash.ui.navigation.DrawerScreens
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
+import com.starry.greenstash.utils.NumberUtils
import com.starry.greenstash.utils.getActivity
import com.starry.greenstash.utils.hasNotificationPermission
import com.starry.greenstash.utils.toToast
@@ -883,7 +883,7 @@ private fun InputTextFields(
OutlinedTextField(
value = targetAmount,
- onValueChange = { newText -> onAmountChange(Utils.getValidatedNumber(newText)) },
+ onValueChange = { newText -> onAmountChange(NumberUtils.getValidatedNumber(newText)) },
modifier = Modifier.fillMaxWidth(0.86f),
label = {
Text(
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 096fffc4..0735a45d 100644
--- a/app/src/main/java/com/starry/greenstash/utils/GoalTextUtils.kt
+++ b/app/src/main/java/com/starry/greenstash/utils/GoalTextUtils.kt
@@ -87,8 +87,11 @@ object GoalTextUtils {
"\n" + context.getString(R.string.currently_saved_complete)
}
text = text.format(
- Utils.formatCurrency(goalItem.getCurrentlySavedAmount(), currencyCode = currencyCode),
- Utils.formatCurrency(goalItem.goal.targetAmount, currencyCode = currencyCode)
+ NumberUtils.formatCurrency(
+ goalItem.getCurrentlySavedAmount(),
+ currencyCode = currencyCode
+ ),
+ NumberUtils.formatCurrency(goalItem.goal.targetAmount, currencyCode = currencyCode)
)
return text
}
@@ -117,8 +120,8 @@ object GoalTextUtils {
.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),
+ NumberUtils.formatCurrency(
+ NumberUtils.roundDecimal(remainingAmount / calculatedDays.remainingDays),
currencyCode = currencyCode
)
)
@@ -127,8 +130,8 @@ object GoalTextUtils {
val weeks = calculatedDays.remainingDays / 7
text = text.dropLast(1) // remove full stop
text += ", ${
- Utils.formatCurrency(
- Utils.roundDecimal(
+ NumberUtils.formatCurrency(
+ NumberUtils.roundDecimal(
remainingAmount / weeks
),
currencyCode = currencyCode
@@ -142,8 +145,8 @@ object GoalTextUtils {
val months = calculatedDays.remainingDays / 30
text = text.dropLast(1) // remove full stop
text += ", ${
- Utils.formatCurrency(
- Utils.roundDecimal(
+ NumberUtils.formatCurrency(
+ NumberUtils.roundDecimal(
remainingAmount / months
),
currencyCode = currencyCode
diff --git a/app/src/main/java/com/starry/greenstash/utils/Utils.kt b/app/src/main/java/com/starry/greenstash/utils/Utils.kt
index 54ff5e32..842dc707 100644
--- a/app/src/main/java/com/starry/greenstash/utils/Utils.kt
+++ b/app/src/main/java/com/starry/greenstash/utils/Utils.kt
@@ -46,44 +46,6 @@ import java.util.TimeZone
*/
object Utils {
- /**
- * Get validated number from the text.
- *
- * @param text The text to validate
- * @return The validated number
- */
- @Deprecated(
- "Use NumberUtils.getValidatedNumber instead",
- ReplaceWith("NumberUtils.getValidatedNumber(text)")
- )
- fun getValidatedNumber(text: String) = NumberUtils.getValidatedNumber(text)
-
- /**
- * Round the decimal number to two decimal places.
- *
- * @param number The number to round
- * @return The rounded number
- */
- @Deprecated(
- "Use NumberUtils.roundDecimal instead",
- ReplaceWith("NumberUtils.roundDecimal(number)")
- )
- fun roundDecimal(number: Double) = NumberUtils.roundDecimal(number)
-
- /**
- * Format currency based on the currency code.
- *
- * @param amount The amount to format
- * @param currencyCode The currency code
- * @return The formatted currency
- */
- @Deprecated(
- "Use NumberUtils.formatCurrency instead",
- ReplaceWith("NumberUtils.formatCurrency(amount, currencyCode)")
- )
- fun formatCurrency(amount: Double, currencyCode: String) =
- NumberUtils.formatCurrency(amount, currencyCode)
-
/**
* Retrieves the appropriate authenticators based on the Android version.
*
diff --git a/app/src/main/res/drawable/ic_backup_csv.xml b/app/src/main/res/drawable/ic_backup_csv.xml
new file mode 100644
index 00000000..eccc9bc4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_backup_csv.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index e9329852..25b3640c 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -153,7 +153,11 @@
Haz una copia de seguridad de tus datos incluyendo metas, progreso actual, transacciones, etc para recuperarlos cuando quieras.
Nota: La copia de seguridad no incluye la configuración de la app.
Respaldar
- Restaurar
+ Restaurar
+ Seleccionar tipo de archivo de respaldo
+ Nota: Debido a las limitaciones del formato CSV, las imágenes de los objetivos no se respaldarán.
+ Crear Respaldo
+ Restaurar Respaldo
¡La copia de seguridad se ha restaurado correctamente!
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index fc162229..9a6c7283 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -152,7 +152,11 @@
Esegui il backup dei dati dell\'app, compresi tutti gli obiettivi di risparmio, i progressi attuali, le transazioni e così via, e ripristinali facilmente ogni volta che lo desideri.
Ricordati che i backup non includono le impostazioni dell\'app.
Fai il Backup
- Ripristina
+ Ripristina
+ Seleziona il tipo di file di backup
+ Nota: A causa delle limitazioni del formato CSV, le immagini degli obiettivi non verranno salvate nel backup.
+ Crea Backup
+ Ripristina Backup
Backup ripristinato con successo!
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 3be7680c..ebc91d01 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -151,7 +151,11 @@
Faça backup dos dados do aplicativo, incluindo todas as suas metas de economia, progresso atual, transações, etc., e restaure facilmente sempre que quiser.
Observe que os backups não incluem configurações do aplicativo.
Backup
- Restaurar
+ Restaurar
+ Selecionar tipo de arquivo de backup
+ Nota: Devido às limitações do formato CSV, as imagens dos objetivos não serão incluídas no backup.
+ Criar Backup
+ Restaurar Backup
Backup restaurado com sucesso!
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index dcd5956f..c8b587b4 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -153,7 +153,11 @@
Создайте резервную копию данных приложения, включая все цели накопления, текущий прогресс, транзакции и т. д., и легко восстановите ее в любое время.
Обратите внимание, что резервные копии не включают настройки приложения.
Резервное копирование
- Восстановить
+ Восстановить
+ Выберите тип файла резервной копии
+ Примечание: Из-за ограничений формата CSV изображения целей не будут включены в резервную копию.
+ Создать резервную копию
+ Восстановить резервную копию
Резервная копия успешно восстановлена!
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 978eee47..a7134c8b 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -154,7 +154,11 @@
Tüm tasarruf hedefleriniz, mevcut ilerlemeniz, işlemleriniz vb. dahil olmak üzere uygulama verilerini yedekleyin ve istediğiniz zaman kolayca geri yükleyin.
Not: Yedekler uygulama ayarlarını içermez.
Yedekle
- Geri Yükle
+ Geri Yükle
+ Yedekleme dosya türünü seç
+ Not: CSV formatının sınırlamaları nedeniyle, hedef resimleri yedeklenmeyecektir.
+ Yedek Oluştur
+ Yedeği Geri Yükle
Yedek başarıyla geri yüklendi!
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 90af9a37..db4e6ff1 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -152,7 +152,11 @@
备份应用数据——包括您所有的省钱目标、当前进展、收支等等,并且可以随时轻松恢复。
注意:备份不包括应用设置信息。
备份
- 恢复
+ 恢复
+ 选择备份文件类型
+ 注意:由于CSV格式的限制,目标图片将不会备份。
+ 创建备份
+ 恢复备份
备份成功恢复!
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 455c6891..a7865272 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -151,7 +151,11 @@
備份應用程式資料,包括所有的儲蓄目標、目前進度、交易等,並在您需要時可以輕鬆還原。
請注意,備份不包括應用程式設定。
備份
- 還原
+ 還原
+ 選擇備份檔案類型
+ 注意:由於CSV格式的限制,目標圖片將不會備份。
+ 創建備份
+ 恢復備份
備份已成功還原!
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2964e750..2b150657 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -152,7 +152,11 @@
Backup app data including all of your saving goals, current progress, transactions etc and easily restore it whenever you want.
Please note that backups do not include app settings.
Backup
- Restore
+ Restore
+ Select backup file type
+ Note: Due to limitations of the CSV format, goal images will not be backed up.
+ Create Backup
+ Restore Backup
Backup restored successfully!
diff --git a/fastlane/metadata/android/en-US/changelogs/381.txt b/fastlane/metadata/android/en-US/changelogs/381.txt
new file mode 100644
index 00000000..9045e0eb
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/381.txt
@@ -0,0 +1,3 @@
+- Fixed inconsistent behavior of date & time picker dialog in different locales.
+- Bump target SDK version to v35 (Android 15).
+- Update dependencies & some minor improvements.
\ No newline at end of file