init #48
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
|
||||||
"%.2f %s".format(summaryAmount, currency),
|
|
||||||
style = MaterialTheme.typography.headlineLarge
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Row(
|
Text(
|
||||||
horizontalArrangement = Arrangement.Center
|
"%.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(
|
StatisticsScreen(
|
||||||
summaryPerCategoryList,
|
summaryPerCategoryList,
|
||||||
summaryAmount = 125.24,
|
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.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,10 +213,15 @@ fun AddTripBottomSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showDatePicker) {
|
if (showDatePicker) {
|
||||||
DatePicker(startDate, onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
|
DateRangePicker(
|
||||||
startDate = newDate
|
startDate = startDate,
|
||||||
showDatePicker = false
|
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)
|
@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()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
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,
|
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"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
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
|
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>> =
|
||||||
|
|||||||
@@ -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(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user