init #48

Merged
admin merged 18 commits from develop into main 2026-04-30 10:35:58 +02:00
20 changed files with 381 additions and 135 deletions
Showing only changes of commit 767d54e8f6 - Show all commits

View File

@@ -97,7 +97,7 @@ dependencies {
implementation("androidx.paging:paging-compose:3.4.2") implementation("androidx.paging:paging-compose:3.4.2")
implementation("androidx.datastore:datastore-preferences:1.2.1") 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") implementation("com.google.dagger:hilt-android:2.57.1")
ksp("com.google.dagger:hilt-android-compiler:2.57.1") ksp("com.google.dagger:hilt-android-compiler:2.57.1")

View File

@@ -16,15 +16,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.navigation.BottomNavigation 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.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -53,12 +58,6 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
TripMoneyTheme { TripMoneyTheme {
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTripId by settingsViewModel.currentTrip.collectAsState()
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
expenseAndCategoryViewModel.clearOldRates()
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId)
.collectAsLazyPagingItems()
NavigationDrawer() NavigationDrawer()
} }
} }
@@ -78,12 +77,12 @@ fun NavigationDrawer() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var filter by remember { mutableStateOf("") } 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) { CustomNavigationDrawer(navController, drawerState) {
Scaffold( Scaffold(
modifier = Modifier.semantics {
testTagsAsResourceId = true
},
topBar = { topBar = {
if (current == Screens.SETTINGS) TopBarSettings( if (current == Screens.SETTINGS) TopBarSettings(
navController navController
@@ -99,7 +98,7 @@ fun NavigationDrawer() {
} }
}, },
isSearchable = current == Screens.LIST_EXPENSE, isSearchable = current == Screens.LIST_EXPENSE,
onFilterChange = { newFilter -> filter = newFilter}) onFilterChange = { newFilter -> filter = newFilter })
}, },
bottomBar = { BottomNavigation(navController) }) { innerPadding -> bottomBar = { BottomNavigation(navController) }) { innerPadding ->
@@ -109,7 +108,10 @@ fun NavigationDrawer() {
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) { ) {
composable(Screens.LIST_EXPENSE) { composable(Screens.LIST_EXPENSE) {
ListExpenseScreen(filter) ListExpenseScreen(
filter,
initialAutoOpen = shouldTriggerAutoOpen,
onAutoOpenConsumed = { hasHandledStartupOpen = true })
} }
composable(Screens.TRIP_PICKER) { composable(Screens.TRIP_PICKER) {
TripPickerScreen(navController) TripPickerScreen(navController)

View File

@@ -120,6 +120,7 @@ private class DatabasePrepopulator(
Trip( Trip(
name = "Włochy", name = "Włochy",
startDate = LocalDate.parse("2026-03-01"), startDate = LocalDate.parse("2026-03-01"),
endDate = LocalDate.parse("2026-03-15"),
currency = "PLN" currency = "PLN"
) )
) )
@@ -127,6 +128,7 @@ private class DatabasePrepopulator(
Trip( Trip(
name = "Szwajcaria", name = "Szwajcaria",
startDate = LocalDate.parse("2025-03-01"), startDate = LocalDate.parse("2025-03-01"),
endDate = LocalDate.parse("2025-03-15"),
currency = "EUR" currency = "EUR"
) )
) )
@@ -134,6 +136,7 @@ private class DatabasePrepopulator(
Trip( Trip(
name = "Portugalia", name = "Portugalia",
startDate = LocalDate.parse("2025-03-01"), startDate = LocalDate.parse("2025-03-01"),
endDate = LocalDate.parse("2025-03-15"),
currency = "USD" currency = "USD"
) )
) )

View File

@@ -49,6 +49,14 @@ interface ExpenseDao {
) )
fun expenseDto(tripId: Int, filter: String): Flow<List<ExpenseDto>> fun expenseDto(tripId: Int, filter: String): Flow<List<ExpenseDto>>
@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 @Delete
suspend fun delete(expense: Expense) suspend fun delete(expense: Expense)
} }

View File

@@ -15,11 +15,17 @@ data class Trip(
@PrimaryKey(autoGenerate = true) val id: Int = 0, @PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo("name") val name: String, @ColumnInfo("name") val name: String,
@ColumnInfo("start_date") val startDate: LocalDate, @ColumnInfo("start_date") val startDate: LocalDate,
@ColumnInfo("end_date") val endDate: LocalDate,
@ColumnInfo("currency") val currency: String, @ColumnInfo("currency") val currency: String,
@ColumnInfo("budget") val budget: Double @ColumnInfo("budget") val budget: Double = 0.0
){ ) {
companion object { companion object {
@RequiresApi(Build.VERSION_CODES.O) @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,
)
} }
} }

View File

@@ -25,6 +25,10 @@ class ExpenseRepository @Inject constructor(
private val exchangeRateRepository: ExchangeRateRepository private val exchangeRateRepository: ExchangeRateRepository
) { ) {
fun getBudgetLeft(tripId: Int): Double {
return expenseDao.budgetLeft(tripId)
}
@WorkerThread @WorkerThread
suspend fun save(expense: Expense) { suspend fun save(expense: Expense) {
expenseDao.insert(expense) expenseDao.insert(expense)

View File

@@ -1,10 +1,12 @@
package cc.n0th1ng.tripmoney.data.repository package cc.n0th1ng.tripmoney.data.repository
import android.content.Context import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore 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.APP_THEME
import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.CURRENT_TRIP import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.CURRENT_TRIP
import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.DEFAULT_CURRENCY import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.DEFAULT_CURRENCY
@@ -23,6 +25,7 @@ object PreferenceKeys {
val APP_THEME = intPreferencesKey("app_theme") val APP_THEME = intPreferencesKey("app_theme")
val CURRENT_TRIP = intPreferencesKey("current_trip") val CURRENT_TRIP = intPreferencesKey("current_trip")
val DEFAULT_CURRENCY = stringPreferencesKey("default_currency") 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) AppTheme.fromValue(value)
} }
val currentAddExpenseSwitchFlow: Flow<Boolean> =
context.preferencesDataStore.data.map { prefs ->
val value = prefs[ADD_EXPENSE_SWITCH]
?: false
value
}
val currentTripFlow: Flow<Int> = val currentTripFlow: Flow<Int> =
context.preferencesDataStore.data.map { prefs -> context.preferencesDataStore.data.map { prefs ->
prefs[CURRENT_TRIP] ?: -1 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) { enum class AppTheme(val value: Int) {

View File

@@ -2,13 +2,10 @@ package cc.n0th1ng.tripmoney.screens.addexpense
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.focusable 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -34,7 +31,6 @@ import androidx.compose.material3.SheetState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf 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.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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 com.composables.icons.materialsymbols.outlined.R.drawable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -104,7 +97,7 @@ fun AddExpenseBottomSheet(
onDismiss = onDismiss, onDismiss = onDismiss,
expenseDtoToEdit = expenseDtoToEdit, expenseDtoToEdit = expenseDtoToEdit,
state = state, state = state,
currentTrip = currentTrip!!, currentTrip = currentTrip ?: Trip.DUMMY,
categories = categories categories = categories
) )
} }
@@ -128,10 +121,14 @@ fun AddExpenseBottomSheet(
var amount by remember { var amount by remember {
mutableStateOf( 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() } val dummyFocusRequester = remember { FocusRequester() }
var showCurrencyDialog by remember { mutableStateOf(false) } var showCurrencyDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) } var showCategoryDialog by remember { mutableStateOf(false) }
@@ -193,7 +190,7 @@ fun AddExpenseBottomSheet(
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(
text = if (amount.contains(Regex("[+\\/*-]\\d+"))) "%.2f".format( text = if (amount.contains(Regex("[+/*-]\\d+"))) "%.2f".format(
equationResult equationResult
) else "", ) else "",
fontSize = 14.sp, fontSize = 14.sp,
@@ -240,7 +237,7 @@ fun AddExpenseBottomSheet(
NumberKeyboard( NumberKeyboard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onOperatorClick = { operator -> onOperatorClick = { operator ->
if (amount.isDoubleTwoDigitsOrEquation() && amount.contains(Regex("[+\\/*-]\\d+"))) { if (amount.isDoubleTwoDigitsOrEquation() && amount.contains(Regex("[+/*-]\\d+"))) {
amount = evaluate(amount).toString() amount = evaluate(amount).toString()
} }
val newText = amount + operator val newText = amount + operator
@@ -369,7 +366,7 @@ private inline fun String.indexOfFirstIndexed(predicate: (index: Int, Char) -> B
} }
private fun String.isDoubleTwoDigitsOrEquation(): Boolean { 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 @Composable
@@ -518,7 +515,6 @@ fun KeyboardButton(
text: String? = null, text: String? = null,
icon: Painter? = null, icon: Painter? = null,
onClick: () -> Unit, onClick: () -> Unit,
enabled: Boolean = true,
onLongClick: () -> Unit = {}, onLongClick: () -> Unit = {},
containerColor: Color = MaterialTheme.colorScheme.primary, containerColor: Color = MaterialTheme.colorScheme.primary,
contentColor: Color = MaterialTheme.colorScheme.onPrimary contentColor: Color = MaterialTheme.colorScheme.onPrimary
@@ -574,6 +570,7 @@ fun PreviewAddExpenseDisabled() {
1, 1,
"Trip", "Trip",
LocalDate.parse("2020-01-01"), LocalDate.parse("2020-01-01"),
LocalDate.parse("2020-01-15"),
Currencies.entries.random().name Currencies.entries.random().name
), ),
categories = categoriesToPreview categories = categoriesToPreview
@@ -607,13 +604,17 @@ fun PreviewAddExpenseEnabled() {
tripId = 1 tripId = 1
), ),
category = categoriesToPreview[0], 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, state = sheetState,
currentTrip = Trip( currentTrip = Trip(
1, 1,
"Trip", "Trip",
LocalDate.parse("2020-01-01"), LocalDate.parse("2020-01-01"),
LocalDate.parse("2020-01-11"),
Currencies.entries.random().name Currencies.entries.random().name
), ),
categories = categoriesToPreview categories = categoriesToPreview

View File

@@ -5,14 +5,14 @@ import androidx.annotation.RequiresApi
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DatePickerState import androidx.compose.material3.DateRangePicker
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerState import androidx.compose.material3.TimePickerState
import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberDateRangePickerState
import androidx.compose.material3.rememberTimePickerState import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -20,17 +20,55 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import cc.n0th1ng.tripmoney.R.* import cc.n0th1ng.tripmoney.R.*
import java.sql.Time
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar 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) @RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View File

@@ -46,14 +46,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.LoadState
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey import androidx.paging.compose.itemKey
@@ -73,7 +71,6 @@ import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -83,7 +80,9 @@ import kotlin.random.Random
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun ListExpenseScreen(filter: String) { fun ListExpenseScreen(filter: String,
initialAutoOpen: Boolean,
onAutoOpenConsumed: () -> Unit ) {
val settingsViewModel: SettingsViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel()
val tripViewModel: TripViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel()
val currentTripId by settingsViewModel.currentTrip.collectAsState() val currentTripId by settingsViewModel.currentTrip.collectAsState()
@@ -97,7 +96,9 @@ fun ListExpenseScreen(filter: String) {
expensesFlow = expensesFlow, expensesFlow = expensesFlow,
onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) }, onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) },
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) }, onDeleteExpense = { expenseAndCategoryViewModel.delete(it) },
isRecalculatingRate = isRecalculatingRate isRecalculatingRate = isRecalculatingRate,
initialAutoOpen = initialAutoOpen,
onAutoOpenConsumed = onAutoOpenConsumed
) )
} }
@@ -108,12 +109,23 @@ fun ListExpenseScreen(filter: String) {
fun ListExpenseScreen( fun ListExpenseScreen(
expensesFlow: Flow<PagingData<ExpenseListItemUi>>, expensesFlow: Flow<PagingData<ExpenseListItemUi>>,
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit, 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 items = expensesFlow.collectAsLazyPagingItems()
val listState = rememberLazyListState() val listState = rememberLazyListState()
var showBottomSheet by remember { mutableStateOf(false) }
var expenseDtoToEdit by remember { mutableStateOf<ExpenseDto?>(null) } var expenseDtoToEdit by remember { mutableStateOf<ExpenseDto?>(null) }
var itemToDelete by remember { mutableStateOf<Expense?>(null) } var itemToDelete by remember { mutableStateOf<Expense?>(null) }
@@ -196,7 +208,7 @@ fun ListExpenseScreen(
showBottomSheet = false showBottomSheet = false
}, },
expenseDtoToEdit = expenseDtoToEdit, expenseDtoToEdit = expenseDtoToEdit,
state = rememberModalBottomSheetState(skipPartiallyExpanded = true) state = sheetState
) )
} }
} }
@@ -216,7 +228,9 @@ fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
date.format( date.format(
DateTimeFormatter.ofPattern("dd EEEE") DateTimeFormatter.ofPattern("dd EEEE")
).toString(), ).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 style = MaterialTheme.typography.titleMedium
) )
Row( Row(
@@ -446,7 +460,9 @@ fun PreviewListExpenseScreen() {
expensesFlow = MutableStateFlow(pagingData), expensesFlow = MutableStateFlow(pagingData),
onSaveExpense = {}, onSaveExpense = {},
onDeleteExpense = {}, onDeleteExpense = {},
true isRecalculatingRate = true,
false,
{}
) )
} }
@@ -497,7 +513,8 @@ private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseListItemUi> {
id = 1, id = 1,
name = "Vacation", name = "Vacation",
currency = "USD", 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() val startLong = LocalDateTime.now().minusDays(10).toEpochMilli()

View File

@@ -19,6 +19,7 @@ import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -69,12 +70,12 @@ import java.nio.file.Files
fun SettingsScreen(navController: NavHostController) { fun SettingsScreen(navController: NavHostController) {
val settingsViewModel: SettingsViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTheme by settingsViewModel.theme.collectAsState() val currentTheme by settingsViewModel.theme.collectAsState()
val currentAddExpenseSwitch by settingsViewModel.addExpenseSwitch.collectAsState()
val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState() val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
val currentTripId by settingsViewModel.currentTrip.collectAsState() val currentTripId by settingsViewModel.currentTrip.collectAsState()
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val tripViewModel: TripViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel()
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY) val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
val context = LocalContext.current val context = LocalContext.current
val tripName = currentTrip?.name ?: "" val tripName = currentTrip?.name ?: ""
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -84,6 +85,9 @@ fun SettingsScreen(navController: NavHostController) {
currentTheme = currentTheme, currentTheme = currentTheme,
onThemeSave = { settingsViewModel.setTheme(it) }, onThemeSave = { settingsViewModel.setTheme(it) },
onCurrencySave = { settingsViewModel.setDefaultCurrency(it) }, onCurrencySave = { settingsViewModel.setDefaultCurrency(it) },
onAddExpenseSwitch = {
settingsViewModel.setCurrentAddExpenseSwitch(it)
},
tripName = tripName, tripName = tripName,
onExportToCsv = { onExportToCsv = {
scope.launch { 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, onCurrencySave: (Currencies) -> Unit,
tripName: String, tripName: String,
onExportToCsv: () -> Unit, onExportToCsv: () -> Unit,
onCategoriesClick: () -> Unit onCategoriesClick: () -> Unit,
onAddExpenseSwitch: (Boolean) -> Unit,
currentAddExpenseSwitch: Boolean
) { ) {
Scaffold { padding -> Scaffold { padding ->
var showThemeDialog by remember { mutableStateOf(false) } var showThemeDialog by remember { mutableStateOf(false) }
var showCurrencyDialog by remember { mutableStateOf(false) } var showCurrencyDialog by remember { mutableStateOf(false) }
var showCategoriesDialog by remember { mutableStateOf(false) }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -161,6 +167,15 @@ fun SettingsScreen(
supportingText = stringResource(string.manage_categories), supportingText = stringResource(string.manage_categories),
iconResource = R.drawable.materialsymbols_ic_label_outlined 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) { if (showThemeDialog) {
ThemeSelectionDialog( ThemeSelectionDialog(
@@ -209,7 +224,7 @@ fun SettingsListItem(
headlineText: String, headlineText: String,
trailingContent: @Composable () -> Unit = {}, trailingContent: @Composable () -> Unit = {},
supportingText: String, supportingText: String,
iconResource: Int iconResource: Int,
) { ) {
Card { Card {
ListItem( ListItem(
@@ -226,6 +241,7 @@ fun SettingsListItem(
} }
} }
@Composable @Composable
fun ThemeSelectionDialog( fun ThemeSelectionDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
@@ -275,13 +291,16 @@ fun ThemeSelectionDialog(
fun PreviewSettingsScreen() { fun PreviewSettingsScreen() {
TripMoneyTheme { TripMoneyTheme {
SettingsScreen( SettingsScreen(
Currencies.entries.random(), currentDefaultCurrency = Currencies.entries.random(),
AppTheme.entries.random(), currentTheme = AppTheme.entries.random(),
{}, onThemeSave = {},
{}, onCurrencySave = {},
"Włochy", onExportToCsv = {},
{}, tripName = "Włochy",
{}) onCategoriesClick = {},
onAddExpenseSwitch = {},
currentAddExpenseSwitch = false
)
} }
} }

View File

@@ -14,12 +14,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape 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.CardDefaults
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -31,13 +27,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.core.graphics.toColorLong
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel 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.dto.SummaryPerCategory
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.data.entity.Trip
@@ -66,7 +62,8 @@ fun StatisticsScreen() {
StatisticsScreen( StatisticsScreen(
summaryPerCategoryList, summaryPerCategoryList,
summaryAmount, 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( fun StatisticsScreen(
summaryPerCategoryList: List<SummaryPerCategory>, summaryPerCategoryList: List<SummaryPerCategory>,
summaryAmount: Double, summaryAmount: Double,
tripCurrency: Currencies tripCurrency: Currencies,
moneyLeft: Double
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -83,48 +81,70 @@ fun StatisticsScreen(
.fillMaxSize(), .fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp) 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) SummaryPerCategoryCard(summaryPerCategoryList)
} }
} }
@Composable @Composable
fun Summary(summaryAmount: Double, currency: String) { fun Summary(
modifier: Modifier = Modifier,
amount: Double,
currency: String,
text: String,
icon: Int,
iconColor: Color
) {
ElevatedCard( ElevatedCard(
modifier = Modifier.fillMaxWidth(), modifier = modifier,
colors = CardDefaults.elevatedCardColors() colors = CardDefaults.elevatedCardColors()
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer) .copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
) { ) {
Row( Column(
modifier = Modifier modifier = Modifier.padding(10.dp),
.fillMaxWidth() verticalArrangement = Arrangement.spacedBy(10.dp)
.padding(15.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) { ) {
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( Text(
stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses), text,
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall
) )
}
Text( Text(
"%.2f %s".format(summaryAmount, currency), "%.2f %s".format(amount, currency),
style = MaterialTheme.typography.headlineLarge style = MaterialTheme.typography.titleLarge,
) )
} }
Row(
horizontalArrangement = Arrangement.Center
)
{
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( StatisticsScreen(
summaryPerCategoryList, summaryPerCategoryList,
summaryAmount = 125.24, summaryAmount = 125.24,
Currencies.entries.random() Currencies.entries.random(),
432.14
) )
} }
} }

View File

@@ -29,6 +29,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -37,6 +38,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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.addexpense.CurrencyButton
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker
import cc.n0th1ng.tripmoney.screens.listexpense.DateRangePicker
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.pretty
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import io.ktor.http.hostIsIp import io.ktor.http.hostIsIp
import kotlinx.coroutines.CoroutineScope 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 showCurrencyDialog by remember { mutableStateOf(false) }
var showDatePicker 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 currency by remember { mutableStateOf(tripToEdit?.currency ?: defaultCurrency.name) }
var enableSave by remember { mutableStateOf(tripToEdit != null) } var enableSave by remember { mutableStateOf(tripToEdit != null) }
@@ -127,6 +138,23 @@ fun AddTripBottomSheet(
name = newText name = newText
enableSave = !name.isEmpty() 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( Row(
modifier = Modifier.fillMaxWidth(0.9f), modifier = Modifier.fillMaxWidth(0.9f),
horizontalArrangement = Arrangement.spacedBy(10.dp) horizontalArrangement = Arrangement.spacedBy(10.dp)
@@ -137,17 +165,14 @@ fun AddTripBottomSheet(
.weight(1f), .weight(1f),
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
onClick = { showDatePicker = true }) { onClick = { showDatePicker = true }) {
val startDateFormatted = startDate.pretty()
val endDateFormatted = endDate.pretty()
Text( Text(
text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")), text = "$startDateFormatted - $endDateFormatted",
fontSize = 17.sp 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, shape = MaterialTheme.shapes.medium,
onClick = { onClick = {
val trip = 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)) onSave(if (tripToEdit == null) trip else trip.copy(id = tripToEdit.id))
}) { }) {
@@ -182,8 +213,13 @@ fun AddTripBottomSheet(
} }
if (showDatePicker) { if (showDatePicker) {
DatePicker(startDate, onDismiss = { showDatePicker = false }, onConfirm = { newDate -> DateRangePicker(
startDate = newDate startDate = startDate,
endDate = endDate,
onDismiss = { showDatePicker = false },
onConfirm = { newStartDate, newEndDate ->
startDate = newStartDate
endDate = newEndDate
showDatePicker = false 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) @RequiresApi(Build.VERSION_CODES.O)
@SuppressLint("CoroutineCreationDuringComposition") @SuppressLint("CoroutineCreationDuringComposition")
@@ -231,7 +289,8 @@ fun PreviewAddTripBottomSheetEditTrip() {
AddTripBottomSheet( 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, sheetState,
defaultCurrency = Currencies.entries.random() defaultCurrency = Currencies.entries.random()
) )

View File

@@ -58,6 +58,7 @@ import cc.n0th1ng.tripmoney.navigation.Screens
import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.pretty
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -204,6 +205,7 @@ fun SwipeToDeleteTripCard(
} }
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun TripCard( fun TripCard(
trip: Trip, trip: Trip,
@@ -236,15 +238,34 @@ fun TripCard(
Column( Column(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) { ) {
Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name) Text(
Text(trip.startDate.toString()) 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( Text(
trip.currency.uppercase(), trip.currency.uppercase(),
modifier = Modifier.padding(20.dp), style = MaterialTheme.typography.titleLarge,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
Text(
"budget:",
style = MaterialTheme.typography.bodySmall,
)
Text(
"%.2f".format(trip.budget),
style = MaterialTheme.typography.bodySmall,
)
}
} }
} }
} }
@@ -258,18 +279,22 @@ fun PreviewTripPickerScreen() {
1, 1,
name = "Włochy", name = "Włochy",
startDate = LocalDate.parse("2026-03-01"), startDate = LocalDate.parse("2026-03-01"),
currency = "PLN" endDate = LocalDate.parse("2026-03-14"),
currency = "PLN",
budget = 1053.53
), ),
Trip( Trip(
2, 2,
name = "Szwajcaria", name = "Szwajcaria",
startDate = LocalDate.parse("2025-03-01"), startDate = LocalDate.parse("2025-03-01"),
endDate = LocalDate.parse("2025-03-11"),
currency = "EUR" currency = "EUR"
), ),
Trip( Trip(
3, 3,
name = "Portugalia", name = "Portugalia",
startDate = LocalDate.parse("2025-03-01"), startDate = LocalDate.parse("2025-03-01"),
endDate = LocalDate.parse("2025-03-11"),
currency = "USD" currency = "USD"
) )
) )

View File

@@ -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"))
}

View File

@@ -40,16 +40,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
private val tripRepo: TripRepository private val tripRepo: TripRepository
) : ViewModel() { ) : ViewModel() {
fun archiveCategory(category: Category) { fun getBudgetLeft(tripId: Int): Double {
viewModelScope.launch { return expenseRepo.getBudgetLeft(tripId)
categoryRepo.save(category.copy(archived = true))
}
}
fun deArchiveCategory(category: Category) {
viewModelScope.launch {
categoryRepo.save(category.copy(archived = false))
}
} }
fun getExpensesDtoPaged(tripId: Int, filter: String = ""): Flow<PagingData<ExpenseDto>> = fun getExpensesDtoPaged(tripId: Int, filter: String = ""): Flow<PagingData<ExpenseDto>> =

View File

@@ -5,15 +5,23 @@ import android.content.Context
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import cc.n0th1ng.tripmoney.data.repository.AppTheme import cc.n0th1ng.tripmoney.data.repository.AppTheme
import cc.n0th1ng.tripmoney.data.repository.PreferencesRepository import cc.n0th1ng.tripmoney.data.repository.PreferencesRepository
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import dagger.Provides
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext 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.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.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -22,6 +30,7 @@ class SettingsViewModel @Inject constructor(
private val repo: PreferencesRepository, private val repo: PreferencesRepository,
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) : ViewModel() { ) : ViewModel() {
private val uiModeManager: UiModeManager = private val uiModeManager: UiModeManager =
context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
val theme = repo.themeFlow val theme = repo.themeFlow
@@ -31,6 +40,18 @@ class SettingsViewModel @Inject constructor(
AppTheme.SYSTEM 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( val currentTrip = repo.currentTripFlow.stateIn(
viewModelScope, SharingStarted.WhileSubscribed(5000), viewModelScope, SharingStarted.WhileSubscribed(5000),
-1 -1
@@ -47,6 +68,11 @@ class SettingsViewModel @Inject constructor(
} }
} }
fun setCurrentAddExpenseSwitch(value: Boolean) {
viewModelScope.launch {
repo.saveAddExpenseSwitch(value)
}
}
fun setCurrentTrip(tripId: Int) { fun setCurrentTrip(tripId: Int) {
viewModelScope.launch { viewModelScope.launch {
repo.saveCurrentTrip(tripId) repo.saveCurrentTrip(tripId)
@@ -77,5 +103,3 @@ class SettingsViewModel @Inject constructor(
} }
} }

View File

@@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="purple_200">#FFBB86FC</color> <color name="good_green">#A0D585</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources> </resources>

View File

@@ -36,4 +36,7 @@
<string name="you_want_archive">Do you want to archive?</string> <string name="you_want_archive">Do you want to archive?</string>
<string name="archive_category_info">No expense will be deleted.</string> <string name="archive_category_info">No expense will be deleted.</string>
<string name="delete_category_info">Your all expenses with category Hotel will be removed.</string> <string name="delete_category_info">Your all expenses with category Hotel will be removed.</string>
<string name="budget">Budget</string>
<string name="money_left">Money left</string>
<string name="add_expense_settings">Open add expense form on startup</string>
</resources> </resources>

View File

@@ -1,5 +1,6 @@
[versions] [versions]
agp = "8.13.2" agp = "8.13.2"
iconsMaterialSymbolsOutlinedAndroid = "2.2.1"
kotlin = "2.2.21" kotlin = "2.2.21"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"
@@ -17,6 +18,7 @@ profileinstaller = "1.3.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }