diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 21deee2..4ec2c5c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -98,4 +98,8 @@ dependencies { implementation("com.google.dagger:hilt-android:2.57.1") ksp("com.google.dagger:hilt-android-compiler:2.57.1") implementation("androidx.hilt:hilt-navigation-compose:1.3.0") + + implementation("io.ktor:ktor-client-core:3.4.1") + implementation("io.ktor:ktor-client-okhttp:3.4.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0") } \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt index e6e26fa..f5f1e71 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt @@ -11,9 +11,11 @@ import androidx.compose.material3.DrawerValue import androidx.compose.material3.Scaffold import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState @@ -28,6 +30,7 @@ import cc.n0th1ng.tripmoney.screens.settings.SettingsScreen import cc.n0th1ng.tripmoney.screens.statistics.StatisticsScreen import cc.n0th1ng.tripmoney.screens.trippicker.TripPickerScreen import cc.n0th1ng.tripmoney.theme.TripMoneyTheme +import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -49,6 +52,8 @@ class MainActivity : ComponentActivity() { @RequiresApi(Build.VERSION_CODES.O) @Composable fun NavigationDrawer() { + val settingsViewModel: SettingsViewModel = hiltViewModel() + val currentTripId by settingsViewModel.currentTrip.collectAsState() val navController = rememberNavController() val navBackStack by navController.currentBackStackEntryAsState() val current = navBackStack?.destination?.route @@ -74,7 +79,7 @@ fun NavigationDrawer() { bottomBar = { BottomNavigation(navController) }) { innerPadding -> NavHost( navController = navController, - startDestination = Screens.TRIP_PICKER, + startDestination = if(currentTripId == -1) Screens.TRIP_PICKER else Screens.LIST_EXPENSE, modifier = Modifier.padding(innerPadding) ) { composable(Screens.LIST_EXPENSE) { diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt index 6c2bcec..a1d072a 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt @@ -8,9 +8,11 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.sqlite.db.SupportSQLiteDatabase import cc.n0th1ng.tripmoney.data.dao.CategoryDao +import cc.n0th1ng.tripmoney.data.dao.ExchangeRateDao import cc.n0th1ng.tripmoney.data.dao.ExpenseDao import cc.n0th1ng.tripmoney.data.dao.TripDao import cc.n0th1ng.tripmoney.data.entity.Category +import cc.n0th1ng.tripmoney.data.entity.ExchangeRate import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.utils.Icons @@ -26,11 +28,12 @@ import java.time.LocalDateTime import javax.inject.Inject import javax.inject.Singleton -@Database(entities = [Trip::class, Expense::class, Category::class], version = 1) +@Database(entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class], version = 1) abstract class TripDatabase : RoomDatabase() { abstract fun tripDao(): TripDao abstract fun expenseDao(): ExpenseDao abstract fun categoryDao(): CategoryDao + abstract fun exchangeRateDao(): ExchangeRateDao } @@ -74,6 +77,12 @@ object DatabaseModule { fun provideCategoryDao(database: TripDatabase): CategoryDao { return database.categoryDao() } + + @Provides + @Singleton + fun provideExchangeRateDao(database: TripDatabase): ExchangeRateDao { + return database.exchangeRateDao() + } } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExchangeRateDao.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExchangeRateDao.kt new file mode 100644 index 0000000..45095a0 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExchangeRateDao.kt @@ -0,0 +1,22 @@ +package cc.n0th1ng.tripmoney.data.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert +import cc.n0th1ng.tripmoney.data.entity.Category +import cc.n0th1ng.tripmoney.data.entity.ExchangeRate +import kotlinx.coroutines.flow.Flow + +@Dao +interface ExchangeRateDao { + @Upsert + suspend fun insert(exchangeRate: ExchangeRate) + + @Query("SELECT * FROM exchange_rate WHERE id = :id") + suspend fun getById(id: String): ExchangeRate? + + @Query("DELETE FROM exchange_rate WHERE DATE(date) < :cutoffDate") + suspend fun deleteOldRates(cutoffDate: String) +} diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt index 89098e6..2edaad8 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt @@ -16,6 +16,7 @@ interface TripDao { @Query( """ SELECT * FROM trip + ORDER BY DATE(trip.start_date) DESC """ ) fun tripsPaged(): PagingSource diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/ExchangeRate.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/ExchangeRate.kt new file mode 100644 index 0000000..b2398fa --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/ExchangeRate.kt @@ -0,0 +1,20 @@ +package cc.n0th1ng.tripmoney.data.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity("exchange_rate") +data class ExchangeRate( + @PrimaryKey + val id: String, + val base: String, + val target: String, + val rate: Double, + val date: String +) { + companion object { + fun buildKey(base: String, target: String, date: String): String { + return "${base}_${target}_${date}" + } + } +} diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt new file mode 100644 index 0000000..7966010 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt @@ -0,0 +1,54 @@ +package cc.n0th1ng.tripmoney.data.repository + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.WorkerThread +import cc.n0th1ng.tripmoney.data.dao.CategoryDao +import cc.n0th1ng.tripmoney.data.dao.ExchangeRateDao +import cc.n0th1ng.tripmoney.data.entity.Category +import cc.n0th1ng.tripmoney.data.entity.ExchangeRate +import cc.n0th1ng.tripmoney.service.ExchangeService +import cc.n0th1ng.tripmoney.utils.Currencies +import kotlinx.coroutines.flow.Flow +import java.time.LocalDate +import javax.inject.Inject + +class ExchangeRateRepository @Inject constructor( + private val exchangeRateDao: ExchangeRateDao, + private val exchangeService: ExchangeService +) { + + @WorkerThread + suspend fun save(exchangeRate: ExchangeRate) { + exchangeRateDao.insert(exchangeRate) + } + + @RequiresApi(Build.VERSION_CODES.O) + suspend fun getRate(base: Currencies, target: Currencies, date: LocalDate): Double { + val id = ExchangeRate.buildKey(base.name, target.name, date.toString()) + val cachedRate = exchangeRateDao.getById(id) + return if (cachedRate != null) { + cachedRate.rate + } else { + val rate = exchangeService.getRate(base, target, date) + exchangeRateDao.insert( + ExchangeRate( + id = id, + base = base.name, + target = target.name, + rate = rate, + date = date.toString() + ) + ) + clearOldRates() + rate + } + } + + + @RequiresApi(Build.VERSION_CODES.O) + private suspend fun clearOldRates(daysToKeep: Int = 180) { + val cutoffDate = LocalDate.now().minusDays(daysToKeep.toLong()).toString() + exchangeRateDao.deleteOldRates(cutoffDate) + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt index 9efca80..42f333a 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt @@ -1,8 +1,13 @@ package cc.n0th1ng.tripmoney.screens.addexpense +import android.annotation.SuppressLint import android.os.Build import androidx.annotation.RequiresApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,16 +15,21 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions 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.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SheetState import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -29,11 +39,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -50,26 +66,38 @@ import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.format.DateTimeFormatter + @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(Build.VERSION_CODES.O) @Composable fun AddExpenseBottomSheet( onSave: (Expense) -> Unit, onDismiss: () -> Unit, - categories: List, - expenseDtoToEdit: ExpenseDto? + expenseDtoToEdit: ExpenseDto?, + state: SheetState, +// categories: List = emptyList() ) { + val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel() val currentTripId by settingsViewModel.currentTrip.collectAsState() +// val currentTripId = 1 + val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList()) + if (categories.isEmpty()) { + return + } var amount by remember { mutableStateOf( expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00" ) } + val dummyFocusRequester = remember { FocusRequester() } var showCurrencyDialog by remember { mutableStateOf(false) } var showCategoryDialog by remember { mutableStateOf(false) } var showDateTimePicker by remember { mutableStateOf(false) } @@ -88,21 +116,33 @@ fun AddExpenseBottomSheet( } var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") } var enableSave by remember { mutableStateOf(expenseDtoToEdit != null) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( onDismissRequest = onDismiss, - sheetState = sheetState, + sheetState = state, ) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(0.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(start = 15.dp), + text = stringResource(if (expenseDtoToEdit == null) R.string.add_expense else R.string.edit_expense), + fontWeight = FontWeight.Bold, + fontSize = 35.sp, + textAlign = TextAlign.Start + ) + HorizontalDivider(modifier = Modifier.fillMaxWidth()) Row( + modifier = Modifier.fillMaxWidth(0.9f), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(9.dp) + horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = amount.ifEmpty { "0.00" }, @@ -111,41 +151,44 @@ fun AddExpenseBottomSheet( ) CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency) } - Spacer(Modifier.height(14.dp)) - OutlinedButton(onClick = { showDateTimePicker = true }) { - Text( - text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")), - fontSize = 17.sp - ) - } - Spacer(Modifier.height(14.dp)) - CategoryButton(onClick = { showCategoryDialog = true }, category = category) - Spacer(Modifier.height(14.dp)) Row( - modifier = Modifier.height(50.dp), + modifier = Modifier.fillMaxWidth(0.9f), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - NoteInput(note = note) { newNote -> note = newNote } - SaveButton( - enabled = enableSave, - onClick = { - val expenseToSave = Expense( - amount = amount.toDouble(), - currency = currency, - note = note, - datetime = datetime.toString(), - categoryId = category.id, - tripId = currentTripId - ) - onSave( - if (expenseDtoToEdit == null) expenseToSave - else expenseToSave.copy(id = expenseDtoToEdit.expense.id) - ) - } + OutlinedButton( + onClick = { showDateTimePicker = true }, + modifier = Modifier.weight(1f) + ) { + Text( + text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")), + fontSize = 17.sp + ) + } + CategoryButton( + onClick = { showCategoryDialog = true }, + category = category, + modifier = Modifier.weight(1f) + ) + + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + NoteInput( + note = note, + onTextChange = { newNote -> note = newNote }, + modifier = Modifier.fillMaxWidth(0.9f), + focusRequester = dummyFocusRequester ) } - Spacer(Modifier.height(14.dp)) + + Box( + modifier = Modifier + .size(0.dp) + .focusRequester(dummyFocusRequester) + .focusable() + ) NumberKeyboard( onNumberClick = { number -> val newText = (if (amount == "0.00") "" else amount) + number @@ -155,13 +198,28 @@ fun AddExpenseBottomSheet( } else if (amount == "0.00") { enableSave = false } - + dummyFocusRequester.requestFocus() }, onBackspaceClick = { if (amount == "0.00") return@NumberKeyboard amount = amount.safeSubstring(0, amount.length - 1) enableSave = amount.isDoubleTwoDigitsAboveZero() - }) + }, + onSave = { + val expenseToSave = Expense( + amount = amount.toDouble(), + currency = currency, + note = note, + datetime = datetime.toString(), + categoryId = category.id, + tripId = currentTripId + ) + onSave( + if (expenseDtoToEdit == null) expenseToSave + else expenseToSave.copy(id = expenseDtoToEdit.expense.id) + ) + }, enableSave = enableSave + ) } } @@ -210,29 +268,39 @@ fun String.isDoubleTwoDigitsAboveZero(): Boolean { } @Composable -fun NoteInput(note: String, onTextChange: (String) -> Unit) { +fun NoteInput(note: String, onTextChange: (String) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester) { var text by remember { mutableStateOf(note) } OutlinedTextField( + modifier = modifier, label = { Text(stringResource(R.string.note)) }, value = note, onValueChange = { newText -> text = newText onTextChange(text) - }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + focusRequester.requestFocus() + } + ) ) } @Composable -fun CurrencyButton(onClick: () -> Unit, text: String) { - OutlinedButton(onClick = onClick) { +fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) { + OutlinedButton(onClick = onClick, modifier = modifier) { Text(text) } } @Composable -fun CategoryButton(onClick: () -> Unit, category: Category) { +fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) { OutlinedButton( onClick = onClick, - modifier = Modifier.fillMaxWidth(0.5f) + modifier = modifier ) { Icon( modifier = Modifier.padding(end = 10.dp), @@ -258,20 +326,13 @@ fun SaveButton(enabled: Boolean, onClick: () -> Unit) { } } -@Preview -@Composable -fun Preview() { - TripMoneyTheme(darkTheme = true) { - NumberKeyboard(onNumberClick = {}, onBackspaceClick = {}) - } -} - - @Composable fun NumberKeyboard( modifier: Modifier = Modifier, onNumberClick: (String) -> Unit, - onBackspaceClick: () -> Unit + onBackspaceClick: () -> Unit, + onSave: () -> Unit, + enableSave: Boolean ) { val buttonModifier = Modifier .padding(4.dp) @@ -285,91 +346,109 @@ fun NumberKeyboard( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - OutlinedButton( + TextButton( onClick = { onNumberClick("1") }, modifier = buttonModifier.weight(1f) ) { Text("1", fontSize = 20.sp) } - OutlinedButton( + TextButton( onClick = { onNumberClick("2") }, modifier = buttonModifier.weight(1f) ) { Text("2", fontSize = 20.sp) } - OutlinedButton( + TextButton( onClick = { onNumberClick("3") }, modifier = buttonModifier.weight(1f) ) { Text("3", fontSize = 20.sp) } + TextButton( + onClick = { onNumberClick("") }, + modifier = buttonModifier.weight(1f) + ) { + Text("+", fontSize = 20.sp) + } } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - OutlinedButton( + TextButton( onClick = { onNumberClick("4") }, modifier = buttonModifier.weight(1f) ) { Text("4", fontSize = 20.sp) } - OutlinedButton( + TextButton( onClick = { onNumberClick("5") }, modifier = buttonModifier.weight(1f) ) { Text("5", fontSize = 20.sp) } - OutlinedButton( + TextButton( onClick = { onNumberClick("6") }, modifier = buttonModifier.weight(1f) ) { Text("6", fontSize = 20.sp) } + TextButton( + onClick = { onNumberClick("") }, + modifier = buttonModifier.weight(1f) + ) { + Text("-", fontSize = 20.sp) + } } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - OutlinedButton( + TextButton( onClick = { onNumberClick("7") }, modifier = buttonModifier.weight(1f) ) { Text("7", fontSize = 20.sp) } - OutlinedButton( + TextButton( onClick = { onNumberClick("8") }, modifier = buttonModifier.weight(1f) ) { Text("8", fontSize = 20.sp) } - OutlinedButton( + TextButton( onClick = { onNumberClick("9") }, modifier = buttonModifier.weight(1f) ) { Text("9", fontSize = 20.sp) } + TextButton( + onClick = { onNumberClick("") }, + modifier = buttonModifier.weight(1f) + ) { + Text("*", fontSize = 20.sp) + } } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - OutlinedButton( + TextButton( onClick = { onNumberClick(".") }, modifier = buttonModifier.weight(1f) ) { Text(".", fontSize = 20.sp) } - OutlinedButton( + TextButton( onClick = { onNumberClick("0") }, modifier = buttonModifier.weight(1f) ) { Text("0", fontSize = 20.sp) } - OutlinedButton( + TextButton( onClick = onBackspaceClick, modifier = buttonModifier.weight(1f) ) { @@ -378,6 +457,147 @@ fun NumberKeyboard( contentDescription = stringResource(R.string.backspace) ) } + TextButton( + onClick = onSave, + modifier = buttonModifier.weight(1f), + enabled = enableSave + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(R.string.backspace) + ) + } } } -} \ No newline at end of file +} + + +//@SuppressLint("CoroutineCreationDuringComposition") +//@RequiresApi(Build.VERSION_CODES.O) +//@OptIn(ExperimentalMaterial3Api::class) +//@Preview +//@Composable +//fun PreviewLight() { +// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) +// CoroutineScope(Dispatchers.IO).launch { +// sheetState.show() +// } +// +// TripMoneyTheme { +// AddExpenseBottomSheet( +// {}, {}, null, sheetState, +// categories = listOf( +// Category( +// name = "Hotel", +// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL, +// color = "#B3E5FC" +// ), +// Category( +// name = "Jedzenie", +// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT, +// color = "#C8E6C9" +// ), +// Category( +// name = "Transport", +// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT, +// color = "#FFCDD2" +// ), +// Category( +// name = "Rozrywka", +// icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION, +// color = "#FFF9C4" +// ), +// Category( +// name = "Zakupy", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#E1BEE7" +// ), +// Category( +// name = "Zakupy1", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#D7CCC8" +// ), +// Category( +// name = "Zakupy2", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#BBDEFB" +// ), +// Category( +// name = "Zakupy3", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#D1C4E9" +// ), +// Category( +// name = "Zakupy4", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#DCEDC8" +// ), +// ) +// ) +// } +//} +// +//@SuppressLint("CoroutineCreationDuringComposition") +//@RequiresApi(Build.VERSION_CODES.O) +//@OptIn(ExperimentalMaterial3Api::class) +//@Preview +//@Composable +//fun PreviewDark() { +// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) +// CoroutineScope(Dispatchers.IO).launch { +// sheetState.show() +// } +// +// TripMoneyTheme(darkTheme = true) { +// AddExpenseBottomSheet( +// {}, {}, null, sheetState, +// categories = listOf( +// Category( +// name = "Hotel", +// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL, +// color = "#B3E5FC" +// ), +// Category( +// name = "Jedzenie", +// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT, +// color = "#C8E6C9" +// ), +// Category( +// name = "Transport", +// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT, +// color = "#FFCDD2" +// ), +// Category( +// name = "Rozrywka", +// icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION, +// color = "#FFF9C4" +// ), +// Category( +// name = "Zakupy", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#E1BEE7" +// ), +// Category( +// name = "Zakupy1", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#D7CCC8" +// ), +// Category( +// name = "Zakupy2", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#BBDEFB" +// ), +// Category( +// name = "Zakupy3", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#D1C4E9" +// ), +// Category( +// name = "Zakupy4", +// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, +// color = "#DCEDC8" +// ), +// ) +// ) +// } +//} \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt index 3f3f864..7a5da42 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt @@ -5,6 +5,7 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,6 +34,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -56,10 +58,14 @@ import cc.n0th1ng.tripmoney.R.string import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet +import cc.n0th1ng.tripmoney.service.ExchangeService +import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel +import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import javax.inject.Inject @OptIn(ExperimentalMaterial3Api::class) @@ -71,8 +77,6 @@ fun ListExpenseScreen() { val settingsViewModel: SettingsViewModel = hiltViewModel() val currentTrip by settingsViewModel.currentTrip.collectAsState() - val categories by expenseAndCategoryViewModel.getCategories() - .collectAsState(initial = emptyList()) val expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems() val listState = rememberLazyListState() var showBottomSheet by remember { mutableStateOf(false) } @@ -98,27 +102,13 @@ fun ListExpenseScreen() { val expenseDto = expenses[index] if (expenseDto != null) { val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1) - val showDayDivider = index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime) .toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime) .toLocalDate() Spacer(Modifier.height(5.dp)) if (showDayDivider) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Absolute.Center, - verticalAlignment = Alignment.CenterVertically - ) { - HorizontalDivider(modifier = Modifier.weight(1f)) - Text( - LocalDateTime.parse(expenseDto.expense.datetime).format( - DateTimeFormatter.ofPattern("dd EEEE") - ).toString(), - modifier = Modifier.background(Color.White.copy(alpha = 0f)) - ) - HorizontalDivider(modifier = Modifier.weight(1f)) - } + CustomDivider(expenseDto) } Spacer(Modifier.height(5.dp)) SwipeToDeleteExpenseCard( @@ -143,13 +133,32 @@ fun ListExpenseScreen() { expenseDtoToEdit = null showBottomSheet = false }, - categories = categories, - expenseDtoToEdit = expenseDtoToEdit + expenseDtoToEdit = expenseDtoToEdit, + state = rememberModalBottomSheetState(skipPartiallyExpanded = true) ) } } } +@RequiresApi(Build.VERSION_CODES.O) +@Composable +fun CustomDivider(expenseDto: ExpenseDto) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Absolute.Center, + verticalAlignment = Alignment.CenterVertically + ) { + HorizontalDivider(modifier = Modifier.weight(1f)) + Text( + LocalDateTime.parse(expenseDto.expense.datetime).format( + DateTimeFormatter.ofPattern("dd EEEE") + ).toString(), + modifier = Modifier.background(Color.White.copy(alpha = 0f)) + ) + HorizontalDivider(modifier = Modifier.weight(1f)) + } +} + @RequiresApi(Build.VERSION_CODES.O) @Composable fun SwipeToDeleteExpenseCard( @@ -257,7 +266,10 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) { modifier = Modifier .fillMaxWidth(0.9f) .height(70.dp) - .clickable { onClick(expenseDto) }, + .combinedClickable( + enabled = true, + onClick = { onClick(expenseDto) }, + onLongClick = { onClick(expenseDto) }), elevation = CardDefaults.cardElevation(defaultElevation = 7.dp) ) { Row( @@ -312,8 +324,16 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) { fontWeight = FontWeight.Bold ) if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) { + val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() + val amount by + expenseAndCategoryViewModel.convertAmount( + amount = expenseDto.expense.amount, + base = Currencies.valueOf(expenseDto.expense.currency), + target = Currencies.valueOf(expenseDto.trip.currency), + date = LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate() + ).collectAsState(initial = 0.0) Text( - text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.amount), + text = "≈ %.2f ${expenseDto.trip.currency}".format(amount), fontSize = 12.sp ) } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt index d0103f1..9ccef0f 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt @@ -2,20 +2,27 @@ package cc.n0th1ng.tripmoney.screens.trippicker import android.os.Build import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Shapes +import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -23,31 +30,32 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cc.n0th1ng.tripmoney.R -import cc.n0th1ng.tripmoney.R.string import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton -import cc.n0th1ng.tripmoney.screens.addexpense.isDoubleTwoDigitsAboveZero import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker -import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker import cc.n0th1ng.tripmoney.utils.Currencies -import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime -import java.time.ZoneId import java.time.format.DateTimeFormatter @RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit: Trip?) { +fun AddTripBottomSheet( + onDismiss: () -> Unit, + onSave: (Trip) -> Unit, + tripToEdit: Trip?, + sheetState: SheetState +) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var name by remember { mutableStateOf(tripToEdit?.name ?: "") } var startDate by remember { mutableStateOf( @@ -65,31 +73,61 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit ) { Column( modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.Start + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(start = 15.dp), + text = stringResource(if(tripToEdit == null) R.string.add_trip else R.string.edit_trip), + fontWeight = FontWeight.Bold, + fontSize = 35.sp, + textAlign = TextAlign.Start + ) + HorizontalDivider(modifier = Modifier.fillMaxWidth()) NameInput(name = name, onTextChange = { newText -> name = newText enableSave = !name.isEmpty() }) - CurrencyButton(onClick = {showCurrencyDialog = true}, currency) - OutlinedButton(onClick = { showDatePicker = true }) { - Text( - text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")), - fontSize = 17.sp + Row( + modifier = Modifier.fillMaxWidth(0.9f), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + CurrencyButton( + modifier = Modifier + .weight(1f) + .fillMaxWidth(1f), + onClick = { showCurrencyDialog = true }, text = currency ) + OutlinedButton( + modifier = Modifier + .fillMaxWidth(1f) + .weight(1f), + onClick = { showDatePicker = true }) { + Text( + text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")), + fontSize = 17.sp + ) + } + } - OutlinedButton( + + Button( + modifier = Modifier.fillMaxWidth(0.9f), enabled = enableSave, onClick = { - onSave(Trip(name = name, startDate = startDate.toString(), currency = currency)) - }) { + val trip = Trip(name = name, startDate = startDate.toString(), currency = currency) + + onSave(if(tripToEdit == null) trip else trip.copy(id = tripToEdit.id)) + }) { Icon( imageVector = Icons.Filled.Check, contentDescription = stringResource(R.string.save) ) } + Spacer(Modifier.height(5.dp)) } } @@ -105,7 +143,7 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit } if (showDatePicker) { - DatePicker(startDate, onDismiss = {showDatePicker = false}, onConfirm = { newDate -> + DatePicker(startDate, onDismiss = { showDatePicker = false }, onConfirm = { newDate -> startDate = newDate showDatePicker = false }) @@ -116,10 +154,10 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit fun NameInput(name: String, onTextChange: (String) -> Unit) { var text by remember { mutableStateOf(name) } OutlinedTextField( + modifier = Modifier.fillMaxWidth(0.9f), label = { Text(stringResource(R.string.name)) }, value = name, onValueChange = { newText -> text = newText onTextChange(text) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) -} - +} \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt index 4f3450e..27f207e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt @@ -5,6 +5,7 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,6 +20,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton @@ -28,6 +30,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -39,6 +42,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -56,6 +61,7 @@ import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.TripViewModel +@OptIn(ExperimentalMaterial3Api::class) @RequiresApi(Build.VERSION_CODES.O) @Composable @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @@ -67,6 +73,7 @@ fun TripPickerScreen( var showBottomSheet by remember { mutableStateOf(false) } val trips: LazyPagingItems = tripViewModel.getTrips().collectAsLazyPagingItems() val currentTripId by settingsViewModel.currentTrip.collectAsState() + var tripToEdit by remember { mutableStateOf(null) } Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = { FloatingActionButton( onClick = { showBottomSheet = true }) { @@ -83,12 +90,17 @@ fun TripPickerScreen( Spacer(Modifier.height(10.dp)) val trip = trips[i] if (trip != null) { - SwipeToDeleteTripCard(trip, onDelete = { + SwipeToDeleteTripCard( + trip, onDelete = { tripViewModel.delete(trip) }, onClick = { settingsViewModel.setCurrentTrip(trip.id) navController.navigate(Screens.LIST_EXPENSE) - }, isSelected = currentTripId == trip.id) + }, isSelected = currentTripId == trip.id, + onLongClick = { trip -> + tripToEdit = trip + showBottomSheet = true + }) } Spacer(Modifier.height(10.dp)) } @@ -98,12 +110,15 @@ fun TripPickerScreen( AddTripBottomSheet( onDismiss = { showBottomSheet = false + tripToEdit = null }, onSave = { trip -> tripViewModel.save(trip) showBottomSheet = false + tripToEdit = null }, - null + tripToEdit = tripToEdit, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ) } } @@ -112,7 +127,8 @@ fun TripPickerScreen( @RequiresApi(Build.VERSION_CODES.O) @Composable fun SwipeToDeleteTripCard( - trip: Trip, onDelete: (Trip) -> Unit, onClick: (Trip) -> Unit, isSelected: Boolean + trip: Trip, onDelete: (Trip) -> Unit, onClick: (Trip) -> Unit, isSelected: Boolean, + onLongClick: (Trip) -> Unit ) { var dismissed by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) } @@ -151,18 +167,27 @@ fun SwipeToDeleteTripCard( Icon(Icons.Default.Delete, contentDescription = stringResource(string.delete)) } }) { - TripCard(trip, isSelected, onClick = onClick) + TripCard(trip, isSelected, onClick = onClick, onLongClick = onLongClick) } } } @Composable -fun TripCard(trip: Trip, isSelected: Boolean, onClick: (Trip) -> Unit) { +fun TripCard( + trip: Trip, + isSelected: Boolean, + onClick: (Trip) -> Unit, + onLongClick: (Trip) -> Unit +) { + val haptics = LocalHapticFeedback.current ElevatedCard( modifier = Modifier .height(100.dp) - .clickable(true, onClick = { onClick(trip) }), + .combinedClickable(enabled = true, onLongClick = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick(trip) + }, onClick = { onClick(trip) }), elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp) ) { Row( diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/service/ExchangeService.kt b/app/src/main/java/cc/n0th1ng/tripmoney/service/ExchangeService.kt new file mode 100644 index 0000000..88d1f9d --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/service/ExchangeService.kt @@ -0,0 +1,35 @@ +package cc.n0th1ng.tripmoney.service + +import cc.n0th1ng.tripmoney.utils.Currencies +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.double +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.json.JSONObject +import java.time.LocalDate +import javax.inject.Inject + +class ExchangeService @Inject() constructor() { + private val API_URL: String = "https://api.frankfurter.dev" + private val client = HttpClient() + + suspend fun getRate(base: Currencies, target: Currencies, date: LocalDate): Double { + return try { + val response = client.get("$API_URL/v1/$date") { + url { + parameters.append("base", base.name) + parameters.append("symbols", target.name) + } + } + val json = Json + json.parseToJsonElement(response.bodyAsText()).jsonObject["rates"]?.jsonObject[target.name]?.jsonPrimitive?.double + ?: throw Exception("can not find rates") + } catch (e: Exception) { + throw IllegalStateException("Error fetching exchange rate: ${e.message}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/theme/Theme.kt b/app/src/main/java/cc/n0th1ng/tripmoney/theme/Theme.kt index 2a232d6..35317bd 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/theme/Theme.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/theme/Theme.kt @@ -2,13 +2,16 @@ package cc.n0th1ng.tripmoney.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp private val DarkColorScheme = darkColorScheme( primary = Purple80, @@ -44,7 +47,6 @@ fun TripMoneyTheme( val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme else -> LightColorScheme } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt index dd0f0e8..de1ea28 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt @@ -9,16 +9,23 @@ import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import cc.n0th1ng.tripmoney.data.repository.CategoryRepository +import cc.n0th1ng.tripmoney.data.repository.ExchangeRateRepository import cc.n0th1ng.tripmoney.data.repository.ExpenseRepository +import cc.n0th1ng.tripmoney.service.ExchangeService +import cc.n0th1ng.tripmoney.utils.Currencies import dagger.hilt.android.lifecycle.HiltViewModel +import io.ktor.client.request.get import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch +import java.time.LocalDate import javax.inject.Inject @HiltViewModel class ExpenseAndCategoryViewModel @Inject constructor( private val expenseRepo: ExpenseRepository, - private val categoryRepo: CategoryRepository + private val categoryRepo: CategoryRepository, + private val exchangeRateRepository: ExchangeRateRepository ) : ViewModel() { fun getExpenses(tripId: Int): Flow> = @@ -43,4 +50,10 @@ class ExpenseAndCategoryViewModel @Inject constructor( categoryRepo.save(category) } } + + fun convertAmount(amount: Double, base: Currencies, target: Currencies, date: LocalDate): Flow { + return flow { + emit(amount * exchangeRateRepository.getRate(base, target, date)) + } + } } \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2a0a4c3..130d879 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -16,6 +16,8 @@ Zgodnie z systemem Motyw Ciemny motyw - Dodaj Wycieczkę + Dodaj wycieczkę Nazwa + Edytuj wycieczkę + Edytuj wydatek \ 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 c5216a9..82d1395 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,4 +18,6 @@ System settings Add Trip Name + Edit trip + Edit expense \ No newline at end of file