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