From 767d54e8f65da68b4313079d2dbabe0b50cdabc7 Mon Sep 17 00:00:00 2001 From: Rafal Wisniewski <2krafal.wisniewski@gmail.com> Date: Thu, 2 Apr 2026 10:46:41 +0200 Subject: [PATCH] init --- app/build.gradle.kts | 2 +- .../java/cc/n0th1ng/tripmoney/MainActivity.kt | 24 ++--- .../cc/n0th1ng/tripmoney/data/TripDatabase.kt | 3 + .../n0th1ng/tripmoney/data/dao/ExpenseDao.kt | 8 ++ .../cc/n0th1ng/tripmoney/data/entity/Trip.kt | 12 ++- .../data/repository/ExpenseRepository.kt | 4 + .../data/repository/PreferencesRepository.kt | 16 ++++ .../addexpense/AddExpenseBottomSheet.kt | 31 ++++--- .../screens/listexpense/DateTimePicker.kt | 48 +++++++++- .../screens/listexpense/ListExpenseScreen.kt | 39 +++++--- .../screens/settings/SettingsScreen.kt | 43 ++++++--- .../screens/statistics/StatisticsScreen.kt | 91 ++++++++++++------- .../screens/trippicker/AddTripBottomSheet.kt | 85 ++++++++++++++--- .../screens/trippicker/TripPickerScreen.kt | 43 +++++++-- .../cc/n0th1ng/tripmoney/utils/DateUtils.kt | 11 +++ .../viewmodel/ExpenseAndCategoryViewModel.kt | 12 +-- .../tripmoney/viewmodel/SettingsViewModel.kt | 30 +++++- app/src/main/res/values/colors.xml | 9 +- app/src/main/res/values/strings.xml | 3 + gradle/libs.versions.toml | 2 + 20 files changed, 381 insertions(+), 135 deletions(-) create mode 100644 app/src/main/java/cc/n0th1ng/tripmoney/utils/DateUtils.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bf3193c..d582721 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -97,7 +97,7 @@ dependencies { implementation("androidx.paging:paging-compose:3.4.2") implementation("androidx.datastore:datastore-preferences:1.2.1") - implementation("com.composables:icons-material-symbols-outlined-android:2.2.1") + implementation(libs.icons.material.symbols.outlined.android) implementation("com.google.dagger:hilt-android:2.57.1") ksp("com.google.dagger:hilt-android-compiler:2.57.1") diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt index db70c55..a344b1b 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt @@ -16,15 +16,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import androidx.paging.compose.collectAsLazyPagingItems import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.navigation.BottomNavigation @@ -42,7 +45,9 @@ 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.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -53,12 +58,6 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { TripMoneyTheme { - val settingsViewModel: SettingsViewModel = hiltViewModel() - val currentTripId by settingsViewModel.currentTrip.collectAsState() - val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() - expenseAndCategoryViewModel.clearOldRates() - expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId) - .collectAsLazyPagingItems() NavigationDrawer() } } @@ -78,12 +77,12 @@ fun NavigationDrawer() { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() var filter by remember { mutableStateOf("") } + val autoOpenPref by settingsViewModel.autoOpenStartupPref.collectAsState() + var hasHandledStartupOpen by rememberSaveable { mutableStateOf(false) } + val shouldTriggerAutoOpen = autoOpenPref == true && !hasHandledStartupOpen CustomNavigationDrawer(navController, drawerState) { Scaffold( - modifier = Modifier.semantics { - testTagsAsResourceId = true - }, topBar = { if (current == Screens.SETTINGS) TopBarSettings( navController @@ -99,7 +98,7 @@ fun NavigationDrawer() { } }, isSearchable = current == Screens.LIST_EXPENSE, - onFilterChange = { newFilter -> filter = newFilter}) + onFilterChange = { newFilter -> filter = newFilter }) }, bottomBar = { BottomNavigation(navController) }) { innerPadding -> @@ -109,7 +108,10 @@ fun NavigationDrawer() { modifier = Modifier.padding(innerPadding) ) { composable(Screens.LIST_EXPENSE) { - ListExpenseScreen(filter) + ListExpenseScreen( + filter, + initialAutoOpen = shouldTriggerAutoOpen, + onAutoOpenConsumed = { hasHandledStartupOpen = true }) } composable(Screens.TRIP_PICKER) { TripPickerScreen(navController) 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 597409c..bf57ea5 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt @@ -120,6 +120,7 @@ private class DatabasePrepopulator( Trip( name = "Włochy", startDate = LocalDate.parse("2026-03-01"), + endDate = LocalDate.parse("2026-03-15"), currency = "PLN" ) ) @@ -127,6 +128,7 @@ private class DatabasePrepopulator( Trip( name = "Szwajcaria", startDate = LocalDate.parse("2025-03-01"), + endDate = LocalDate.parse("2025-03-15"), currency = "EUR" ) ) @@ -134,6 +136,7 @@ private class DatabasePrepopulator( Trip( name = "Portugalia", startDate = LocalDate.parse("2025-03-01"), + endDate = LocalDate.parse("2025-03-15"), currency = "USD" ) ) 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 7368c89..7485d66 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 @@ -49,6 +49,14 @@ interface ExpenseDao { ) fun expenseDto(tripId: Int, filter: String): Flow> + @Query(""" + SELECT trip.budget - IFNULL(SUM(expense.amount * expense.rate), 0) + FROM trip + LEFT JOIN expense ON expense.trip_id = trip.id + WHERE trip.id = :tripId + """) + fun budgetLeft(tripId: Int): Double + @Delete suspend fun delete(expense: Expense) } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt index 2efc7c4..e2e7225 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt @@ -15,11 +15,17 @@ data class Trip( @PrimaryKey(autoGenerate = true) val id: Int = 0, @ColumnInfo("name") val name: String, @ColumnInfo("start_date") val startDate: LocalDate, + @ColumnInfo("end_date") val endDate: LocalDate, @ColumnInfo("currency") val currency: String, - @ColumnInfo("budget") val budget: Double -){ + @ColumnInfo("budget") val budget: Double = 0.0 +) { companion object { @RequiresApi(Build.VERSION_CODES.O) - val DUMMY = Trip(-1, "", LocalDate.now(), Currencies.default().name, budget = 0.0) + val DUMMY = Trip( + -1, + "", + LocalDate.now(), + endDate = LocalDate.now(), Currencies.default().name, budget = 0.0, + ) } } \ No newline at end of file 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 7e259c9..bf77bb1 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 @@ -25,6 +25,10 @@ class ExpenseRepository @Inject constructor( private val exchangeRateRepository: ExchangeRateRepository ) { + fun getBudgetLeft(tripId: Int): Double { + return expenseDao.budgetLeft(tripId) + } + @WorkerThread suspend fun save(expense: Expense) { expenseDao.insert(expense) 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 b4c0b2b..b37a5c8 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 @@ -1,10 +1,12 @@ package cc.n0th1ng.tripmoney.data.repository import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.ADD_EXPENSE_SWITCH 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 @@ -23,6 +25,7 @@ object PreferenceKeys { val APP_THEME = intPreferencesKey("app_theme") val CURRENT_TRIP = intPreferencesKey("current_trip") val DEFAULT_CURRENCY = stringPreferencesKey("default_currency") + val ADD_EXPENSE_SWITCH = booleanPreferencesKey("add_expense_switch") } @@ -34,6 +37,13 @@ class PreferencesRepository @Inject constructor(@ApplicationContext private val AppTheme.fromValue(value) } + val currentAddExpenseSwitchFlow: Flow = + context.preferencesDataStore.data.map { prefs -> + val value = prefs[ADD_EXPENSE_SWITCH] + ?: false + value + } + val currentTripFlow: Flow = context.preferencesDataStore.data.map { prefs -> prefs[CURRENT_TRIP] ?: -1 @@ -61,6 +71,12 @@ class PreferencesRepository @Inject constructor(@ApplicationContext private val } } + suspend fun saveAddExpenseSwitch(value: Boolean) { + context.preferencesDataStore.edit { prefs -> + prefs[ADD_EXPENSE_SWITCH] = value + } + } + } enum class AppTheme(val value: Int) { 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 6a048b8..0092cfd 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 @@ -2,13 +2,10 @@ package cc.n0th1ng.tripmoney.screens.addexpense import android.annotation.SuppressLint import android.os.Build -import android.util.Log import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.focusable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -34,7 +31,6 @@ import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableDoubleStateOf @@ -47,7 +43,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -76,8 +71,6 @@ import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import com.composables.icons.materialsymbols.outlined.R.drawable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.time.LocalDate import java.time.LocalDateTime @@ -104,7 +97,7 @@ fun AddExpenseBottomSheet( onDismiss = onDismiss, expenseDtoToEdit = expenseDtoToEdit, state = state, - currentTrip = currentTrip!!, + currentTrip = currentTrip ?: Trip.DUMMY, categories = categories ) } @@ -128,10 +121,14 @@ fun AddExpenseBottomSheet( var amount by remember { mutableStateOf( - expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00" + "%.2f".format(expenseDtoToEdit?.expense?.amount ?: 0.00) + ) + } + var equationResult by remember { + mutableDoubleStateOf( + expenseDtoToEdit?.expense?.amount ?: 0.00 ) } - var equationResult by remember { mutableDoubleStateOf(0.0) } val dummyFocusRequester = remember { FocusRequester() } var showCurrencyDialog by remember { mutableStateOf(false) } var showCategoryDialog by remember { mutableStateOf(false) } @@ -193,7 +190,7 @@ fun AddExpenseBottomSheet( fontWeight = FontWeight.Bold ) Text( - text = if (amount.contains(Regex("[+\\/*-]\\d+"))) "%.2f".format( + text = if (amount.contains(Regex("[+/*-]\\d+"))) "%.2f".format( equationResult ) else "", fontSize = 14.sp, @@ -240,7 +237,7 @@ fun AddExpenseBottomSheet( NumberKeyboard( modifier = Modifier.fillMaxWidth(), onOperatorClick = { operator -> - if (amount.isDoubleTwoDigitsOrEquation() && amount.contains(Regex("[+\\/*-]\\d+"))) { + if (amount.isDoubleTwoDigitsOrEquation() && amount.contains(Regex("[+/*-]\\d+"))) { amount = evaluate(amount).toString() } val newText = amount + operator @@ -369,7 +366,7 @@ private inline fun String.indexOfFirstIndexed(predicate: (index: Int, Char) -> B } private fun String.isDoubleTwoDigitsOrEquation(): Boolean { - return this != "0.00" && this.matches(Regex("^(-?(0\\.?|0\\.\\d{1,2}|[1-9]\\d*(\\.\\d{0,2})?))([+\\/*-](0\\.?|0\\.\\d{1,2}|[1-9]\\d*(\\.\\d{0,2})?)?)?$")) + return this != "0.00" && this.matches(Regex("^(-?(0\\.?|0\\.\\d{1,2}|[1-9]\\d*(\\.\\d{0,2})?))([+/*-](0\\.?|0\\.\\d{1,2}|[1-9]\\d*(\\.\\d{0,2})?)?)?$")) } @Composable @@ -518,7 +515,6 @@ fun KeyboardButton( text: String? = null, icon: Painter? = null, onClick: () -> Unit, - enabled: Boolean = true, onLongClick: () -> Unit = {}, containerColor: Color = MaterialTheme.colorScheme.primary, contentColor: Color = MaterialTheme.colorScheme.onPrimary @@ -574,6 +570,7 @@ fun PreviewAddExpenseDisabled() { 1, "Trip", LocalDate.parse("2020-01-01"), + LocalDate.parse("2020-01-15"), Currencies.entries.random().name ), categories = categoriesToPreview @@ -607,13 +604,17 @@ fun PreviewAddExpenseEnabled() { tripId = 1 ), category = categoriesToPreview[0], - Trip(1, "Włochy", LocalDate.parse("2025-01-02"), "PLN") + Trip( + 1, "Włochy", LocalDate.parse("2025-01-02"), + LocalDate.parse("2025-01-15"), "PLN" + ) ), state = sheetState, currentTrip = Trip( 1, "Trip", LocalDate.parse("2020-01-01"), + LocalDate.parse("2020-01-11"), Currencies.entries.random().name ), categories = categoriesToPreview 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 22434eb..8e7e10b 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 @@ -5,14 +5,14 @@ import androidx.annotation.RequiresApi import androidx.compose.material3.AlertDialog import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog -import androidx.compose.material3.DatePickerState +import androidx.compose.material3.DateRangePicker import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TimePicker import androidx.compose.material3.TimePickerState import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberDateRangePickerState import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -20,17 +20,55 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.sp import cc.n0th1ng.tripmoney.R.* -import java.sql.Time import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId -import java.time.format.DateTimeFormatter import java.util.Calendar +@RequiresApi(Build.VERSION_CODES.O) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateRangePicker( + startDate: LocalDate, + endDate: LocalDate, + onDismiss: () -> Unit, + onConfirm: (LocalDate, LocalDate) -> Unit +) { + val datePickerState = + rememberDateRangePickerState(initialSelectedStartDateMillis = startDate.toEpochMilli(), + initialSelectedEndDateMillis = endDate.toEpochMilli()) + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { + val selectedStartDateMillis = datePickerState.selectedStartDateMillis + val selectedEndDateMillis = datePickerState.selectedEndDateMillis + if (selectedStartDateMillis != null && selectedEndDateMillis != null) { + val selectedStartDate = Instant.ofEpochMilli(selectedStartDateMillis) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + val selectedEndDate = + Instant.ofEpochMilli(selectedEndDateMillis).atZone(ZoneId.systemDefault()) + .toLocalDate() + onConfirm(selectedStartDate, selectedEndDate) + } + }) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(string.cancel)) } + } + ) { + DateRangePicker(state = datePickerState, showModeToggle = false, + title = {}) + } +} + @RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class) @Composable 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 0546af4..75b7431 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 @@ -46,14 +46,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey @@ -73,7 +71,6 @@ import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filter import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneOffset @@ -83,7 +80,9 @@ import kotlin.random.Random @RequiresApi(Build.VERSION_CODES.O) @Composable -fun ListExpenseScreen(filter: String) { +fun ListExpenseScreen(filter: String, + initialAutoOpen: Boolean, + onAutoOpenConsumed: () -> Unit ) { val settingsViewModel: SettingsViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel() val currentTripId by settingsViewModel.currentTrip.collectAsState() @@ -97,7 +96,9 @@ fun ListExpenseScreen(filter: String) { expensesFlow = expensesFlow, onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) }, onDeleteExpense = { expenseAndCategoryViewModel.delete(it) }, - isRecalculatingRate = isRecalculatingRate + isRecalculatingRate = isRecalculatingRate, + initialAutoOpen = initialAutoOpen, + onAutoOpenConsumed = onAutoOpenConsumed ) } @@ -108,12 +109,23 @@ fun ListExpenseScreen(filter: String) { fun ListExpenseScreen( expensesFlow: Flow>, onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit, - isRecalculatingRate: Boolean + isRecalculatingRate: Boolean, + initialAutoOpen: Boolean, + onAutoOpenConsumed: () -> Unit ) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var showBottomSheet by remember { mutableStateOf(false) } + + LaunchedEffect(initialAutoOpen) { + if (initialAutoOpen) { + showBottomSheet = true + onAutoOpenConsumed() + } + } + val items = expensesFlow.collectAsLazyPagingItems() val listState = rememberLazyListState() - var showBottomSheet by remember { mutableStateOf(false) } var expenseDtoToEdit by remember { mutableStateOf(null) } var itemToDelete by remember { mutableStateOf(null) } @@ -196,7 +208,7 @@ fun ListExpenseScreen( showBottomSheet = false }, expenseDtoToEdit = expenseDtoToEdit, - state = rememberModalBottomSheetState(skipPartiallyExpanded = true) + state = sheetState ) } } @@ -216,7 +228,9 @@ fun CustomDivider(date: LocalDate, sum: Double, currency: String) { date.format( DateTimeFormatter.ofPattern("dd EEEE") ).toString(), - modifier = Modifier.padding(horizontal = 5.dp).background(Color.White.copy(alpha = 0f)), + modifier = Modifier + .padding(horizontal = 5.dp) + .background(Color.White.copy(alpha = 0f)), style = MaterialTheme.typography.titleMedium ) Row( @@ -446,7 +460,9 @@ fun PreviewListExpenseScreen() { expensesFlow = MutableStateFlow(pagingData), onSaveExpense = {}, onDeleteExpense = {}, - true + isRecalculatingRate = true, + false, + {} ) } @@ -497,7 +513,8 @@ private fun sampleExpenseDtoWithConvertedAmountList(): List { id = 1, name = "Vacation", currency = "USD", - startDate = LocalDate.parse("2026-01-01") + startDate = LocalDate.parse("2026-01-01"), + endDate = LocalDate.parse("2026-01-11"), ) val startLong = LocalDateTime.now().minusDays(10).toEpochMilli() 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 ff4465d..0feb2ef 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 @@ -19,6 +19,7 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -69,12 +70,12 @@ import java.nio.file.Files fun SettingsScreen(navController: NavHostController) { val settingsViewModel: SettingsViewModel = hiltViewModel() val currentTheme by settingsViewModel.theme.collectAsState() + val currentAddExpenseSwitch by settingsViewModel.addExpenseSwitch.collectAsState() val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState() val currentTripId by settingsViewModel.currentTrip.collectAsState() val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel() val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY) - val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList()) val context = LocalContext.current val tripName = currentTrip?.name ?: "" val scope = rememberCoroutineScope() @@ -84,6 +85,9 @@ fun SettingsScreen(navController: NavHostController) { currentTheme = currentTheme, onThemeSave = { settingsViewModel.setTheme(it) }, onCurrencySave = { settingsViewModel.setDefaultCurrency(it) }, + onAddExpenseSwitch = { + settingsViewModel.setCurrentAddExpenseSwitch(it) + }, tripName = tripName, onExportToCsv = { scope.launch { @@ -98,7 +102,8 @@ fun SettingsScreen(navController: NavHostController) { } } }, - onCategoriesClick = {navController.navigate(Screens.MANAGE_CATEGORIES)} + onCategoriesClick = { navController.navigate(Screens.MANAGE_CATEGORIES) }, + currentAddExpenseSwitch = currentAddExpenseSwitch ) } @@ -111,13 +116,14 @@ fun SettingsScreen( onCurrencySave: (Currencies) -> Unit, tripName: String, onExportToCsv: () -> Unit, - onCategoriesClick: () -> Unit + onCategoriesClick: () -> Unit, + onAddExpenseSwitch: (Boolean) -> Unit, + currentAddExpenseSwitch: Boolean ) { Scaffold { padding -> var showThemeDialog by remember { mutableStateOf(false) } var showCurrencyDialog by remember { mutableStateOf(false) } - var showCategoriesDialog by remember { mutableStateOf(false) } Column( modifier = Modifier .fillMaxWidth() @@ -161,6 +167,15 @@ fun SettingsScreen( supportingText = stringResource(string.manage_categories), iconResource = R.drawable.materialsymbols_ic_label_outlined ) + SettingsListItem( + onClick = onCategoriesClick, + stringResource(string.add_expense), + supportingText = stringResource(string.add_expense_settings), + iconResource = R.drawable.materialsymbols_ic_payments_outlined, + trailingContent = { + Switch(checked = currentAddExpenseSwitch, onCheckedChange = {onAddExpenseSwitch(it)}) + } + ) if (showThemeDialog) { ThemeSelectionDialog( @@ -209,7 +224,7 @@ fun SettingsListItem( headlineText: String, trailingContent: @Composable () -> Unit = {}, supportingText: String, - iconResource: Int + iconResource: Int, ) { Card { ListItem( @@ -226,6 +241,7 @@ fun SettingsListItem( } } + @Composable fun ThemeSelectionDialog( onDismiss: () -> Unit, @@ -275,13 +291,16 @@ fun ThemeSelectionDialog( fun PreviewSettingsScreen() { TripMoneyTheme { SettingsScreen( - Currencies.entries.random(), - AppTheme.entries.random(), - {}, - {}, - "Włochy", - {}, - {}) + currentDefaultCurrency = Currencies.entries.random(), + currentTheme = AppTheme.entries.random(), + onThemeSave = {}, + onCurrencySave = {}, + onExportToCsv = {}, + tripName = "Włochy", + onCategoriesClick = {}, + onAddExpenseSwitch = {}, + currentAddExpenseSwitch = false + ) } } 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 b29fde8..e6ca67b 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 @@ -14,12 +14,8 @@ 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.material.icons.filled.Add -import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.FabPosition -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -31,13 +27,13 @@ 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.layout.ModifierLocalBeyondBoundsLayout +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt +import androidx.core.graphics.toColorLong import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import cc.n0th1ng.tripmoney.R.string import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Trip @@ -66,7 +62,8 @@ fun StatisticsScreen() { StatisticsScreen( summaryPerCategoryList, summaryAmount, - Currencies.valueOf(currentTrip?.currency ?: Currencies.default().name) + Currencies.valueOf(currentTrip?.currency ?: Currencies.default().name), + expenseAndCategoryViewModel.getBudgetLeft(currentTripId) ) } @@ -75,7 +72,8 @@ fun StatisticsScreen() { fun StatisticsScreen( summaryPerCategoryList: List, summaryAmount: Double, - tripCurrency: Currencies + tripCurrency: Currencies, + moneyLeft: Double ) { Column( modifier = Modifier @@ -83,48 +81,70 @@ fun StatisticsScreen( .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Summary(summaryAmount, tripCurrency.name) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Summary( + Modifier.weight(1f), -1 * summaryAmount, tripCurrency.name, + stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses), + R.drawable.materialsymbols_ic_payment_arrow_down_outlined, + iconColor = MaterialTheme.colorScheme.error + ) + Summary( + Modifier.weight(1f), moneyLeft, tripCurrency.name, + stringResource(cc.n0th1ng.tripmoney.R.string.money_left), + R.drawable.materialsymbols_ic_payments_outlined, + iconColor = colorResource(cc.n0th1ng.tripmoney.R.color.good_green) + ) + } SummaryPerCategoryCard(summaryPerCategoryList) + } } + @Composable -fun Summary(summaryAmount: Double, currency: String) { +fun Summary( + modifier: Modifier = Modifier, + amount: Double, + currency: String, + text: String, + icon: Int, + iconColor: Color +) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = modifier, colors = CardDefaults.elevatedCardColors() .copy(containerColor = MaterialTheme.colorScheme.surfaceContainer) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(15.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + Column( + modifier = Modifier.padding(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Column { + Row( + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Icon( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.surfaceDim, + shape = MaterialTheme.shapes.small + ) + .padding(5.dp), + painter = painterResource(icon), + tint = iconColor, + contentDescription = null, + ) Text( - stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses), + text, style = MaterialTheme.typography.titleSmall ) - Text( - "%.2f %s".format(summaryAmount, currency), - style = MaterialTheme.typography.headlineLarge - ) + } - Row( - horizontalArrangement = Arrangement.Center + Text( + "%.2f %s".format(amount, currency), + style = MaterialTheme.typography.titleLarge, ) - { - Icon( - painter = painterResource(R.drawable.materialsymbols_ic_payment_arrow_down_outlined), - contentDescription = null, - modifier = Modifier.size(45.dp) - ) - - } } - } } @@ -217,7 +237,8 @@ fun Preview() { StatisticsScreen( summaryPerCategoryList, summaryAmount = 125.24, - Currencies.entries.random() + Currencies.entries.random(), + 432.14 ) } } 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 c00c932..e32de5e 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 @@ -29,6 +29,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -37,6 +38,7 @@ 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.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -47,9 +49,11 @@ 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.screens.listexpense.DateRangePicker import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.utils.AllPreviews import cc.n0th1ng.tripmoney.utils.Currencies +import cc.n0th1ng.tripmoney.utils.pretty import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import io.ktor.http.hostIsIp import kotlinx.coroutines.CoroutineScope @@ -98,8 +102,15 @@ fun AddTripBottomSheet( ) } + var endDate by remember { + mutableStateOf( + tripToEdit?.startDate ?: LocalDate.now() + ) + } + var showCurrencyDialog by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) } + var budgetString by remember { mutableStateOf(tripToEdit?.budget?.toString() ?: "") } var currency by remember { mutableStateOf(tripToEdit?.currency ?: defaultCurrency.name) } var enableSave by remember { mutableStateOf(tripToEdit != null) } @@ -127,6 +138,23 @@ fun AddTripBottomSheet( name = newText enableSave = !name.isEmpty() }) + Row( + modifier = Modifier.fillMaxWidth(0.9f), + horizontalArrangement = Arrangement.spacedBy(15.dp), + verticalAlignment = Alignment.CenterVertically + ) { + BudgetInput( + modifier = Modifier.fillMaxWidth(0.7f), + budget = budgetString, + onTextChange = { newBudget -> budgetString = newBudget }) + CurrencyButton( + modifier = Modifier + .weight(1f) + .fillMaxWidth(1f), + onClick = { showCurrencyDialog = true }, text = currency + ) + } + Row( modifier = Modifier.fillMaxWidth(0.9f), horizontalArrangement = Arrangement.spacedBy(10.dp) @@ -137,17 +165,14 @@ fun AddTripBottomSheet( .weight(1f), shape = MaterialTheme.shapes.medium, onClick = { showDatePicker = true }) { + val startDateFormatted = startDate.pretty() + val endDateFormatted = endDate.pretty() Text( - text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")), + text = "$startDateFormatted - $endDateFormatted", fontSize = 17.sp ) } - CurrencyButton( - modifier = Modifier - .weight(1f) - .fillMaxWidth(1f), - onClick = { showCurrencyDialog = true }, text = currency - ) + } @@ -157,7 +182,13 @@ fun AddTripBottomSheet( shape = MaterialTheme.shapes.medium, onClick = { val trip = - Trip(name = name, startDate = startDate, currency = currency) + Trip( + name = name, + startDate = startDate, + endDate = endDate, + currency = currency, + budget = budgetString.toDoubleOrNull() ?: 0.0 + ) onSave(if (tripToEdit == null) trip else trip.copy(id = tripToEdit.id)) }) { @@ -182,10 +213,15 @@ fun AddTripBottomSheet( } if (showDatePicker) { - DatePicker(startDate, onDismiss = { showDatePicker = false }, onConfirm = { newDate -> - startDate = newDate - showDatePicker = false - }) + DateRangePicker( + startDate = startDate, + endDate = endDate, + onDismiss = { showDatePicker = false }, + onConfirm = { newStartDate, newEndDate -> + startDate = newStartDate + endDate = newEndDate + showDatePicker = false + }) } } @@ -201,6 +237,28 @@ fun NameInput(name: String, onTextChange: (String) -> Unit) { ) } +@Composable +fun BudgetInput(modifier: Modifier = Modifier, budget: String, onTextChange: (String) -> Unit) { + var text by remember { mutableStateOf(budget) } + OutlinedTextField( + placeholder = { Text("0.0") }, + modifier = modifier, + label = { Text(stringResource(R.string.budget)) }, + value = text, + onValueChange = { newText -> + val regex = Regex("^\\d*\\.?\\d{0,2}$") + if (regex.matches(newText)) { + text = newText + onTextChange(text) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Decimal, + imeAction = ImeAction.Done + ) + ) +} + @RequiresApi(Build.VERSION_CODES.O) @SuppressLint("CoroutineCreationDuringComposition") @@ -231,7 +289,8 @@ fun PreviewAddTripBottomSheetEditTrip() { AddTripBottomSheet( {}, {}, - Trip(1, "Włochy", LocalDate.parse("2025-01-02"), "PLN"), + Trip(1, "Włochy", LocalDate.parse("2025-01-02"), + LocalDate.parse("2025-01-15"), "PLN", budget = 0.0), sheetState, defaultCurrency = Currencies.entries.random() ) 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 63954f9..a42ac7c 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 @@ -58,6 +58,7 @@ import cc.n0th1ng.tripmoney.navigation.Screens import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.utils.AllPreviews +import cc.n0th1ng.tripmoney.utils.pretty import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import kotlinx.coroutines.flow.Flow @@ -204,6 +205,7 @@ fun SwipeToDeleteTripCard( } +@RequiresApi(Build.VERSION_CODES.O) @Composable fun TripCard( trip: Trip, @@ -236,15 +238,34 @@ fun TripCard( Column( modifier = Modifier.padding(16.dp) ) { - Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name) - Text(trip.startDate.toString()) + Text( + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + text = trip.name + ) + Text( + style = MaterialTheme.typography.bodySmall, + text = "start: " + trip.startDate.pretty() + "\nend: " + trip.endDate.pretty() + ) + } + Column( + modifier = Modifier.padding(end = 20.dp), + horizontalAlignment = Alignment.End) { + Text( + trip.currency.uppercase(), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + "budget:", + style = MaterialTheme.typography.bodySmall, + ) + Text( + "%.2f".format(trip.budget), + style = MaterialTheme.typography.bodySmall, + ) + } - Text( - trip.currency.uppercase(), - modifier = Modifier.padding(20.dp), - fontSize = 20.sp, - fontWeight = FontWeight.SemiBold - ) } } } @@ -258,18 +279,22 @@ fun PreviewTripPickerScreen() { 1, name = "Włochy", startDate = LocalDate.parse("2026-03-01"), - currency = "PLN" + endDate = LocalDate.parse("2026-03-14"), + currency = "PLN", + budget = 1053.53 ), Trip( 2, name = "Szwajcaria", startDate = LocalDate.parse("2025-03-01"), + endDate = LocalDate.parse("2025-03-11"), currency = "EUR" ), Trip( 3, name = "Portugalia", startDate = LocalDate.parse("2025-03-01"), + endDate = LocalDate.parse("2025-03-11"), currency = "USD" ) ) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/utils/DateUtils.kt b/app/src/main/java/cc/n0th1ng/tripmoney/utils/DateUtils.kt new file mode 100644 index 0000000..626cba8 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/utils/DateUtils.kt @@ -0,0 +1,11 @@ +package cc.n0th1ng.tripmoney.utils + +import android.os.Build +import androidx.annotation.RequiresApi +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +@RequiresApi(Build.VERSION_CODES.O) +fun LocalDate.pretty(): String { + return this.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) +} \ No newline at end of file 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 269acb8..4b61a8f 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt @@ -40,16 +40,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor( private val tripRepo: TripRepository ) : ViewModel() { - fun archiveCategory(category: Category) { - viewModelScope.launch { - categoryRepo.save(category.copy(archived = true)) - } - } - - fun deArchiveCategory(category: Category) { - viewModelScope.launch { - categoryRepo.save(category.copy(archived = false)) - } + fun getBudgetLeft(tripId: Int): Double { + return expenseRepo.getBudgetLeft(tripId) } fun getExpensesDtoPaged(tripId: Int, filter: String = ""): Flow> = 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 e7da36e..00c8e5e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SettingsViewModel.kt @@ -5,15 +5,23 @@ import android.content.Context import android.os.Build import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel -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.Provides import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ViewScoped +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -22,6 +30,7 @@ class SettingsViewModel @Inject constructor( private val repo: PreferencesRepository, @ApplicationContext private val context: Context ) : ViewModel() { + private val uiModeManager: UiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager val theme = repo.themeFlow @@ -31,6 +40,18 @@ class SettingsViewModel @Inject constructor( AppTheme.SYSTEM ) + val autoOpenStartupPref = repo.currentAddExpenseSwitchFlow + .take(1) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = null + ) + + val addExpenseSwitch = repo.currentAddExpenseSwitchFlow.stateIn( + viewModelScope, SharingStarted.WhileSubscribed(5000), false + ) + val currentTrip = repo.currentTripFlow.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), -1 @@ -47,6 +68,11 @@ class SettingsViewModel @Inject constructor( } } + fun setCurrentAddExpenseSwitch(value: Boolean) { + viewModelScope.launch { + repo.saveAddExpenseSwitch(value) + } + } fun setCurrentTrip(tripId: Int) { viewModelScope.launch { repo.saveCurrentTrip(tripId) @@ -77,5 +103,3 @@ class SettingsViewModel @Inject constructor( } } - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..5e81d96 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,5 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF + #A0D585 + \ 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 59ee799..12a3abd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,4 +36,7 @@ Do you want to archive? No expense will be deleted. Your all expenses with category Hotel will be removed. + Budget + Money left + Open add expense form on startup \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1b29632..4c5ec5b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.13.2" +iconsMaterialSymbolsOutlinedAndroid = "2.2.1" kotlin = "2.2.21" coreKtx = "1.10.1" junit = "4.13.2" @@ -17,6 +18,7 @@ profileinstaller = "1.3.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +icons-material-symbols-outlined-android = { module = "com.composables:icons-material-symbols-outlined-android", version.ref = "iconsMaterialSymbolsOutlinedAndroid" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }