init
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -49,6 +49,14 @@ interface ExpenseDao {
|
||||
)
|
||||
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
|
||||
suspend fun delete(expense: Expense)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Boolean> =
|
||||
context.preferencesDataStore.data.map { prefs ->
|
||||
val value = prefs[ADD_EXPENSE_SWITCH]
|
||||
?: false
|
||||
value
|
||||
}
|
||||
|
||||
val currentTripFlow: Flow<Int> =
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PagingData<ExpenseListItemUi>>,
|
||||
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<ExpenseDto?>(null) }
|
||||
var itemToDelete by remember { mutableStateOf<Expense?>(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<ExpenseListItemUi> {
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<SummaryPerCategory>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
|
||||
11
app/src/main/java/cc/n0th1ng/tripmoney/utils/DateUtils.kt
Normal file
11
app/src/main/java/cc/n0th1ng/tripmoney/utils/DateUtils.kt
Normal 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"))
|
||||
}
|
||||
@@ -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<PagingData<ExpenseDto>> =
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</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>
|
||||
<color name="good_green">#A0D585</color>
|
||||
|
||||
</resources>
|
||||
@@ -36,4 +36,7 @@
|
||||
<string name="you_want_archive">Do you want to archive?</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="budget">Budget</string>
|
||||
<string name="money_left">Money left</string>
|
||||
<string name="add_expense_settings">Open add expense form on startup</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user