diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt index f5f1e71..6ae412f 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt @@ -30,7 +30,9 @@ 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.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel +import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -43,6 +45,8 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { TripMoneyTheme { + val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() + expenseAndCategoryViewModel.clearOldRates() NavigationDrawer() } } @@ -53,7 +57,9 @@ class MainActivity : ComponentActivity() { @Composable fun NavigationDrawer() { val settingsViewModel: SettingsViewModel = hiltViewModel() + val tripViewModel: TripViewModel = hiltViewModel() val currentTripId by settingsViewModel.currentTrip.collectAsState() + val currentTrip = tripViewModel.getTrip(currentTripId) val navController = rememberNavController() val navBackStack by navController.currentBackStackEntryAsState() val current = navBackStack?.destination?.route @@ -65,7 +71,9 @@ fun NavigationDrawer() { topBar = { if (current == Screens.SETTINGS) TopBarSettings( navController - ) else TopBar(onClick = { + ) else TopBar( + title = currentTrip?.name ?: "", + onClick = { scope.launch { if (drawerState.isClosed) { drawerState.open() 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 a1d072a..ccc5de4 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt @@ -16,6 +16,7 @@ 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 +import cc.n0th1ng.tripmoney.utils.colors import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -96,19 +97,12 @@ private class DatabasePrepopulator( tripDao.insert(Trip(name = "Włochy", startDate = "2025-01-01", currency = "PLN")) tripDao.insert(Trip(name = "Szwajcaria", startDate = "2025-03-01", currency = "EUR")) tripDao.insert(Trip(name = "Portugalia", startDate = "2026-03-01", currency = "USD")) - categoryDao.insert(Category(name = "Hotel", icon = Icons.HOTEL, color = "#B3E5FC")) - categoryDao.insert(Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = "#C8E6C9")) - categoryDao.insert(Category(name = "Transport", icon = Icons.FLIGHT, color = "#FFCDD2")) - categoryDao.insert(Category(name = "Rozrywka", icon = Icons.ATTRACTION, color = "#FFF9C4")) - categoryDao.insert(Category(name = "Zakupy", icon = Icons.GROCERIES, color = "#E1BEE7")) - categoryDao.insert(Category(name = "Zakupy1", icon = Icons.GROCERIES, color = "#D7CCC8")) - categoryDao.insert(Category(name = "Zakupy2", icon = Icons.GROCERIES, color = "#BBDEFB")) - categoryDao.insert(Category(name = "Zakupy3", icon = Icons.GROCERIES, color = "#D1C4E9")) - categoryDao.insert(Category(name = "Zakupy4", icon = Icons.GROCERIES, color = "#DCEDC8")) - categoryDao.insert(Category(name = "Zakupy5", icon = Icons.GROCERIES, color = "#F0F4C3")) - categoryDao.insert(Category(name = "Zakupy6", icon = Icons.GROCERIES, color = "#FFE0B2")) - categoryDao.insert(Category(name = "Zakupy7", icon = Icons.GROCERIES, color = "#D7CCC8")) - categoryDao.insert(Category(name = "Zakupy8", icon = Icons.GROCERIES, color = "#CFD8DC")) + categoryDao.insert(Category(name = "Hotel", icon = Icons.HOTEL, color = colors.random())) + categoryDao.insert(Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random())) + categoryDao.insert(Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random())) + categoryDao.insert(Category(name = "Rozrywka", icon = Icons.ATTRACTION, color = colors.random())) + categoryDao.insert(Category(name = "Zakupy", icon = Icons.GROCERIES,color = colors.random())) + val now = LocalDateTime.now() expenseDao.insert( diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt index 9cb1465..f8d9a7a 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt @@ -3,27 +3,58 @@ package cc.n0th1ng.tripmoney.data.dao import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Delete -import androidx.room.Insert import androidx.room.Query import androidx.room.Transaction import androidx.room.Upsert +import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategoryRaw import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.ExpenseDto +import kotlinx.coroutines.flow.Flow @Dao interface ExpenseDao { @Upsert suspend fun insert(expense: Expense) - @Transaction @Query( """ SELECT * FROM expense WHERE trip_id = :tripId ORDER BY DATETIME(expense.datetime) DESC """ ) - fun expenseDto(tripId: Int): PagingSource + fun expenseDtoPaged(tripId: Int): PagingSource + + @Transaction + @Query( + """ + SELECT * FROM expense WHERE trip_id = :tripId + ORDER BY DATETIME(expense.datetime) DESC + """ + ) + fun expenseDto(tripId: Int): Flow> @Delete suspend fun delete(expense: Expense) + + @Query( + """ + SELECT + c.id as categoryId, + c.name as categoryName, + c.icon as icon, + c.color as color, + SUM(e.amount) as amount, + e.currency as currency + FROM + expense e + JOIN + category c ON e.category_id = c.id + WHERE + e.trip_id = :tripId + GROUP BY + c.id, c.name, c.icon, c.color, e.currency + """ + ) + fun summaryPerCategoryRaw(tripId: Int): Flow> + } 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 2edaad8..e07726b 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 @@ -23,4 +23,9 @@ interface TripDao { @Delete suspend fun delete(trip: Trip) + + @Query( + "SELECT * FROM trip where trip.id = :tripId" + ) + fun trip(tripId: Int): Trip? } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dto/SummaryPerCategory.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dto/SummaryPerCategory.kt new file mode 100644 index 0000000..4a4c399 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dto/SummaryPerCategory.kt @@ -0,0 +1,22 @@ +package cc.n0th1ng.tripmoney.data.dto + +import cc.n0th1ng.tripmoney.data.entity.Category +import cc.n0th1ng.tripmoney.utils.Currencies +import cc.n0th1ng.tripmoney.utils.Icons + +data class SummaryPerCategory( + val category: Category, + val amount: Double, + val percent: Float, + val currency: Currencies +) + +data class SummaryPerCategoryRaw( + val categoryId: Int, + val categoryName: String, + val icon: Icons, + val color: String, + val amount: Double, + val currency: String +) + 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 index 7966010..98c73e4 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt @@ -40,14 +40,13 @@ class ExchangeRateRepository @Inject constructor( date = date.toString() ) ) - clearOldRates() rate } } @RequiresApi(Build.VERSION_CODES.O) - private suspend fun clearOldRates(daysToKeep: Int = 180) { + suspend fun clearOldRates(daysToKeep: Int = 180) { val cutoffDate = LocalDate.now().minusDays(daysToKeep.toLong()).toString() exchangeRateDao.deleteOldRates(cutoffDate) } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt index ddb482b..74e48bf 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt @@ -5,6 +5,7 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import cc.n0th1ng.tripmoney.data.dao.ExpenseDao +import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategoryRaw import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import kotlinx.coroutines.flow.Flow @@ -22,10 +23,18 @@ class ExpenseRepository @Inject constructor(private val expenseDao: ExpenseDao) expenseDao.delete(expense) } - fun getExpenses(tripId: Int): Flow> { + fun getExpensesPaged(tripId: Int): Flow> { return Pager( config = PagingConfig(pageSize = 50, enablePlaceholders = false), - pagingSourceFactory = { expenseDao.expenseDto(tripId) } + pagingSourceFactory = { expenseDao.expenseDtoPaged(tripId) } ).flow } + + fun getExpenses(tripId: Int): Flow> { + return expenseDao.expenseDto(tripId) + } + + fun getSummaryPerCategory(tripId: Int): Flow> { + return expenseDao.summaryPerCategoryRaw(tripId) + } } \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/PreferencesRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/PreferencesRepository.kt index d1d1347..b4c0b2b 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/PreferencesRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/PreferencesRepository.kt @@ -7,9 +7,13 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.APP_THEME import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.CURRENT_TRIP +import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.DEFAULT_CURRENCY +import cc.n0th1ng.tripmoney.utils.Currencies import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.DEFAULT_CONCURRENCY import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.util.Currency import javax.inject.Inject @@ -18,6 +22,7 @@ val Context.preferencesDataStore by preferencesDataStore(name = "app_preferences object PreferenceKeys { val APP_THEME = intPreferencesKey("app_theme") val CURRENT_TRIP = intPreferencesKey("current_trip") + val DEFAULT_CURRENCY = stringPreferencesKey("default_currency") } @@ -34,6 +39,16 @@ class PreferencesRepository @Inject constructor(@ApplicationContext private val prefs[CURRENT_TRIP] ?: -1 } + val defaultCurrencyFlow: Flow = + context.preferencesDataStore.data.map { prefs -> + Currencies.valueOf(prefs[DEFAULT_CURRENCY] ?: Currencies.default().name) + } + + suspend fun saveDefaultCurrency(currency: Currencies) { + context.preferencesDataStore.edit { prefs -> + prefs[DEFAULT_CURRENCY] = currency.name + } + } suspend fun saveCurrentTrip(tripId: Int) { context.preferencesDataStore.edit { prefs -> prefs[CURRENT_TRIP] = tripId diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt index d0109ea..f2456bc 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt @@ -23,6 +23,10 @@ class TripRepository @Inject constructor(private val tripDao: TripDao) { ).flow } + fun getTrip(tripId: Int): Trip? { + return tripDao.trip(tripId) + } + @WorkerThread suspend fun delete(trip: Trip) { tripDao.delete(trip) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt index dad784f..5c2a69a 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt @@ -14,9 +14,9 @@ import androidx.navigation.NavHostController @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TopBar(onClick: () -> Unit) { +fun TopBar(onClick: () -> Unit, title: String = "") { TopAppBar( - title = {}, + title = { Text(title) }, navigationIcon = { IconButton(onClick = { onClick() }) { Icon(Icons.Default.Menu, contentDescription = "Menu") diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/AddCetegoryDialog.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/AddCetegoryDialog.kt index 9159139..34a285c 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/AddCetegoryDialog.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/AddCetegoryDialog.kt @@ -1,6 +1,5 @@ package cc.n0th1ng.tripmoney.screens -import android.graphics.drawable.Icon import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll @@ -28,19 +27,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt import cc.n0th1ng.tripmoney.data.entity.Category -import cc.n0th1ng.tripmoney.theme.TripMoneyTheme -import cc.n0th1ng.tripmoney.utils.Colors import cc.n0th1ng.tripmoney.utils.Icons +import cc.n0th1ng.tripmoney.utils.colors @Composable fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) { var name by remember { mutableStateOf("") } var icon by remember { mutableStateOf(Icons.entries[0]) } - var color by remember { mutableStateOf(Colors.entries[0].hexString) } + var color by remember { mutableStateOf(colors[0]) } AlertDialog( onDismissRequest = onDismiss, title = { Text("Add new category") }, text = { AlertDialogFill( @@ -48,7 +45,7 @@ fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) { name = newText }, onIconChange = { newIcon -> icon = newIcon }, - onColorChange = {newColor -> color = newColor} + onColorChange = { newColor -> color = newColor } ) }, confirmButton = { Button( @@ -72,10 +69,14 @@ fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) { } @Composable -fun AlertDialogFill(onTextChange: (String) -> Unit, onIconChange: (Icons) -> Unit, onColorChange: (String) -> Unit) { +fun AlertDialogFill( + onTextChange: (String) -> Unit, + onIconChange: (Icons) -> Unit, + onColorChange: (String) -> Unit +) { var text by remember { mutableStateOf("") } var iconId by remember { mutableIntStateOf(Icons.entries[0].resource) } - var colorHex by remember { mutableStateOf(Colors.entries[0].hexString) } + var colorHex by remember { mutableStateOf(colors[0]) } Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row( verticalAlignment = Alignment.CenterVertically, @@ -118,16 +119,16 @@ fun AlertDialogFill(onTextChange: (String) -> Unit, onIconChange: (Icons) -> Uni rememberScrollState() ) ) { - Colors.entries.forEach { color -> + colors.forEach { color -> Box( modifier = Modifier .clickable(onClick = { - colorHex = color.hexString + colorHex = color onColorChange(colorHex) }) .size(30.dp) .aspectRatio(1f) - .background(Color(color.hexString.toColorInt())) + .background(Color(color.toColorInt())) ) {} } } 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 42f333a..7e0e350 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 @@ -66,6 +66,7 @@ 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 cc.n0th1ng.tripmoney.viewmodel.TripViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -81,13 +82,13 @@ fun AddExpenseBottomSheet( onSave: (Expense) -> Unit, onDismiss: () -> Unit, expenseDtoToEdit: ExpenseDto?, - state: SheetState, -// categories: List = emptyList() + state: SheetState ) { + val tripViewModel: TripViewModel = hiltViewModel() val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel() val currentTripId by settingsViewModel.currentTrip.collectAsState() -// val currentTripId = 1 + val currentTrip = tripViewModel.getTrip(currentTripId) val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList()) if (categories.isEmpty()) { return @@ -103,7 +104,7 @@ fun AddExpenseBottomSheet( var showDateTimePicker by remember { mutableStateOf(false) } var currency by remember { mutableStateOf( - expenseDtoToEdit?.expense?.currency ?: Currencies.PLN.name + expenseDtoToEdit?.expense?.currency ?: currentTrip?.currency ?: Currencies.default().name ) } var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/DateTimePicker.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/DateTimePicker.kt index f4f57c9..22434eb 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/DateTimePicker.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/DateTimePicker.kt @@ -104,7 +104,9 @@ fun DateTimePicker( if (showDatePicker) { DatePicker(onDismiss = { showDatePicker = false }, onConfirm = { newDate -> - date = newDate + date = newDate + showDatePicker = false + showTimePicker = true }) } 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 7a5da42..a875eb1 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 @@ -37,8 +37,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -53,34 +55,58 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.graphics.toColorInt import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems import cc.n0th1ng.tripmoney.R.string +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.entity.Trip import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet -import cc.n0th1ng.tripmoney.service.ExchangeService +import cc.n0th1ng.tripmoney.theme.TripMoneyTheme +import cc.n0th1ng.tripmoney.utils.AllPreviews import cc.n0th1ng.tripmoney.utils.Currencies +import cc.n0th1ng.tripmoney.utils.colors import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel +import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel.ExpenseDtoWithConvertedAmount import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import java.time.LocalDate import java.time.LocalDateTime +import java.time.ZoneOffset import java.time.format.DateTimeFormatter -import javax.inject.Inject +import kotlin.random.Random +@RequiresApi(Build.VERSION_CODES.O) +@Composable +fun ListExpenseScreen() { + val settingsViewModel: SettingsViewModel = hiltViewModel() + val currentTrip by settingsViewModel.currentTrip.collectAsState() + val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() + val expensesWithConvertedFlow = expenseAndCategoryViewModel + .getExpensesWithConvertedAmountsPaged(currentTrip) + + ListExpenseScreen( + expensesWithConvertedFlow = expensesWithConvertedFlow, + onSaveExpense = { expenseAndCategoryViewModel.save(it) }, + onDeleteExpense = { expenseAndCategoryViewModel.delete(it) }) +} + @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @RequiresApi(Build.VERSION_CODES.O) @Composable -fun ListExpenseScreen() { - val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() - val settingsViewModel: SettingsViewModel = hiltViewModel() - - val currentTrip by settingsViewModel.currentTrip.collectAsState() - val expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems() +fun ListExpenseScreen( + expensesWithConvertedFlow: Flow>, + onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit +) { + val expensesWithConverted = expensesWithConvertedFlow.collectAsLazyPagingItems() val listState = rememberLazyListState() var showBottomSheet by remember { mutableStateOf(false) } var expenseDtoToEdit: ExpenseDto? = null + val sumMap = remember { mutableStateMapOf() } Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = { ExtendedFloatingActionButton( @@ -90,30 +116,50 @@ fun ListExpenseScreen() { ) }) { + LaunchedEffect(expensesWithConverted.itemSnapshotList.items) { + val items = expensesWithConverted.itemSnapshotList.items + val newSums = items + .groupBy { LocalDateTime.parse(it.expenseDto.expense.datetime).toLocalDate() } + .mapValues { (_, expensesForDay) -> + expensesForDay.sumOf { it.convertedAmount } + } + sumMap.clear() + sumMap.putAll(newSums) + } + LazyColumn( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, state = listState ) { items( - count = expenses.itemCount, - key = { index -> expenses[index]?.expense?.id ?: index } + count = expensesWithConverted.itemCount, + key = { index -> expensesWithConverted[index]?.expenseDto?.expense?.id ?: index } ) { index -> - val expenseDto = expenses[index] - if (expenseDto != null) { - val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1) + val expenseDtoWithConverted = expensesWithConverted[index] + val expenseDto = expenseDtoWithConverted?.expenseDto + if (expenseDtoWithConverted != null && expenseDto != null) { + val previousExpense = + expensesWithConverted.itemSnapshotList.items.getOrNull(index - 1)?.expenseDto val showDayDivider = index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime) .toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime) .toLocalDate() - Spacer(Modifier.height(5.dp)) + Spacer(Modifier + .height(5.dp) + .background(MaterialTheme.colorScheme.onBackground)) if (showDayDivider) { - CustomDivider(expenseDto) + CustomDivider( + expenseDto, + sumMap.getOrDefault( + LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate(), 0.00 + ) + ) } Spacer(Modifier.height(5.dp)) SwipeToDeleteExpenseCard( - expenseDto = expenseDto, - onDelete = { expense -> expenseAndCategoryViewModel.delete(expense) }, + expenseDtoWithConverted = expenseDtoWithConverted, + onDelete = { expense -> onDeleteExpense(expense) }, onClick = { expenseDto -> expenseDtoToEdit = expenseDto showBottomSheet = true @@ -125,7 +171,7 @@ fun ListExpenseScreen() { if (showBottomSheet) { AddExpenseBottomSheet( onSave = { expense -> - expenseAndCategoryViewModel.save(expense) + onSaveExpense(expense) showBottomSheet = false expenseDtoToEdit = null }, @@ -140,9 +186,10 @@ fun ListExpenseScreen() { } } + @RequiresApi(Build.VERSION_CODES.O) @Composable -fun CustomDivider(expenseDto: ExpenseDto) { +fun CustomDivider(expenseDto: ExpenseDto, sum: Double) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Absolute.Center, @@ -153,16 +200,32 @@ fun CustomDivider(expenseDto: ExpenseDto) { LocalDateTime.parse(expenseDto.expense.datetime).format( DateTimeFormatter.ofPattern("dd EEEE") ).toString(), - modifier = Modifier.background(Color.White.copy(alpha = 0f)) + modifier = Modifier.background(Color.White.copy(alpha = 0f)), + style = MaterialTheme.typography.titleMedium ) - HorizontalDivider(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalArrangement = Arrangement.Absolute.Center, + verticalAlignment = Alignment.CenterVertically + ) { + HorizontalDivider(modifier = Modifier.weight(2f)) + Text( + "%.2f %s".format(sum, expenseDto.trip.currency), + modifier = Modifier.background(Color.White.copy(alpha = 0f)), + style = MaterialTheme.typography.bodyMedium + ) + HorizontalDivider(modifier = Modifier.weight(1f)) + } + } } @RequiresApi(Build.VERSION_CODES.O) @Composable fun SwipeToDeleteExpenseCard( - expenseDto: ExpenseDto, + expenseDtoWithConverted: ExpenseDtoWithConvertedAmount, onDelete: (Expense) -> Unit, onClick: (ExpenseDto) -> Unit ) { @@ -186,7 +249,7 @@ fun SwipeToDeleteExpenseCard( onConfirm = { showDialog = false dismissed = true - onDelete(expenseDto.expense) + onDelete(expenseDtoWithConverted.expenseDto.expense) }, onCancel = { showDialog = false } ) @@ -209,7 +272,7 @@ fun SwipeToDeleteExpenseCard( } } ) { - ExpenseCard(expenseDto, onClick = onClick) + ExpenseCard(expenseDtoWithConverted, onClick = onClick) } } } @@ -226,15 +289,15 @@ fun DeleteConfirmationDialog( Column( Modifier .background( - MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.secondaryContainer, shape = MaterialTheme.shapes.medium ) .padding(24.dp) ) { Text( stringResource(string.delete_confirmation), - fontWeight = FontWeight.Bold, - fontSize = 20.sp + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSecondaryContainer ) Row( horizontalArrangement = Arrangement.End, @@ -244,6 +307,8 @@ fun DeleteConfirmationDialog( ) { Text( text = stringResource(string.cancel), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier .padding(end = 24.dp) .clickable { onCancel() } @@ -251,7 +316,7 @@ fun DeleteConfirmationDialog( Text( text = stringResource(string.delete), color = MaterialTheme.colorScheme.error, - fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.clickable { onConfirm() } ) } @@ -261,8 +326,14 @@ fun DeleteConfirmationDialog( @RequiresApi(Build.VERSION_CODES.O) @Composable -fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) { +fun ExpenseCard( + expenseDtoWithConverted: ExpenseDtoWithConvertedAmount, + onClick: (ExpenseDto) -> Unit +) { + val expenseDto = expenseDtoWithConverted.expenseDto ElevatedCard( + colors = CardDefaults.elevatedCardColors() + .copy(containerColor = MaterialTheme.colorScheme.secondaryContainer), modifier = Modifier .fillMaxWidth(0.9f) .height(70.dp) @@ -299,14 +370,14 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) { { Text( text = expenseDto.category.name, - fontWeight = FontWeight.Bold, - lineHeight = 5.sp + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer ) Text( modifier = Modifier.padding(0.dp), text = expenseDto.expense.note, - fontSize = 11.sp, - lineHeight = 5.sp + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer ) } @@ -314,31 +385,132 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) { text = LocalDateTime.parse(expenseDto.expense.datetime).format( DateTimeFormatter.ofPattern("dd MMM HH:mm") ), - fontSize = 12.sp, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer ) } } Column { Text( text = "- %.2f ${expenseDto.expense.currency}".format(expenseDto.expense.amount), - fontWeight = FontWeight.Bold + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + + ) 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(amount), - fontSize = 12.sp + text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDtoWithConverted.convertedAmount), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer ) } } } } -} \ No newline at end of file +} + +@RequiresApi(Build.VERSION_CODES.O) +@AllPreviews +@Composable +fun PreviewListExpenseScreen() { + TripMoneyTheme() { + val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList()) + ListExpenseScreen( + expensesWithConvertedFlow = MutableStateFlow(pagingData), + onSaveExpense = {}, + onDeleteExpense = {} + ) + + } +} + +@AllPreviews +@Composable +fun PreviewDeleteConfirmationDialog() { + TripMoneyTheme() { + DeleteConfirmationDialog( + onConfirm = {}, + onCancel = {}) + } +} + + +@RequiresApi(Build.VERSION_CODES.O) +private fun sampleExpenseDtoWithConvertedAmountList(): List { + val sampleCategories = listOf( + Category( + name = "Hotel", + icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL, + color = colors.random() + ), + Category( + name = "Jedzenie", + icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT, + color = colors.random() + ), + Category( + name = "Transport", + icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT, + color = colors.random() + ), + Category( + name = "Rozrywka", + icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION, + color = colors.random() + ), + Category( + name = "Zakupy", + icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES, + color = colors.random() + ), + ) + + val trip = Trip( + id = 1, + name = "Vacation", + currency = "USD", + startDate = "2026-01-01" + ) + + val startLong = LocalDateTime.now().minusDays(10).toEpochMilli() + val endLong = LocalDateTime.now().toEpochMilli() + + val result: MutableList = mutableListOf() + for (i in 0..15) { + val category = sampleCategories.random() + val datetime = if (i > 4) { + LocalDateTime.ofEpochSecond( + Random.nextLong(startLong, endLong), + 0, + ZoneOffset.UTC + ).toString() + } else LocalDateTime.now().toString() + + val expense = Expense( + id = i, + categoryId = category.id, + tripId = 1, + amount = Random.nextDouble(0.1, 300.0), + currency = Currencies.entries.random().name, + note = if (i % 3 == 0) "Some note" else "", + datetime = datetime + ) + val expenseDto = ExpenseDto( + expense = expense, + category = category, + trip = trip + ) + result.add( + ExpenseDtoWithConvertedAmount( + expenseDto, + convertedAmount = if (Random.nextBoolean()) Random.nextDouble( + 0.1, + 300.0 + ) else expense.amount + ) + ) + } + return result +} diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/settings/SettingsScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/settings/SettingsScreen.kt index 90a1faa..32e6f8f 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/settings/SettingsScreen.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import cc.n0th1ng.tripmoney.R.* import cc.n0th1ng.tripmoney.data.repository.AppTheme +import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog +import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel @RequiresApi(Build.VERSION_CODES.S) @@ -40,8 +42,9 @@ import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel fun SettingsScreen() { val settingsViewModel: SettingsViewModel = hiltViewModel() val currentTheme by settingsViewModel.theme.collectAsState() - var showDialog by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() + val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState() + var showThemeDialog by remember { mutableStateOf(false) } + var showCurrencyDialog by remember { mutableStateOf(false) } Column( modifier = Modifier .fillMaxWidth() @@ -49,7 +52,7 @@ fun SettingsScreen() { verticalArrangement = Arrangement.spacedBy(10.dp) ) { Card { - SettingsListItem(onClick = { showDialog = true }, stringResource(string.theme)) { + SettingsListItem(onClick = { showThemeDialog = true }, stringResource(string.theme)) { Text( if (isSystemInDarkTheme()) stringResource(string.dark_theme) else stringResource( string.light_theme @@ -58,16 +61,33 @@ fun SettingsScreen() { } } - if (showDialog) { + Card { + SettingsListItem( + onClick = { showCurrencyDialog = true }, + stringResource(string.default_currency) + ) { + Text(currentDefaultCurrency.name) + } + } + + if (showThemeDialog) { ThemeSelectionDialog( - onDismiss = { showDialog = false }, + onDismiss = { showThemeDialog = false }, onThemeSelected = { theme -> settingsViewModel.setTheme(theme) - showDialog = false + showThemeDialog = false }, selected = currentTheme ) } + + if (showCurrencyDialog) { + CurrencySelectionDialog(onDismiss = {showCurrencyDialog = false}, onCurrencySelected = { + currencyString -> + settingsViewModel.setDefaultCurrency(Currencies.valueOf(currencyString)) + showCurrencyDialog = false + }, currentDefaultCurrency.name) + } } } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt index 6e1dd5c..6fb4ebe 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt @@ -1,9 +1,150 @@ package cc.n0th1ng.tripmoney.screens.statistics +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background +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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +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.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.graphics.toColorInt +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory +import cc.n0th1ng.tripmoney.data.entity.Category +import cc.n0th1ng.tripmoney.theme.TripMoneyTheme +import cc.n0th1ng.tripmoney.utils.Currencies +import cc.n0th1ng.tripmoney.utils.Icons +import cc.n0th1ng.tripmoney.utils.colors +import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel +import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel +@RequiresApi(Build.VERSION_CODES.O) @Composable fun StatisticsScreen() { - Text("TODO") -} \ No newline at end of file + val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() + val settingsViewModel: SettingsViewModel = hiltViewModel() + val currentTrip by settingsViewModel.currentTrip.collectAsState() + val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTrip) + .collectAsState(emptyList()) + + Column(modifier = Modifier.padding(10.dp)) { + SummaryPerCategoryCard(summaryPerCategoryList) + } +} + + +@Composable +fun SummaryPerCategoryCard(summaryPerCategoryList: List) { + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(15.dp), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { +// Text(text = "Summary", fontWeight = FontWeight.Bold, fontSize = 25.sp) + summaryPerCategoryList.forEach { + CategoryCard( + summaryPerCategory = it, modifier = Modifier + .fillMaxWidth() + ) + } + } + } +} + +@Composable +fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) { + Column(modifier = modifier) { + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { + Icon( + painter = painterResource(summaryPerCategory.category.icon.resource), + contentDescription = null, + modifier = Modifier.size(MaterialTheme.typography.bodyLarge.fontSize.value.dp), + tint = Color(summaryPerCategory.category.color.toColorInt()) + ) + Text("%s".format(summaryPerCategory.category.name, (summaryPerCategory.percent * 100).toInt()), + style = MaterialTheme.typography.bodyLarge, color = Color(summaryPerCategory.category.color.toColorInt())) + } + + Text("%.2f ${summaryPerCategory.currency}".format(summaryPerCategory.amount), + style = MaterialTheme.typography.bodyMedium) + } + Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically){ + Box( + modifier = Modifier + .height(40.dp) + .fillMaxWidth(0.12f + (0.90f - 0.12f) * summaryPerCategory.percent) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.primary) + ) { + Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize().padding(11.dp)) { + Text("%d%%".format((summaryPerCategory.percent * 100).toInt()), + style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimary) + + } + } +// Text("%d%%".format((summaryPerCategory.percent * 100).toInt()), +// style = MaterialTheme.typography.labelSmall) + } + } +} + +@Preview +@Composable +fun previewLight() { + TripMoneyTheme { + SummaryPerCategoryCard(summaryPerCategoryList) + } +} + +@Preview +@Composable +fun previewDark() { + TripMoneyTheme(darkTheme = true) { + SummaryPerCategoryCard(summaryPerCategoryList) + } +} + +val categories = listOf( + Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()), + Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()), + Category(name = "Rozrywka", icon = Icons.ATTRACTION, color = colors.random()), + Category(name = "Zakupy", icon = Icons.GROCERIES, color = colors.random()), + Category(name = "Zakupy1", icon = Icons.GROCERIES, color = colors.random()), + Category(name = "Zakupy2", icon = Icons.GROCERIES, color = colors.random()), + Category(name = "Zakupy3", icon = Icons.GROCERIES, color = colors.random()) +) + +val summaryPerCategoryList = listOf( + SummaryPerCategory(categories[0], 50.0, 1f, Currencies.PLN), + SummaryPerCategory(categories[1], 120.0, 0.3f, Currencies.PLN), + SummaryPerCategory(categories[2], 80.0, 0.2f, Currencies.PLN), + SummaryPerCategory(categories[3], 50.0, 0.1f, Currencies.PLN), + SummaryPerCategory(categories[4], 120.0, 0.3f, Currencies.PLN), + SummaryPerCategory(categories[5], 50.0, 0.0001f, Currencies.PLN), +) \ No newline at end of file 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 9ccef0f..204e6a1 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 @@ -24,6 +24,7 @@ import androidx.compose.material3.Shapes import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -37,12 +38,15 @@ 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 androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import cc.n0th1ng.tripmoney.R import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker import cc.n0th1ng.tripmoney.utils.Currencies +import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel +import io.ktor.http.hostIsIp import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -62,9 +66,11 @@ fun AddTripBottomSheet( LocalDate.parse(tripToEdit?.startDate ?: LocalDate.now().toString()) ) } + val settingsViewModel: SettingsViewModel = hiltViewModel() + val defaultCurrency by settingsViewModel.defaultCurrency.collectAsState() var showCurrencyDialog by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) } - var currency by remember { mutableStateOf(tripToEdit?.currency ?: Currencies.default().name) } + var currency by remember { mutableStateOf(tripToEdit?.currency ?: defaultCurrency.name) } var enableSave by remember { mutableStateOf(tripToEdit != null) } ModalBottomSheet( 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 27f207e..d9fe28a 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 @@ -182,6 +182,13 @@ fun TripCard( ) { val haptics = LocalHapticFeedback.current ElevatedCard( + colors = CardDefaults.elevatedCardColors( + containerColor = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.secondary + } + ), modifier = Modifier .height(100.dp) .combinedClickable(enabled = true, onLongClick = { diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/theme/Color.kt b/app/src/main/java/cc/n0th1ng/tripmoney/theme/Color.kt index d4d21b1..1a2fac5 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/theme/Color.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/theme/Color.kt @@ -2,10 +2,218 @@ package cc.n0th1ng.tripmoney.theme import androidx.compose.ui.graphics.Color -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) +val primaryLight = Color(0xFF48672E) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFFC9EEA7) +val onPrimaryContainerLight = Color(0xFF324E19) +val secondaryLight = Color(0xFF56624A) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFDAE7C9) +val onSecondaryContainerLight = Color(0xFF3F4A34) +val tertiaryLight = Color(0xFF386664) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFBBECE9) +val onTertiaryContainerLight = Color(0xFF1E4E4D) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF93000A) +val backgroundLight = Color(0xFFF9FAEF) +val onBackgroundLight = Color(0xFF1A1D16) +val surfaceLight = Color(0xFFF9FAEF) +val onSurfaceLight = Color(0xFF1A1D16) +val surfaceVariantLight = Color(0xFFE0E4D6) +val onSurfaceVariantLight = Color(0xFF44483E) +val outlineLight = Color(0xFF74796D) +val outlineVariantLight = Color(0xFFC4C8BA) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF2E312A) +val inverseOnSurfaceLight = Color(0xFFF0F2E7) +val inversePrimaryLight = Color(0xFFAED18D) +val surfaceDimLight = Color(0xFFD9DBD0) +val surfaceBrightLight = Color(0xFFF9FAEF) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFF3F5EA) +val surfaceContainerLight = Color(0xFFEDEFE4) +val surfaceContainerHighLight = Color(0xFFE7E9DE) +val surfaceContainerHighestLight = Color(0xFFE2E3D9) -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val primaryLightMediumContrast = Color(0xFF213D08) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF57763B) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF2F3925) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF657158) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF073D3C) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF477573) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF740006) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFCF2C27) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFF9FAEF) +val onBackgroundLightMediumContrast = Color(0xFF1A1D16) +val surfaceLightMediumContrast = Color(0xFFF9FAEF) +val onSurfaceLightMediumContrast = Color(0xFF0F120C) +val surfaceVariantLightMediumContrast = Color(0xFFE0E4D6) +val onSurfaceVariantLightMediumContrast = Color(0xFF33382E) +val outlineLightMediumContrast = Color(0xFF4F5449) +val outlineVariantLightMediumContrast = Color(0xFF6A6F63) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF2E312A) +val inverseOnSurfaceLightMediumContrast = Color(0xFFF0F2E7) +val inversePrimaryLightMediumContrast = Color(0xFFAED18D) +val surfaceDimLightMediumContrast = Color(0xFFC5C7BD) +val surfaceBrightLightMediumContrast = Color(0xFFF9FAEF) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFF3F5EA) +val surfaceContainerLightMediumContrast = Color(0xFFE7E9DE) +val surfaceContainerHighLightMediumContrast = Color(0xFFDCDED3) +val surfaceContainerHighestLightMediumContrast = Color(0xFFD1D3C8) + +val primaryLightHighContrast = Color(0xFF183301) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF34511B) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF252F1B) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF414D36) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF003231) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF21504F) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF600004) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF98000A) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFF9FAEF) +val onBackgroundLightHighContrast = Color(0xFF1A1D16) +val surfaceLightHighContrast = Color(0xFFF9FAEF) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFE0E4D6) +val onSurfaceVariantLightHighContrast = Color(0xFF000000) +val outlineLightHighContrast = Color(0xFF292E24) +val outlineVariantLightHighContrast = Color(0xFF464B40) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF2E312A) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFAED18D) +val surfaceDimLightHighContrast = Color(0xFFB8BAB0) +val surfaceBrightLightHighContrast = Color(0xFFF9FAEF) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFF0F2E7) +val surfaceContainerLightHighContrast = Color(0xFFE2E3D9) +val surfaceContainerHighLightHighContrast = Color(0xFFD3D5CB) +val surfaceContainerHighestLightHighContrast = Color(0xFFC5C7BD) + +val primaryDark = Color(0xFFAED18D) +val onPrimaryDark = Color(0xFF1C3704) +val primaryContainerDark = Color(0xFF324E19) +val onPrimaryContainerDark = Color(0xFFC9EEA7) +val secondaryDark = Color(0xFFBECBAE) +val onSecondaryDark = Color(0xFF29341F) +val secondaryContainerDark = Color(0xFF3F4A34) +val onSecondaryContainerDark = Color(0xFFDAE7C9) +val tertiaryDark = Color(0xFFA0CFCD) +val onTertiaryDark = Color(0xFF003736) +val tertiaryContainerDark = Color(0xFF1E4E4D) +val onTertiaryContainerDark = Color(0xFFBBECE9) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF11140E) +val onBackgroundDark = Color(0xFFE2E3D9) +val surfaceDark = Color(0xFF11140E) +val onSurfaceDark = Color(0xFFE2E3D9) +val surfaceVariantDark = Color(0xFF44483E) +val onSurfaceVariantDark = Color(0xFFC4C8BA) +val outlineDark = Color(0xFF8E9286) +val outlineVariantDark = Color(0xFF44483E) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFE2E3D9) +val inverseOnSurfaceDark = Color(0xFF2E312A) +val inversePrimaryDark = Color(0xFF48672E) +val surfaceDimDark = Color(0xFF11140E) +val surfaceBrightDark = Color(0xFF373A33) +val surfaceContainerLowestDark = Color(0xFF0C0F09) +val surfaceContainerLowDark = Color(0xFF1A1D16) +val surfaceContainerDark = Color(0xFF1E211A) +val surfaceContainerHighDark = Color(0xFF282B24) +val surfaceContainerHighestDark = Color(0xFF33362E) + +val primaryDarkMediumContrast = Color(0xFFC3E8A1) +val onPrimaryDarkMediumContrast = Color(0xFF132B00) +val primaryContainerDarkMediumContrast = Color(0xFF799A5C) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFD4E1C3) +val onSecondaryDarkMediumContrast = Color(0xFF1E2915) +val secondaryContainerDarkMediumContrast = Color(0xFF89957A) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFFB5E5E3) +val onTertiaryDarkMediumContrast = Color(0xFF002B2A) +val tertiaryContainerDarkMediumContrast = Color(0xFF6B9997) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFD2CC) +val onErrorDarkMediumContrast = Color(0xFF540003) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF11140E) +val onBackgroundDarkMediumContrast = Color(0xFFE2E3D9) +val surfaceDarkMediumContrast = Color(0xFF11140E) +val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkMediumContrast = Color(0xFF44483E) +val onSurfaceVariantDarkMediumContrast = Color(0xFFDADED0) +val outlineDarkMediumContrast = Color(0xFFAFB4A6) +val outlineVariantDarkMediumContrast = Color(0xFF8E9285) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFE2E3D9) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF282B24) +val inversePrimaryDarkMediumContrast = Color(0xFF33501A) +val surfaceDimDarkMediumContrast = Color(0xFF11140E) +val surfaceBrightDarkMediumContrast = Color(0xFF43453E) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF060804) +val surfaceContainerLowDarkMediumContrast = Color(0xFF1C1F18) +val surfaceContainerDarkMediumContrast = Color(0xFF262922) +val surfaceContainerHighDarkMediumContrast = Color(0xFF31342C) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C3F37) + +val primaryDarkHighContrast = Color(0xFFD7FCB3) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFAACD89) +val onPrimaryContainerDarkHighContrast = Color(0xFF040E00) +val secondaryDarkHighContrast = Color(0xFFE8F5D6) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFBAC7AA) +val onSecondaryContainerDarkHighContrast = Color(0xFF050E01) +val tertiaryDarkHighContrast = Color(0xFFC9F9F7) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFF9CCBC9) +val onTertiaryContainerDarkHighContrast = Color(0xFF000E0D) +val errorDarkHighContrast = Color(0xFFFFECE9) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFAEA4) +val onErrorContainerDarkHighContrast = Color(0xFF220001) +val backgroundDarkHighContrast = Color(0xFF11140E) +val onBackgroundDarkHighContrast = Color(0xFFE2E3D9) +val surfaceDarkHighContrast = Color(0xFF11140E) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF44483E) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) +val outlineDarkHighContrast = Color(0xFFEEF2E3) +val outlineVariantDarkHighContrast = Color(0xFFC0C4B6) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFE2E3D9) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF33501A) +val surfaceDimDarkHighContrast = Color(0xFF11140E) +val surfaceBrightDarkHighContrast = Color(0xFF4E5149) +val surfaceContainerLowestDarkHighContrast = Color(0xFF000000) +val surfaceContainerLowDarkHighContrast = Color(0xFF1E211A) +val surfaceContainerDarkHighContrast = Color(0xFF2E312A) +val surfaceContainerHighDarkHighContrast = Color(0xFF393C35) +val surfaceContainerHighestDarkHighContrast = Color(0xFF454840) \ 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 35317bd..7fa2c9e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/theme/Theme.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/theme/Theme.kt @@ -13,33 +13,87 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 +private val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, ) -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ +private val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, ) @Composable fun TripMoneyTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, + dynamicColor: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = when { @@ -47,8 +101,8 @@ fun TripMoneyTheme( val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme - else -> LightColorScheme + darkTheme -> darkScheme + else -> lightScheme } MaterialTheme( diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/utils/AllPreviews.kt b/app/src/main/java/cc/n0th1ng/tripmoney/utils/AllPreviews.kt new file mode 100644 index 0000000..7449ef9 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/utils/AllPreviews.kt @@ -0,0 +1,9 @@ +package cc.n0th1ng.tripmoney.utils + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.content.res.Configuration.UI_MODE_TYPE_NORMAL +import androidx.compose.ui.tooling.preview.Preview + +@Preview(name = "Light") +@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL) +annotation class AllPreviews \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/utils/Colors.kt b/app/src/main/java/cc/n0th1ng/tripmoney/utils/Colors.kt index b557c93..ab756af 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/utils/Colors.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/utils/Colors.kt @@ -1,25 +1,31 @@ package cc.n0th1ng.tripmoney.utils -enum class Colors(val hexString: String) { - RED("#D53E0F"), - PINK("#FF69B4"), - ORANGE("#FF8C00"), - YELLOW("#FFD700"), - LIME("#32CD32"), - GREEN("#228B22"), - MINT("#98FB98"), - TEAL("#008080"), - CYAN("#00CED1"), - SKY_BLUE("#1E90FF"), - BLUE("#0000FF"), - LAVENDER("#8A2BE2"), - LILAC("#C8A2C8"), - PURPLE("#800080"), - MAUVE("#D8BFD8"), - MAGENTA("#FF00FF"), - VIOLET("#9400D3"), - INDIGO("#4B0082"), - PERIWINKLE("#8A2BE2"), - GRAY("#696969"); -} +val colors: List = listOf( + "#af1b3f", + "#083D77", + "#5998c5", + "#f7934c", + "#ec0b43", + "#87A330", + "#6F8AB7", + "#F26CA7", + "#5E4AE3", + "#2A7F62", + "#0B7189" +) +// GREEN("#228B22"), +// MINT("#98FB98"), +// TEAL("#008080"), +// CYAN("#00CED1"), +// SKY_BLUE("#1E90FF"), +// BLUE("#0000FF"), +// LAVENDER("#8A2BE2"), +// LILAC("#C8A2C8"), +// PURPLE("#800080"), +// MAUVE("#D8BFD8"), +// MAGENTA("#FF00FF"), +// VIOLET("#9400D3"), +// INDIGO("#4B0082"), +// PERIWINKLE("#8A2BE2"); +//} 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 de1ea28..6153d1f 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt @@ -1,35 +1,39 @@ package cc.n0th1ng.tripmoney.viewmodel +import android.os.Build +import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn +import androidx.paging.map +import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory 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.data.repository.TripRepository 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.flow.map import kotlinx.coroutines.launch -import java.time.LocalDate +import kotlinx.coroutines.runBlocking +import java.time.LocalDateTime import javax.inject.Inject @HiltViewModel -class ExpenseAndCategoryViewModel @Inject constructor( +open class ExpenseAndCategoryViewModel @Inject constructor( private val expenseRepo: ExpenseRepository, private val categoryRepo: CategoryRepository, - private val exchangeRateRepository: ExchangeRateRepository + private val exchangeRateRepository: ExchangeRateRepository, + private val tripRepo: TripRepository ) : ViewModel() { fun getExpenses(tripId: Int): Flow> = - expenseRepo.getExpenses(tripId).cachedIn(viewModelScope) + expenseRepo.getExpensesPaged(tripId).cachedIn(viewModelScope) fun save(expense: Expense) { viewModelScope.launch { @@ -51,9 +55,81 @@ class ExpenseAndCategoryViewModel @Inject constructor( } } - fun convertAmount(amount: Double, base: Currencies, target: Currencies, date: LocalDate): Flow { - return flow { - emit(amount * exchangeRateRepository.getRate(base, target, date)) + @RequiresApi(Build.VERSION_CODES.O) + fun getSummaryPerCategory(tripId: Int): Flow> { + val tripCurrency = tripRepo.getTrip(tripId)?.currency ?: Currencies.default().name + return getExpensesWithConvertedAmounts(tripId) + .map { list -> + // Compute summary + val sumOfAll = list.sumOf { it.convertedAmount } + list.groupBy { it.expenseDto.category } + .map { (category, expenses) -> + val total = expenses.sumOf { it.convertedAmount } + SummaryPerCategory( + category = category, + amount = total, + percent = (total / sumOfAll).toFloat(), + currency = Currencies.valueOf(tripCurrency) + ) + }.sortedBy { it.percent }.reversed() + } + } + + + @RequiresApi(Build.VERSION_CODES.O) + fun getExpensesWithConvertedAmounts(tripId: Int): Flow> { + return expenseRepo.getExpenses(tripId) + .map { list -> + list.map { expenseDto -> + val convertedAmount = + if (expenseDto.expense.currency != expenseDto.trip.currency) { + runBlocking { + expenseDto.toExpenseDtoWithConvertedAmount() + } + } else { + expenseDto.expense.amount + } + ExpenseDtoWithConvertedAmount(expenseDto, convertedAmount) + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + fun getExpensesWithConvertedAmountsPaged(tripId: Int): Flow> { + return expenseRepo.getExpensesPaged(tripId) + .map { pagingData -> + pagingData.map { expenseDto -> + val convertedAmount = + if (expenseDto.expense.currency != expenseDto.trip.currency) { + runBlocking { + expenseDto.toExpenseDtoWithConvertedAmount() + } + } else { + expenseDto.expense.amount + } + ExpenseDtoWithConvertedAmount(expenseDto, convertedAmount) + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + fun clearOldRates() { + viewModelScope.launch { + exchangeRateRepository.clearOldRates() } } + + @RequiresApi(Build.VERSION_CODES.O) + suspend fun ExpenseDto.toExpenseDtoWithConvertedAmount(): Double { + return exchangeRateRepository.getRate( + Currencies.valueOf(this.expense.currency), + Currencies.valueOf(this.trip.currency), + LocalDateTime.parse(this.expense.datetime).toLocalDate() + ) * this.expense.amount + } + + data class ExpenseDtoWithConvertedAmount( + val expenseDto: ExpenseDto, + val convertedAmount: Double + ) } \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SettingsViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SettingsViewModel.kt index 650e055..e7da36e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SettingsViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import cc.n0th1ng.tripmoney.data.repository.AppTheme import cc.n0th1ng.tripmoney.data.repository.PreferencesRepository +import cc.n0th1ng.tripmoney.utils.Currencies import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.SharingStarted @@ -35,6 +36,17 @@ class SettingsViewModel @Inject constructor( -1 ) + val defaultCurrency = repo.defaultCurrencyFlow.stateIn( + viewModelScope, SharingStarted.WhileSubscribed(5000), + Currencies.default() + ) + + fun setDefaultCurrency(currency: Currencies) { + viewModelScope.launch { + repo.saveDefaultCurrency(currency) + } + } + fun setCurrentTrip(tripId: Int) { viewModelScope.launch { repo.saveCurrentTrip(tripId) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/TripViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/TripViewModel.kt index 3686396..fdf9ce6 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/TripViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/TripViewModel.kt @@ -18,6 +18,8 @@ class TripViewModel @Inject constructor(private val repository: TripRepository) fun getTrips(): Flow> = repository.getTrips().cachedIn(viewModelScope) + fun getTrip(tripId: Int): Trip? = repository.getTrip(tripId) + fun delete(trip: Trip) { viewModelScope.launch { repository.delete(trip) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 130d879..508d84a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -20,4 +20,5 @@ Nazwa Edytuj wycieczkę Edytuj wydatek + Domyślna waluta \ 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 82d1395..f487f89 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,4 +20,5 @@ Name Edit trip Edit expense + Default currency \ No newline at end of file