Compare commits

..

13 Commits

Author SHA1 Message Date
Rafal Wisniewski
ae5394aa59 fix: add default colors and icons 2026-05-06 14:01:46 +02:00
Rafal Wisniewski
dae0212cf9 fix: sort stats per day 2026-05-06 13:03:17 +02:00
Rafal Wisniewski
270ff4fa07 fix: add date to filter 2026-05-06 12:47:11 +02:00
Rafal Wisniewski
f83bf62655 fix: add search to category picker 2026-05-06 10:49:23 +02:00
Rafal Wisniewski
6c067f64ce fix: add search to category picker 2026-05-06 10:49:09 +02:00
Rafal Wisniewski
79551ab69d fix: search & filter on list expense screen 2026-05-04 22:01:38 +02:00
Rafal Wisniewski
38f3760cef fix: math on add expense screen 2026-05-04 21:57:23 +02:00
Rafal Wisniewski
9225f2275f fix: adjust style of delete confirmation dialog 2026-05-04 21:42:55 +02:00
Rafal Wisniewski
aad0de1499 fix: scroll to date when clicked on stats screen 2026-05-04 15:44:32 +02:00
Rafal Wisniewski
5fb54bf18e fix: scroll to newly added item 2026-05-04 15:12:19 +02:00
Rafal Wisniewski
bf9309a155 fix: add currencies and search to them 2026-05-03 22:34:52 +02:00
Rafal Wisniewski
cc10ddabbe feat: add categories per day stats 2026-05-01 15:13:58 +02:00
Rafal Wisniewski
3286bcf87a fix: add info text when no data 2026-05-01 13:10:27 +02:00
26 changed files with 1133 additions and 352 deletions

View File

@@ -15,7 +15,7 @@ android {
} }
defaultConfig { defaultConfig {
applicationId = "cc.n0th1ng.tripmoney" applicationId = "cc.n0th1ng.tripmoney"
minSdk = 24 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"

View File

@@ -12,7 +12,6 @@ import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
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.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -26,6 +25,7 @@ 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 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
import cc.n0th1ng.tripmoney.navigation.BottomNavigation import cc.n0th1ng.tripmoney.navigation.BottomNavigation
@@ -44,6 +44,7 @@ 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.launch import kotlinx.coroutines.launch
import java.time.LocalDate
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -57,7 +58,6 @@ class MainActivity : ComponentActivity() {
NavigationDrawer() NavigationDrawer()
} }
} }
} }
} }
@@ -97,7 +97,7 @@ fun NavigationDrawer() {
} }
} }
}, },
isSearchable = current == Screens.LIST_EXPENSE, isSearchable = current != null && current.contains(Screens.LIST_EXPENSE),
onSearchChange = { newSearch -> search = newSearch }, onSearchChange = { newSearch -> search = newSearch },
onFilterChange = { newFilter -> filter = newFilter }, onFilterChange = { newFilter -> filter = newFilter },
categories = categories, categories = categories,
@@ -111,17 +111,22 @@ fun NavigationDrawer() {
startDestination = if (currentTripId == -1) Screens.TRIP_PICKER else Screens.LIST_EXPENSE, startDestination = if (currentTripId == -1) Screens.TRIP_PICKER else Screens.LIST_EXPENSE,
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) { ) {
composable(Screens.LIST_EXPENSE) { composable(
Screens.LIST_EXPENSE + "?dateToScroll={dateToScroll}",
arguments = listOf(navArgument("dateToScroll") { defaultValue = "" })
) { backStackEntry ->
ListExpenseScreen( ListExpenseScreen(
filter = filter, search = search, filter = filter, search = search,
initialAutoOpen = shouldTriggerAutoOpen, initialAutoOpen = shouldTriggerAutoOpen,
onAutoOpenConsumed = { hasHandledStartupOpen = true }) onAutoOpenConsumed = { hasHandledStartupOpen = true },
dateToScroll = backStackEntry.arguments?.getString("dateToScroll") ?: ""
)
} }
composable(Screens.TRIP_PICKER) { composable(Screens.TRIP_PICKER) {
TripPickerScreen(navController) TripPickerScreen(navController)
} }
composable(Screens.STATISTICS) { composable(Screens.STATISTICS) {
StatisticsScreen() StatisticsScreen(navController)
} }
composable(Screens.SETTINGS) { composable(Screens.SETTINGS) {
SettingsScreen(navController) SettingsScreen(navController)
@@ -136,7 +141,9 @@ fun NavigationDrawer() {
data class Filter( data class Filter(
val categories: List<Category> = emptyList(), val startAmount: Double = 0.0, val categories: List<Category> = emptyList(), val startAmount: Double = 0.0,
val endAmount: Double = Double.MAX_VALUE val endAmount: Double = Double.MAX_VALUE,
val startDate: LocalDate = LocalDate.MIN,
val endDate: LocalDate = LocalDate.MAX
) { ) {
fun with(category: Category): Filter { fun with(category: Category): Filter {
return this.copy(categories = categories + category) return this.copy(categories = categories + category)
@@ -150,11 +157,19 @@ data class Filter(
return this.copy(endAmount = amount) return this.copy(endAmount = amount)
} }
fun withStartDate(date: LocalDate): Filter {
return this.copy(startDate = date)
}
fun withEndDate(date: LocalDate): Filter {
return this.copy(endDate = date)
}
fun without(category: Category): Filter { fun without(category: Category): Filter {
return this.copy(categories = categories - category) return this.copy(categories = categories - category)
} }
fun isDefault(): Boolean { fun isDefault(): Boolean {
return this.categories.isEmpty() && startAmount == 0.0 && endAmount == Double.MAX_VALUE return this.categories.isEmpty() && startAmount == 0.0 && endAmount == Double.MAX_VALUE && startDate == LocalDate.MIN && endDate == LocalDate.MAX
} }
} }

View File

@@ -49,8 +49,6 @@ abstract class TripDatabase : RoomDatabase() {
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object DatabaseModule { object DatabaseModule {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Provides @Provides
@Singleton @Singleton
@@ -155,16 +153,16 @@ private class DatabasePrepopulator(
name = "Hotel", icon = Icons.HOTEL, color = colors.random() name = "Hotel", icon = Icons.HOTEL, color = colors.random()
), ),
Category( Category(
name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random() name = "Restaurants", icon = Icons.RESTAURANT, color = colors.random()
), ),
Category( Category(
name = "Transport", icon = Icons.FLIGHT, color = colors.random() name = "Transport", icon = Icons.FLIGHT, color = colors.random()
), ),
Category( Category(
name = "Rozrywka", icon = Icons.ATTRACTION, color = colors.random() name = "Entertainment", icon = Icons.ATTRACTION, color = colors.random()
), ),
Category( Category(
name = "Zakupy", icon = Icons.GROCERIES, color = colors.random() name = "Groceries", icon = Icons.GROCERIES, color = colors.random()
), ),
Category( Category(
name = "Zakupy1", icon = Icons.GROCERIES, color = colors.random() name = "Zakupy1", icon = Icons.GROCERIES, color = colors.random()

View File

@@ -2,7 +2,6 @@ package cc.n0th1ng.tripmoney.data.dao
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
@@ -21,7 +20,7 @@ interface CategoryDao {
@Transaction @Transaction
@Query( @Query(
""" """
SELECT * FROM category WHERE archived is 0 SELECT * FROM category WHERE archived is 0 ORDER BY name
""" """
) )
fun categories(): Flow<List<Category>> fun categories(): Flow<List<Category>>

View File

@@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.Flow
interface ExpenseDao { interface ExpenseDao {
@Upsert @Upsert
suspend fun insert(expense: Expense) suspend fun insert(expense: Expense): Long
@Transaction @Transaction
@@ -40,6 +40,12 @@ interface ExpenseDao {
AND ( AND (
:endAmount IS NULL OR expense.amount <= :endAmount :endAmount IS NULL OR expense.amount <= :endAmount
) )
AND (
:startDate IS NULL OR expense.datetime >= :startDate
)
AND (
:endDate IS NULL OR expense.datetime <= :endDate
)
ORDER BY expense.datetime DESC ORDER BY expense.datetime DESC
""" """
@@ -50,7 +56,9 @@ interface ExpenseDao {
categoryIds: List<Int>, categoryIds: List<Int>,
categoriesEmpty: Boolean, categoriesEmpty: Boolean,
startAmount: Double?, startAmount: Double?,
endAmount: Double? endAmount: Double?,
startDate: Long?,
endDate: Long?
): PagingSource<Int, ExpenseDto> ): PagingSource<Int, ExpenseDto>
@Transaction @Transaction
@@ -74,7 +82,12 @@ interface ExpenseDao {
AND ( AND (
:endAmount IS NULL OR expense.amount <= :endAmount :endAmount IS NULL OR expense.amount <= :endAmount
) )
AND (
:startDate IS NULL OR expense.datetime >= :startDate
)
AND (
:endDate IS NULL OR expense.datetime <= :endDate
)
ORDER BY expense.datetime DESC ORDER BY expense.datetime DESC
""" """
) )
@@ -84,7 +97,9 @@ interface ExpenseDao {
categoryIds: List<Int>, categoryIds: List<Int>,
categoriesEmpty: Boolean, categoriesEmpty: Boolean,
startAmount: Double?, startAmount: Double?,
endAmount: Double? endAmount: Double?,
startDate: Long?,
endDate: Long?
): Flow<List<ExpenseDto>> ): Flow<List<ExpenseDto>>
@Query( @Query(

View File

@@ -2,7 +2,7 @@ package cc.n0th1ng.tripmoney.data.dto
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.Icons import java.time.LocalDate
data class SummaryPerCategory( data class SummaryPerCategory(
val category: Category, val category: Category,
@@ -11,12 +11,8 @@ data class SummaryPerCategory(
val currency: Currencies val currency: Currencies
) )
data class SummaryPerCategoryRaw( data class SummaryPerDay(
val categoryId: Int, val day: LocalDate,
val categoryName: String,
val icon: Icons,
val color: String,
val amount: Double, val amount: Double,
val currency: String val percent: Float
) )

View File

@@ -19,6 +19,10 @@ data class Trip(
@ColumnInfo("currency") val currency: String, @ColumnInfo("currency") val currency: String,
@ColumnInfo("budget") val budget: Double = 0.0 @ColumnInfo("budget") val budget: Double = 0.0
) { ) {
fun isDummy(): Boolean {
return this.id == -1
}
companion object { companion object {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
val DUMMY = Trip( val DUMMY = Trip(

View File

@@ -1,7 +1,5 @@
package cc.n0th1ng.tripmoney.data.repository package cc.n0th1ng.tripmoney.data.repository
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
@@ -10,9 +8,11 @@ import cc.n0th1ng.tripmoney.Filter
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
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.screens.listexpense.toEpochMilli
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class ExpenseRepository @Inject constructor( class ExpenseRepository @Inject constructor(
@@ -25,8 +25,8 @@ class ExpenseRepository @Inject constructor(
} }
@WorkerThread @WorkerThread
suspend fun save(expense: Expense) { suspend fun save(expense: Expense): Long {
expenseDao.insert(expense) return expenseDao.insert(expense)
} }
@WorkerThread @WorkerThread
@@ -49,9 +49,10 @@ class ExpenseRepository @Inject constructor(
categoryIds = categoryIds, categoryIds = categoryIds,
categoriesEmpty = categoryIds.isEmpty(), categoriesEmpty = categoryIds.isEmpty(),
startAmount = filter.startAmount, startAmount = filter.startAmount,
endAmount = filter.endAmount endAmount = filter.endAmount,
startDate = if(filter.startDate == LocalDate.MIN) null else filter.startDate.toEpochMilli(),
endDate = if(filter.endDate == LocalDate.MAX) null else filter.endDate.plusDays(1).toEpochMilli(),
) )
} }
).flow ).flow
} }
@@ -62,18 +63,18 @@ class ExpenseRepository @Inject constructor(
filter: Filter = Filter() filter: Filter = Filter()
): Flow<List<ExpenseDto>> { ): Flow<List<ExpenseDto>> {
val categoryIds = filter.categories.map { it.id } val categoryIds = filter.categories.map { it.id }
return expenseDao.expenseDto( return expenseDao.expenseDto(
tripId = tripId, tripId = tripId,
search = search.takeIf { it.isNotBlank() }, search = search.takeIf { it.isNotBlank() },
categoryIds = categoryIds, categoryIds = categoryIds,
categoriesEmpty = categoryIds.isEmpty(), categoriesEmpty = categoryIds.isEmpty(),
startAmount = filter.startAmount, startAmount = filter.startAmount,
endAmount = filter.endAmount endAmount = filter.endAmount,
startDate = if(filter.startDate == LocalDate.MIN) null else filter.startDate.toEpochMilli(),
endDate = if(filter.endDate == LocalDate.MAX) null else filter.endDate.plusDays(1).toEpochMilli(),
) )
} }
@RequiresApi(Build.VERSION_CODES.O)
suspend fun recalculateTripExpenses(tripId: Int) { suspend fun recalculateTripExpenses(tripId: Int) {
val expenses = getExpensesDto(tripId).first() val expenses = getExpensesDto(tripId).first()
expenses.forEach { expenseDto -> expenses.forEach { expenseDto ->

View File

@@ -2,8 +2,6 @@ package cc.n0th1ng.tripmoney.navigation
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -12,11 +10,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -44,6 +38,7 @@ import cc.n0th1ng.tripmoney.Filter
import cc.n0th1ng.tripmoney.R import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
import cc.n0th1ng.tripmoney.screens.listexpense.FilterDialog
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 com.composables.icons.materialsymbols.outlined.R.drawable import com.composables.icons.materialsymbols.outlined.R.drawable
@@ -159,71 +154,9 @@ fun TopBar(
showFilter = false showFilter = false
} }
) )
} }
} }
@Composable
fun FilterDialog(
onDismiss: () -> Unit,
onSave: (Filter) -> Unit,
onClear: () -> Unit,
categories: List<Category>,
filter: Filter
) {
var filter by remember { mutableStateOf(filter) }
var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) }
var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) }
AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
Button(
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
enabled = true,
onClick = onClear
) { Text(stringResource(R.string.clear)) }
},
confirmButton = {
Button(
enabled = true,
onClick = {
onSave(
filter.withStartAmount(fromAmountString.safeToDouble())
.withEndAmount(toAmountString.safeToDouble())
)
}) { Text(stringResource(R.string.save)) }
}, title = { Text("Filter") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Categories")
FlowRow(horizontalArrangement = Arrangement.spacedBy(7.dp)) {
categories.forEach {
FilterChip(selected = filter.categories.contains(it), onClick = {
filter = if (filter.categories.contains(it)) {
filter.without(it)
} else {
filter.with(it)
}
}, label = {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Icon(painterResource(it.icon.resource), contentDescription = null)
Text(text = it.name)
}
})
}
}
AmountTextField(label = "from", onValueChange = { newText ->
fromAmountString = newText
}, value = fromAmountString)
AmountTextField(label = "to", onValueChange = { newText ->
toAmountString = newText
}, value = toAmountString)
}
})
}
@Composable @Composable
fun AmountTextField(label: String, onValueChange: (String) -> Unit, value: String) { fun AmountTextField(label: String, onValueChange: (String) -> Unit, value: String) {
var value by remember { mutableStateOf(value) } var value by remember { mutableStateOf(value) }
@@ -277,23 +210,3 @@ fun PreviewTopBar() {
) )
} }
} }
@AllPreviews
@Composable
fun PreviewFilterDialog() {
TripMoneyTheme {
FilterDialog(
onDismiss = {},
onSave = {},
categories = categoriesToPreview,
filter = Filter(),
onClear = {}
)
}
}
private fun String.safeToDouble(): Double {
if (this == "") return Double.MAX_VALUE
if (this.isEmpty()) return 0.0
return this.toDouble()
}

View File

@@ -98,7 +98,8 @@ fun AddExpenseBottomSheet(
expenseDtoToEdit = expenseDtoToEdit, expenseDtoToEdit = expenseDtoToEdit,
state = state, state = state,
currentTrip = currentTrip ?: Trip.DUMMY, currentTrip = currentTrip ?: Trip.DUMMY,
categories = categories categories = categories,
onSaveCategory = {expenseAndCategoryViewModel.save(it)}
) )
} }
@@ -111,14 +112,11 @@ fun AddExpenseBottomSheet(
expenseDtoToEdit: ExpenseDto?, expenseDtoToEdit: ExpenseDto?,
state: SheetState, state: SheetState,
currentTrip: Trip, currentTrip: Trip,
categories: List<Category> categories: List<Category>,
onSaveCategory: (Category) -> Unit
) { ) {
val currentTripId = currentTrip.id val currentTripId = currentTrip.id
if (categories.isEmpty()) {
return
}
var amount by remember { var amount by remember {
mutableStateOf( mutableStateOf(
"%.2f".format(expenseDtoToEdit?.expense?.amount ?: 0.00) "%.2f".format(expenseDtoToEdit?.expense?.amount ?: 0.00)
@@ -138,7 +136,11 @@ fun AddExpenseBottomSheet(
expenseDtoToEdit?.expense?.currency ?: currentTrip.currency expenseDtoToEdit?.expense?.currency ?: currentTrip.currency
) )
} }
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) } var category by remember {
mutableStateOf(
expenseDtoToEdit?.category ?: if (categories.isEmpty()) null else categories[0]
)
}
var datetime by remember { var datetime by remember {
mutableStateOf( mutableStateOf(
expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now() expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now()
@@ -273,14 +275,14 @@ fun AddExpenseBottomSheet(
SaveButton( SaveButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = enableSave, enabled = enableSave && category != null,
onClick = { onClick = {
val expenseToSave = Expense( val expenseToSave = Expense(
amount = equationResult, amount = equationResult,
currency = currency, currency = currency,
note = note, note = note,
datetime = datetime, datetime = datetime,
categoryId = category.id, categoryId = category!!.id,
tripId = currentTripId tripId = currentTripId
) )
onSave( onSave(
@@ -319,7 +321,8 @@ fun AddExpenseBottomSheet(
category = selectedCategory category = selectedCategory
}, },
selected = category, selected = category,
categories = categories categories = categories,
onSaveCategory = onSaveCategory
) )
} }
} }
@@ -410,7 +413,7 @@ fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: Str
} }
@Composable @Composable
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) { fun CategoryButton(onClick: () -> Unit, category: Category?, modifier: Modifier = Modifier) {
Button( Button(
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
onClick = onClick, onClick = onClick,
@@ -422,25 +425,21 @@ fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier =
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
) )
) { ) {
// Row(modifier = modifier.fillMaxWidth()) { if (category != null) {
Icon( Icon(
tint = Color(category.color.toColorInt()), tint = Color(category.color.toColorInt()),
modifier = Modifier modifier = Modifier
.size(30.dp) .size(30.dp)
// .background(
// color = MaterialTheme.colorScheme.prima,
// shape = MaterialTheme.shapes.small
// )
.padding(end = 10.dp), .padding(end = 10.dp),
painter = painterResource(category.icon.resource), painter = painterResource(category.icon.resource),
contentDescription = stringResource(R.string.category), contentDescription = stringResource(R.string.category),
) )
}
Text( Text(
text = category.name, text = category?.name ?: stringResource(R.string.pick_category),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
// }
} }
} }
@@ -484,12 +483,20 @@ fun NumberKeyboard(
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f),
containerColor = Color.Transparent, containerColor = Color.Transparent,
onLongClick = onLongBackspaceClick onLongClick = onLongBackspaceClick,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) )
"+", "÷", "", "×" -> KeyboardButton( "+", "÷", "", "×" -> KeyboardButton(
text = key, text = key,
onClick = { onOperatorClick(key) }, onClick = {
when (key) {
"+" -> onOperatorClick("+")
"÷" -> onOperatorClick("/")
"" -> onOperatorClick("-")
"×" -> onOperatorClick("*")
}
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
containerColor = MaterialTheme.colorScheme.tertiaryContainer, containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer contentColor = MaterialTheme.colorScheme.onTertiaryContainer
@@ -500,7 +507,7 @@ fun NumberKeyboard(
onClick = { onNumberClick(key) }, onClick = { onNumberClick(key) },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSecondary contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) )
} }
} }
@@ -532,7 +539,8 @@ fun KeyboardButton(
when { when {
text != null -> Text( text != null -> Text(
text = text, text = text,
style = MaterialTheme.typography.headlineMedium style = MaterialTheme.typography.headlineMedium,
color = contentColor
) )
icon != null -> Icon(painter = icon, contentDescription = null) icon != null -> Icon(painter = icon, contentDescription = null)
@@ -573,7 +581,8 @@ fun PreviewAddExpenseDisabled() {
LocalDate.parse("2020-01-15"), LocalDate.parse("2020-01-15"),
Currencies.entries.random().name Currencies.entries.random().name
), ),
categories = categoriesToPreview categories = categoriesToPreview,
{}
) )
} }
@@ -617,7 +626,8 @@ fun PreviewAddExpenseEnabled() {
LocalDate.parse("2020-01-11"), LocalDate.parse("2020-01-11"),
Currencies.entries.random().name Currencies.entries.random().name
), ),
categories = categoriesToPreview categories = categoriesToPreview,
{}
) )
} }

View File

@@ -13,46 +13,69 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
import androidx.compose.runtime.setValue 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.focus.FocusRequester
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 cc.n0th1ng.tripmoney.R.string
import cc.n0th1ng.tripmoney.R.*
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.SearchTextOutlined
import com.composables.icons.materialsymbols.outlined.R import com.composables.icons.materialsymbols.outlined.R
@Composable @Composable
fun CategorySelectionDialog( fun CategorySelectionDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onCategorySelected: (Category) -> Unit, onCategorySelected: (Category) -> Unit,
selected: Category, selected: Category?,
categories: List<Category>, categories: List<Category>,
onSaveCategory: (Category) -> Unit
) { ) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val listState = rememberLazyListState() val listState = rememberLazyListState()
var showAddCategoryDialog by remember { mutableStateOf(false) } var showAddCategoryDialog by remember { mutableStateOf(false) }
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, title = { Text(stringResource(string.pick_category)) }, text = { onDismissRequest = onDismiss,
title = { Text(stringResource(string.pick_category)) },
text = {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
if(selected != null) {
listState.animateScrollToItem(categories.indexOfFirst { it == selected })
}
// focusRequester.requestFocus()
}
Column { Column {
var search by remember { mutableStateOf("") }
val filteredCategories = if (search.isBlank()) {
categories
} else {
categories.filter { category ->
category.name.lowercase().contains(search.lowercase())
}
}
LazyColumn( LazyColumn(
modifier = Modifier.heightIn(max = 300.dp), modifier = Modifier.heightIn(max = 300.dp),
state = listState, state = listState,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
items( items(
count = categories.size, count = filteredCategories.size,
key = { index -> categories[index].id }) { index -> key = { index -> filteredCategories[index].id }) { index ->
val category = categories[index] val category = filteredCategories[index]
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -83,24 +106,41 @@ fun CategorySelectionDialog(
.clickable { .clickable {
showAddCategoryDialog = true showAddCategoryDialog = true
} }
.padding(top = 15.dp), .padding(bottom = 10.dp),
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
painter = painterResource(R.drawable.materialsymbols_ic_add_outlined), painter = painterResource(R.drawable.materialsymbols_ic_add_outlined),
contentDescription = stringResource(string.category) contentDescription = stringResource(string.category)
) )
Text( Text(
text = stringResource(string.add_new), modifier = Modifier.padding(start = 8.dp), text = stringResource(string.add_new),
modifier = Modifier.padding(start = 8.dp),
) )
} }
SearchTextOutlined(
text = search,
onTextChange = { newText -> search = newText },
focusRequester = focusRequester
)
} }
}, confirmButton = {}) },
confirmButton = {})
if (showAddCategoryDialog) { if (showAddCategoryDialog) {
AddCategoryDialog(onDismiss = { AddCategoryDialog(onDismiss = {
showAddCategoryDialog = false showAddCategoryDialog = false
}, onSave = { category -> }, onSave = { category ->
expenseAndCategoryViewModel.save(category) onSaveCategory(category)
showAddCategoryDialog = false showAddCategoryDialog = false
}) })
} }
} }
@AllPreviews
@Composable
fun PreviewCategorySelectionDialog() {
TripMoneyTheme {
CategorySelectionDialog(
{}, {},
categoriesToPreview.random(), categoriesToPreview, {})
}
}

View File

@@ -1,20 +1,35 @@
package cc.n0th1ng.tripmoney.screens.listexpense package cc.n0th1ng.tripmoney.screens.listexpense
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cc.n0th1ng.tripmoney.R import cc.n0th1ng.tripmoney.R
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.SearchTextOutlined
@Composable @Composable
fun CurrencySelectionDialog( fun CurrencySelectionDialog(
@@ -23,29 +38,83 @@ fun CurrencySelectionDialog(
selected: String selected: String
) { ) {
AlertDialog( AlertDialog(
modifier = Modifier.sizeIn(maxHeight = 500.dp),
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.pick_currency)) }, title = { Text(stringResource(R.string.pick_currency)) },
text = { text = {
Column { val scrollState = rememberLazyListState()
Currencies.names().forEach { currency -> val currencies = Currencies.names()
var search by remember { mutableStateOf("") }
LaunchedEffect(selected) {
val index = currencies.indexOf(selected)
if (index != -1) {
scrollState.animateScrollToItem(index)
}
}
Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
val filteredCurrencies = if (search.isBlank()) {
currencies
} else {
currencies.filter { currency ->
currency.lowercase().contains(search.lowercase())
}
}
LazyColumn(
state = scrollState,
modifier = Modifier
.weight(1f)
.padding(bottom = 10.dp)
) {
items(
count = filteredCurrencies.size,
key = { index -> filteredCurrencies[index] }
) { index ->
val currency = filteredCurrencies[index]
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
onCurrencySelected(currency) onCurrencySelected(currency)
} },
.padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically) { ) {
RadioButton( RadioButton(
selected = selected == currency, onClick = { selected = selected == currency,
onCurrencySelected(currency) onClick = { onCurrencySelected(currency) }
}) )
Text( Text(
text = currency, modifier = Modifier.padding(start = 8.dp) text = currency,
modifier = Modifier.padding(start = 8.dp)
) )
} }
} }
} }
}, SearchTextOutlined(
confirmButton = {}) text = search, onTextChange = { newText -> search = newText })
}
},
confirmButton = {},
dismissButton = {
Button(
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
enabled = true,
onClick = onDismiss,
) { Text(stringResource(R.string.cancel)) }
}
)
}
@AllPreviews
@Composable
fun PreviewCurrencySelectionDialog() {
TripMoneyTheme {
CurrencySelectionDialog(
{},
{},
Currencies.names().random()
)
}
} }

View File

@@ -1,7 +1,5 @@
package cc.n0th1ng.tripmoney.screens.listexpense package cc.n0th1ng.tripmoney.screens.listexpense
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePicker
@@ -21,8 +19,7 @@ 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.tooling.preview.Preview import cc.n0th1ng.tripmoney.R.string
import cc.n0th1ng.tripmoney.R.*
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 java.time.Instant import java.time.Instant
@@ -31,7 +28,6 @@ import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.ZoneId import java.time.ZoneId
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DateRangePicker( fun DateRangePicker(
@@ -75,7 +71,6 @@ fun DateRangePicker(
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DatePicker( fun DatePicker(
@@ -117,7 +112,6 @@ fun DatePicker(
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TimePicker( fun TimePicker(
@@ -147,7 +141,6 @@ fun TimePicker(
} }
@Composable @Composable
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
fun DateTimePicker( fun DateTimePicker(
dateTime: LocalDateTime = LocalDateTime.now(), dateTime: LocalDateTime = LocalDateTime.now(),
@@ -181,15 +174,12 @@ fun DateTimePicker(
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
fun LocalDateTime.toEpochMilli(): Long = fun LocalDateTime.toEpochMilli(): Long =
this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
@RequiresApi(Build.VERSION_CODES.O)
fun LocalDate.toEpochMilli(): Long = fun LocalDate.toEpochMilli(): Long =
this.atStartOfDay().atZone(ZoneId.of("UTC")).toInstant().toEpochMilli() this.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews @AllPreviews
@Composable @Composable
fun DatePickerPreview() { fun DatePickerPreview() {

View File

@@ -0,0 +1,155 @@
package cc.n0th1ng.tripmoney.screens.listexpense
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cc.n0th1ng.tripmoney.Filter
import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.navigation.AmountTextField
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.pretty
import java.time.LocalDate
@Composable
fun FilterDialog(
onDismiss: () -> Unit,
onSave: (Filter) -> Unit,
onClear: () -> Unit,
categories: List<Category>,
filter: Filter
) {
var filter by remember { mutableStateOf(filter) }
var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) }
var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) }
AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
Button(
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
enabled = true,
onClick = onClear
) { Text(stringResource(R.string.clear)) }
},
confirmButton = {
Button(
enabled = true,
onClick = {
onSave(filter)
}) { Text(stringResource(R.string.save)) }
}, title = { Text("Filter") },
text = {
var showDatePicker by remember { mutableStateOf(false) }
var startDate by remember {
mutableStateOf(filter.startDate)
}
var endDate by remember {
mutableStateOf(filter.endDate)
}
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Categories")
FlowRow(
horizontalArrangement = Arrangement.spacedBy(7.dp),
modifier = Modifier
.sizeIn(maxHeight = 200.dp)
.verticalScroll(rememberScrollState())
) {
categories.forEach {
FilterChip(selected = filter.categories.contains(it), onClick = {
filter = if (filter.categories.contains(it)) {
filter.without(it)
} else {
filter.with(it)
}
}, label = {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Icon(painterResource(it.icon.resource), contentDescription = null)
Text(text = it.name)
}
})
}
}
Button(
modifier = Modifier
.fillMaxWidth(1f),
shape = MaterialTheme.shapes.medium,
onClick = { showDatePicker = true }) {
val startDateFormatted = startDate.pretty()
val endDateFormatted = endDate.pretty()
Text(
text =
if(startDate == LocalDate.MIN && endDate == LocalDate.MAX) "Show all dates" else
"$startDateFormatted - $endDateFormatted",
fontSize = 17.sp
)
}
AmountTextField(label = "from", onValueChange = { newText ->
fromAmountString = newText
filter = filter.withStartAmount(newText.safeToDouble())
}, value = fromAmountString)
AmountTextField(label = "to", onValueChange = { newText ->
toAmountString = newText
filter = filter.withEndAmount(newText.safeToDouble())
}, value = toAmountString)
}
if (showDatePicker) {
DateRangePicker(
startDate = if(startDate == LocalDate.MIN) LocalDate.now() else startDate,
endDate = if(endDate == LocalDate.MAX) LocalDate.now() else endDate,
onDismiss = { showDatePicker = false },
onConfirm = { newStartDate, newEndDate ->
startDate = newStartDate
endDate = newEndDate
filter = filter.withStartDate(startDate).withEndDate(endDate)
showDatePicker = false
})
}
})
}
@AllPreviews
@Composable
fun PreviewFilterDialog() {
TripMoneyTheme {
FilterDialog(
onDismiss = {},
onSave = {},
categories = categoriesToPreview.plus(categoriesToPreview).plus(categoriesToPreview),
filter = Filter(),
onClear = {}
)
}
}
private fun String.safeToDouble(): Double {
if (this == "") return Double.MAX_VALUE
if (this.isEmpty()) return 0.0
return this.toDouble()
}

View File

@@ -1,10 +1,7 @@
package cc.n0th1ng.tripmoney.screens.listexpense package cc.n0th1ng.tripmoney.screens.listexpense
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -22,6 +19,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -40,6 +39,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.mutableIntStateOf
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
@@ -47,11 +47,12 @@ 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.platform.testTag
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.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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
@@ -82,13 +83,13 @@ import java.time.format.DateTimeFormatter
import kotlin.random.Random import kotlin.random.Random
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun ListExpenseScreen( fun ListExpenseScreen(
filter: Filter, filter: Filter,
search: String, search: String,
initialAutoOpen: Boolean, initialAutoOpen: Boolean,
onAutoOpenConsumed: () -> Unit onAutoOpenConsumed: () -> Unit,
dateToScroll: String
) { ) {
val settingsViewModel: SettingsViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel()
val tripViewModel: TripViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel()
@@ -98,27 +99,38 @@ fun ListExpenseScreen(
val expensesFlow = val expensesFlow =
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, search, filter) expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, search, filter)
val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState() val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState()
var idToScroll by remember { mutableIntStateOf(-1) }
ListExpenseScreen( ListExpenseScreen(
currentTrip = currentTrip,
expensesFlow = expensesFlow, expensesFlow = expensesFlow,
onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) }, onSaveExpense = {
expenseAndCategoryViewModel.save(
it,
currentTrip!!,
onComplete = { id -> idToScroll = id })
},
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) }, onDeleteExpense = { expenseAndCategoryViewModel.delete(it) },
isRecalculatingRate = isRecalculatingRate, isRecalculatingRate = isRecalculatingRate,
initialAutoOpen = initialAutoOpen, initialAutoOpen = initialAutoOpen,
onAutoOpenConsumed = onAutoOpenConsumed onAutoOpenConsumed = onAutoOpenConsumed,
idToScroll = idToScroll,
dateToScroll = dateToScroll
) )
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun ListExpenseScreen( fun ListExpenseScreen(
currentTrip: Trip?,
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, initialAutoOpen: Boolean,
onAutoOpenConsumed: () -> Unit onAutoOpenConsumed: () -> Unit,
idToScroll: Int,
dateToScroll: String
) { ) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
@@ -137,16 +149,63 @@ fun ListExpenseScreen(
var itemToDelete by remember { mutableStateOf<Expense?>(null) } var itemToDelete by remember { mutableStateOf<Expense?>(null) }
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = { Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
if (currentTrip != null && !currentTrip.isDummy()) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { showBottomSheet = true }, onClick = { showBottomSheet = true },
icon = { Icon(Icons.Filled.Add, stringResource(string.add_expense)) }, icon = { Icon(Icons.Filled.Add, stringResource(string.add_expense)) },
text = { Text(text = stringResource(string.add_expense)) }, text = { Text(text = stringResource(string.add_expense)) },
) )
}
}) })
{ {
Box { Box {
if (items.itemCount == 0) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
contentAlignment = Alignment.Center
) {
val textToShow = if (currentTrip == null || currentTrip.isDummy()) {
stringResource(string.no_trip_picked)
} else {
stringResource(string.no_expenses)
}
Text(
text = textToShow,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
} else {
LaunchedEffect(Unit) {
if (dateToScroll == "") return@LaunchedEffect
for (index in 0 until items.itemCount) {
val item = items.peek(index)
if (item is ExpenseListItemUi.Header && item.date.toString() == dateToScroll) {
listState.animateScrollToItem(index)
break
}
}
}
LaunchedEffect(idToScroll) {
if (idToScroll == -1) return@LaunchedEffect
for (index in 0 until items.itemCount) {
val item = items.peek(index)
if (item is ExpenseListItemUi.Item && item.expenseDto.expense.id == idToScroll) {
listState.animateScrollToItem(index)
break
}
}
}
}
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize().semantics { modifier = Modifier
.fillMaxSize()
.semantics {
contentDescription = "expensesList" contentDescription = "expensesList"
}, },
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -161,9 +220,7 @@ fun ListExpenseScreen(
} }
} }
) { index -> ) { index ->
when (val item = items[index]) { when (val item = items[index]) {
is ExpenseListItemUi.Header -> { is ExpenseListItemUi.Header -> {
CustomDivider( CustomDivider(
date = item.date, date = item.date,
@@ -193,6 +250,8 @@ fun ListExpenseScreen(
} }
} }
}
if (itemToDelete != null) { if (itemToDelete != null) {
DeleteConfirmationDialog( DeleteConfirmationDialog(
onConfirm = { onConfirm = {
@@ -221,10 +280,8 @@ fun ListExpenseScreen(
) )
} }
} }
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun CustomDivider(date: LocalDate, sum: Double, currency: String) { fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
Row( Row(
@@ -267,7 +324,6 @@ fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun SwipeToDeleteExpenseCard( fun SwipeToDeleteExpenseCard(
expenseDto: ExpenseDto, expenseDto: ExpenseDto,
@@ -339,26 +395,27 @@ fun DeleteConfirmationDialog(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 24.dp) .padding(top = 24.dp)
) { ) {
Text( Button(
text = stringResource(string.cancel), modifier = Modifier.padding(end = 20.dp),
style = MaterialTheme.typography.titleMedium, onClick = onCancel
color = MaterialTheme.colorScheme.onSecondaryContainer, ) {
modifier = Modifier Text(stringResource(string.cancel))
.padding(end = 24.dp) }
.clickable { onCancel() }
) Button(
Text( onClick = onConfirm,
text = stringResource(string.delete), colors = ButtonDefaults.buttonColors(
color = MaterialTheme.colorScheme.error, containerColor = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.titleMedium, contentColor = MaterialTheme.colorScheme.onError
modifier = Modifier.clickable { onConfirm() }
) )
) {
Text(stringResource(string.delete))
}
} }
} }
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun ExpenseCard( fun ExpenseCard(
expenseDto: ExpenseDto, expenseDto: ExpenseDto,
@@ -459,19 +516,73 @@ fun ExpenseCard(
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews @AllPreviews
@Composable @Composable
fun PreviewListExpenseScreen() { fun PreviewListExpenseScreen() {
TripMoneyTheme() { TripMoneyTheme() {
val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList()) val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList())
ListExpenseScreen( ListExpenseScreen(
currentTrip = Trip(
id = 1,
name = "Vacation",
currency = "USD",
startDate = LocalDate.parse("2026-01-01"),
endDate = LocalDate.parse("2026-01-11"),
),
expensesFlow = MutableStateFlow(pagingData), expensesFlow = MutableStateFlow(pagingData),
onSaveExpense = {}, onSaveExpense = {},
onDeleteExpense = {}, onDeleteExpense = {},
isRecalculatingRate = true, isRecalculatingRate = true,
false, false,
{} {},
0,
""
)
}
}
@AllPreviews
@Composable
fun PreviewListExpenseScreenWithoutExpenses() {
TripMoneyTheme() {
val pagingData = PagingData.from(emptyList<ExpenseListItemUi>())
ListExpenseScreen(
currentTrip = Trip(
id = 1,
name = "Vacation",
currency = "USD",
startDate = LocalDate.parse("2026-01-01"),
endDate = LocalDate.parse("2026-01-11"),
),
expensesFlow = MutableStateFlow(pagingData),
onSaveExpense = {},
onDeleteExpense = {},
isRecalculatingRate = true,
false,
{},
0,
""
)
}
}
@AllPreviews
@Composable
fun PreviewListExpenseScreenWithoutTrip() {
TripMoneyTheme() {
val pagingData = PagingData.from(emptyList<ExpenseListItemUi>())
ListExpenseScreen(
currentTrip = null,
expensesFlow = MutableStateFlow(pagingData),
onSaveExpense = {},
onDeleteExpense = {},
isRecalculatingRate = true,
false,
{},
0,
""
) )
} }
@@ -480,7 +591,7 @@ fun PreviewListExpenseScreen() {
@AllPreviews @AllPreviews
@Composable @Composable
fun PreviewDeleteConfirmationDialog() { fun PreviewDeleteConfirmationDialog() {
TripMoneyTheme() { TripMoneyTheme {
DeleteConfirmationDialog( DeleteConfirmationDialog(
onConfirm = {}, onConfirm = {},
onCancel = {}) onCancel = {})
@@ -488,7 +599,6 @@ fun PreviewDeleteConfirmationDialog() {
} }
@RequiresApi(Build.VERSION_CODES.O)
private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseListItemUi> { private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseListItemUi> {
val sampleCategories = listOf( val sampleCategories = listOf(
Category( Category(

View File

@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -20,7 +19,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -48,7 +46,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
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.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
@@ -63,9 +60,6 @@ import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.colors import cc.n0th1ng.tripmoney.utils.colors
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import com.composables.icons.materialsymbols.outlined.R import com.composables.icons.materialsymbols.outlined.R
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import kotlin.collections.emptyList
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
@@ -159,7 +153,7 @@ fun ManageCategoriesScreen(
if (itemToDelete != null) { if (itemToDelete != null) {
DeleteConfirmationDialog( DeleteConfirmationDialog(
bodyText = stringResource(string.delete_category_info), bodyText = stringResource(string.delete_category_info).format(itemToDelete?.name),
onConfirm = { onConfirm = {
onDeleteCategory(itemToDelete!!) onDeleteCategory(itemToDelete!!)
itemToDelete = null itemToDelete = null
@@ -236,7 +230,7 @@ fun SwipeToDeleteExpenseCard(
Modifier Modifier
.clip(CardDefaults.elevatedShape) .clip(CardDefaults.elevatedShape)
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.onError) .background(MaterialTheme.colorScheme.errorContainer)
.padding(horizontal = 20.dp), .padding(horizontal = 20.dp),
contentAlignment = Alignment.CenterEnd contentAlignment = Alignment.CenterEnd
) { ) {

View File

@@ -4,15 +4,20 @@ import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
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
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
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.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -32,12 +37,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource 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.text.font.FontWeight
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.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
import cc.n0th1ng.tripmoney.data.dto.SummaryPerDay
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
import cc.n0th1ng.tripmoney.navigation.Screens
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
@@ -47,10 +58,12 @@ 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 com.composables.icons.materialsymbols.outlined.R import com.composables.icons.materialsymbols.outlined.R
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun StatisticsScreen() { fun StatisticsScreen(navController: NavController) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel()
val tripViewModel: TripViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel()
@@ -58,24 +71,31 @@ fun StatisticsScreen() {
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY) val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTripId) val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTripId)
.collectAsState(emptyList()) .collectAsState(emptyList())
val summaryPerDayList by expenseAndCategoryViewModel.getSummaryPerDay(currentTripId)
.collectAsState(emptyList())
val summaryAmount by expenseAndCategoryViewModel.getSummaryAmount(currentTripId) val summaryAmount by expenseAndCategoryViewModel.getSummaryAmount(currentTripId)
.collectAsState(0.0) .collectAsState(0.0)
val moneyLeft by expenseAndCategoryViewModel.getBudgetLeft(currentTripId).collectAsState(null) val moneyLeft by expenseAndCategoryViewModel.getBudgetLeft(currentTripId).collectAsState(null)
StatisticsScreen( StatisticsScreen(
summaryPerCategoryList, summaryPerCategoryList,
summaryPerDayList,
summaryAmount, summaryAmount,
Currencies.valueOf(currentTrip?.currency ?: Currencies.default().name), Currencies.valueOf(currentTrip?.currency ?: Currencies.default().name),
moneyLeft moneyLeft,
onDayClicked = {
date -> navController.navigate(Screens.LIST_EXPENSE + "?dateToScroll=$date")
}
) )
} }
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun StatisticsScreen( fun StatisticsScreen(
summaryPerCategoryList: List<SummaryPerCategory>, summaryPerCategoryList: List<SummaryPerCategory>,
summaryPerDayList: List<SummaryPerDay>,
summaryAmount: Double, summaryAmount: Double,
tripCurrency: Currencies, tripCurrency: Currencies,
moneyLeft: Double? moneyLeft: Double?,
onDayClicked: (String) -> Unit
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -85,7 +105,9 @@ fun StatisticsScreen(
) { ) {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Summary( Summary(
Modifier.weight(1f), -1 * summaryAmount, tripCurrency.name, Modifier.weight(1f),
if (summaryAmount == 0.0) 0.0 else -1 * summaryAmount,
tripCurrency.name,
stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses), stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses),
R.drawable.materialsymbols_ic_payment_arrow_down_outlined, R.drawable.materialsymbols_ic_payment_arrow_down_outlined,
iconColor = MaterialTheme.colorScheme.error iconColor = MaterialTheme.colorScheme.error
@@ -97,8 +119,11 @@ fun StatisticsScreen(
iconColor = colorResource(cc.n0th1ng.tripmoney.R.color.good_green) iconColor = colorResource(cc.n0th1ng.tripmoney.R.color.good_green)
) )
} }
SummaryPerCategoryCard(summaryPerCategoryList) SummaryPerCategoryCard(
modifier = Modifier.heightIn(max = 300.dp),
summaryPerCategoryList = summaryPerCategoryList
)
SummaryPerDayCard(modifier = Modifier.height(300.dp), summaryPerDayList = summaryPerDayList.sortedBy { it.day }, onDayClicked = onDayClicked)
} }
} }
@@ -153,12 +178,31 @@ fun Summary(
} }
@Composable @Composable
fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) { fun SummaryPerCategoryCard(
summaryPerCategoryList: List<SummaryPerCategory>,
modifier: Modifier = Modifier
) {
ElevatedCard( ElevatedCard(
modifier = Modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
colors = CardDefaults.elevatedCardColors() colors = CardDefaults.elevatedCardColors()
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer) .copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
) { ) {
if (summaryPerCategoryList.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(cc.n0th1ng.tripmoney.R.string.no_expenses_summary),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
} else {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(15.dp) .padding(15.dp)
@@ -175,6 +219,49 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
} }
} }
} }
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun SummaryPerDayCard(modifier: Modifier = Modifier, summaryPerDayList: List<SummaryPerDay>, onDayClicked: (String) -> Unit) {
ElevatedCard(
modifier = modifier
.fillMaxWidth(),
colors = CardDefaults.elevatedCardColors()
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
) {
if (summaryPerDayList.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(cc.n0th1ng.tripmoney.R.string.no_expenses_summary),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
} else {
Row(
modifier = Modifier
.padding(15.dp)
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
summaryPerDayList.forEach { it ->
DayCard(
summaryPerDay = it,
onDayClicked = {date -> onDayClicked(date)}
)
}
}
}
}
}
@Composable @Composable
fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) { fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) {
@@ -234,23 +321,107 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa
} }
} }
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable @Composable
fun Preview() { fun DayCard(modifier: Modifier = Modifier, summaryPerDay: SummaryPerDay, onDayClicked: (String) -> Unit) {
TripMoneyTheme { Column(
Scaffold { modifier = modifier.fillMaxHeight(), verticalArrangement = Arrangement.Bottom,
StatisticsScreen( horizontalAlignment = Alignment.CenterHorizontally
summaryPerCategoryList, ) {
summaryAmount = 125.24,
Currencies.entries.random(), Text(
432.14 text = "%.2f".format(summaryPerDay.amount),
style = MaterialTheme.typography.labelSmall,
fontSize = (MaterialTheme.typography.labelSmall.fontSize.value - 2).sp,
)
val width = 45.dp
Box(
modifier = Modifier
.width(width)
.fillMaxHeight(0.2f + (0.98f - 0.2f) * summaryPerDay.percent)
.clip(RoundedCornerShape(width / 2))
.background(MaterialTheme.colorScheme.primary)
.clickable(onClick = {onDayClicked(summaryPerDay.day.toString())})
.padding(top = 5.dp)
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.size(width - 10.dp)
.background(
MaterialTheme.colorScheme.tertiaryContainer,
shape = RoundedCornerShape(width / 2)
)
.padding(vertical = 3.dp),
) {
Text(
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center,
lineHeight = 10.sp,
color = MaterialTheme.colorScheme.onTertiaryContainer,
text = summaryPerDay.day.format(DateTimeFormatter.ofPattern("dd"))
)
Text(
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Light,
fontSize = (MaterialTheme.typography.labelSmall.fontSize.value - 2).sp,
lineHeight = 10.sp,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onTertiaryContainer,
text = summaryPerDay.day.format(DateTimeFormatter.ofPattern("E"))
) )
} }
} }
} }
}
}
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun PreviewStatisticScreen() {
TripMoneyTheme {
Scaffold {
StatisticsScreen(
summaryPerCategoryList,
summaryPerDayList,
summaryAmount = 125.24,
Currencies.entries.random(),
432.14,
{}
)
}
}
}
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun PreviewStatisticScreenWithNoData() {
TripMoneyTheme {
Scaffold {
StatisticsScreen(
emptyList(),
emptyList(),
summaryAmount = 0.0,
Currencies.entries.random(),
null,
{}
)
}
}
}
val categories = listOf( val categories = listOf(
Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()), Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()),
Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()), Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()),
@@ -269,3 +440,25 @@ val summaryPerCategoryList = listOf(
SummaryPerCategory(categories[3], 50.0, 0.1f, Currencies.PLN), SummaryPerCategory(categories[3], 50.0, 0.1f, Currencies.PLN),
SummaryPerCategory(categories[5], 50.0, 0.0001f, Currencies.PLN), SummaryPerCategory(categories[5], 50.0, 0.0001f, Currencies.PLN),
) )
@RequiresApi(Build.VERSION_CODES.O)
val summaryPerDayListRaw = listOf(
SummaryPerDay(LocalDate.now(), 50.0, 0f),
SummaryPerDay(LocalDate.now().minusDays(1), 500.23, 0f),
SummaryPerDay(LocalDate.now().minusDays(2), 1560.53, 0f),
SummaryPerDay(LocalDate.now().minusDays(3), 700.32, 0f),
SummaryPerDay(LocalDate.now().minusDays(4), 201.3, 0f),
SummaryPerDay(LocalDate.now().minusDays(5), 2020.64, 0f),
SummaryPerDay(LocalDate.now().minusDays(6), 510.43, 0f),
SummaryPerDay(LocalDate.now().minusDays(7), 3050.12, 0f),
SummaryPerDay(LocalDate.now().minusDays(8), 264.32, 0f),
SummaryPerDay(LocalDate.now().minusDays(9), 3596.64, 0f)
)
@RequiresApi(Build.VERSION_CODES.O)
val highestAmount = summaryPerDayListRaw.maxOf { it.amount }
@RequiresApi(Build.VERSION_CODES.O)
val summaryPerDayList = summaryPerDayListRaw.map {
it.copy(percent = ((it.amount / highestAmount)).toFloat())
}.sortedBy { it.day.toEpochDay() }

View File

@@ -38,14 +38,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
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.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.paging.PagingData import androidx.paging.PagingData
@@ -113,6 +112,17 @@ fun TripPickerScreen(
Icon(Icons.Filled.Add, stringResource(string.add_trip)) Icon(Icons.Filled.Add, stringResource(string.add_trip))
} }
}) { paddingValues -> }) { paddingValues ->
if (trips.itemCount == 0) {
Box(modifier = Modifier.fillMaxSize().padding(10.dp), contentAlignment = Alignment.Center) {
Text(
text = stringResource(string.no_trip_added),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
} else {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.padding(horizontal = 15.dp) .padding(horizontal = 15.dp)
@@ -138,6 +148,8 @@ fun TripPickerScreen(
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
} }
} }
}
if (showBottomSheet) { if (showBottomSheet) {
AddTripBottomSheet( AddTripBottomSheet(
@@ -250,7 +262,8 @@ fun TripCard(
} }
Column( Column(
modifier = Modifier.padding(end = 20.dp), modifier = Modifier.padding(end = 20.dp),
horizontalAlignment = Alignment.End) { horizontalAlignment = Alignment.End
) {
Text( Text(
trip.currency.uppercase(), trip.currency.uppercase(),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@@ -308,3 +321,18 @@ fun PreviewTripPickerScreen() {
) )
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun PreviewTripPickerScreenNoTrip() {
TripMoneyTheme {
TripPickerScreen(
tripsFlow = MutableStateFlow(PagingData.from(emptyList())),
currentTripId = 1,
onDelete = {},
onClick = {},
onSave = {}
)
}
}

View File

@@ -1,15 +1,15 @@
package cc.n0th1ng.tripmoney.utils package cc.n0th1ng.tripmoney.utils
val colors: List<String> = listOf( val colors: List<String> = listOf(
"#af1b3f",
"#083D77", "#083D77",
"#5998c5", "#0B7189",
"#f7934c",
"#ec0b43",
"#87A330",
"#6F8AB7",
"#F26CA7",
"#5E4AE3",
"#2A7F62", "#2A7F62",
"#0B7189" "#5998c5",
"#5E4AE3",
"#6F8AB7",
"#87A330",
"#F26CA7",
"#af1b3f",
"#ec0b43",
"#f7934c"
) )

View File

@@ -1,10 +1,169 @@
package cc.n0th1ng.tripmoney.utils package cc.n0th1ng.tripmoney.utils
enum class Currencies { enum class Currencies {
AED,
AFN,
ALL,
AMD,
ANG,
AOA,
ARS,
AUD,
AWG,
AZN,
BAM,
BBD,
BDT,
BHD,
BIF,
BMD,
BND,
BOB,
BRL,
BSD,
BTN,
BWP,
BYN,
BZD,
CAD,
CDF,
CHF,
CLP,
CNH,
CNY,
COP,
CRC,
CUP,
CVE,
CZK,
DJF,
DKK,
DOP,
DZD,
EGP,
ERN,
ETB,
FJD,
FKP,
GBP,
GEL,
GGP,
GHS,
GIP,
GMD,
GNF,
GTQ,
GYD,
HKD,
HNL,
HTG,
HUF,
IDR,
ILS,
IMP,
INR,
IQD,
IRR,
ISK,
JEP,
JMD,
JOD,
JPY,
KES,
KGS,
KHR,
KMF,
KRW,
KWD,
KYD,
KZT,
LAK,
LBP,
LKR,
LRD,
LSL,
LYD,
MAD,
MDL,
MGA,
MKD,
MMK,
MNT,
MOP,
MRO,
MRU,
MUR,
MVR,
MWK,
MXN,
MYR,
MZN,
NAD,
NGN,
NIO,
NOK,
NPR,
NZD,
OMR,
PAB,
PEN,
PGK,
PHP,
PKR,
PLN, PLN,
EUR, PYG,
QAR,
RON,
RSD,
RUB,
RWF,
SAR,
SBD,
SCR,
SDG,
SEK,
SGD,
SHP,
SLE,
SOS,
SRD,
SSP,
STN,
SVC,
SYP,
SZL,
THB,
TJS,
TMT,
TND,
TOP,
TRY,
TTD,
TWD,
TZS,
UAH,
UGX,
USD, USD,
RON; UYU,
UZS,
VES,
VND,
VUV,
WST,
XAF,
XAG,
XAU,
XCD,
XCG,
XDR,
XOF,
XPD,
XPF,
XPT,
YER,
ZAR,
ZMW,
ZWG;
companion object { companion object {
fun default(): Currencies { fun default(): Currencies {

View File

@@ -1,11 +1,8 @@
package cc.n0th1ng.tripmoney.utils package cc.n0th1ng.tripmoney.utils
import android.os.Build
import androidx.annotation.RequiresApi
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@RequiresApi(Build.VERSION_CODES.O)
fun LocalDate.pretty(): String { fun LocalDate.pretty(): String {
return this.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) return this.format(DateTimeFormatter.ofPattern("dd MMM yyyy"))
} }

View File

@@ -14,5 +14,10 @@ enum class Icons(@DrawableRes val resource: Int) {
COFFEE(R.drawable.materialsymbols_ic_local_cafe_outlined), COFFEE(R.drawable.materialsymbols_ic_local_cafe_outlined),
GENERAL(R.drawable.materialsymbols_ic_shoppingmode_outlined), GENERAL(R.drawable.materialsymbols_ic_shoppingmode_outlined),
ENTERTAINMENT(R.drawable.materialsymbols_ic_theaters_outlined), ENTERTAINMENT(R.drawable.materialsymbols_ic_theaters_outlined),
LAUNDRY(R.drawable.materialsymbols_ic_local_laundry_service_outlined) LAUNDRY(R.drawable.materialsymbols_ic_local_laundry_service_outlined),
INSURANCE(R.drawable.materialsymbols_ic_health_and_safety_outlined),
SIM_DATA(R.drawable.materialsymbols_ic_sim_card_outlined),
CAR_RENTAL(R.drawable.materialsymbols_ic_directions_car_outlined),
FUEL(R.drawable.materialsymbols_ic_local_gas_station_outlined),
TOURS(R.drawable.materialsymbols_ic_tour_outlined)
} }

View File

@@ -0,0 +1,45 @@
package cc.n0th1ng.tripmoney.utils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import com.composables.icons.materialsymbols.outlined.R.drawable
@Composable
fun SearchTextOutlined(
modifier: Modifier = Modifier,
text: String,
onTextChange: (String) -> Unit,
focusRequester: FocusRequester = FocusRequester()
) {
OutlinedTextField(
value = text,
onValueChange = onTextChange,
modifier = modifier
.fillMaxWidth()
.focusRequester(focusRequester),
trailingIcon = {
if (text.isNotBlank()) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "close",
modifier = Modifier.clickable(true, onClick = { onTextChange("") })
)
} else {
Icon(
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
contentDescription = "search"
)
}
}
)
}

View File

@@ -10,6 +10,7 @@ import androidx.paging.insertSeparators
import androidx.paging.map import androidx.paging.map
import cc.n0th1ng.tripmoney.Filter import cc.n0th1ng.tripmoney.Filter
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
import cc.n0th1ng.tripmoney.data.dto.SummaryPerDay
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
@@ -44,7 +45,11 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
return expenseRepo.getBudgetLeft(tripId) return expenseRepo.getBudgetLeft(tripId)
} }
fun getExpensesDtoPaged(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<PagingData<ExpenseDto>> = fun getExpensesDtoPaged(
tripId: Int,
search: String = "",
filter: Filter = Filter()
): Flow<PagingData<ExpenseDto>> =
expenseRepo.getExpensesDtoPaged(tripId, search, filter).cachedIn(viewModelScope) expenseRepo.getExpensesDtoPaged(tripId, search, filter).cachedIn(viewModelScope)
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@@ -86,18 +91,22 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
}.cachedIn(viewModelScope) }.cachedIn(viewModelScope)
} }
fun getExpensesDto(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<List<ExpenseDto>> = fun getExpensesDto(
tripId: Int,
search: String = "",
filter: Filter = Filter()
): Flow<List<ExpenseDto>> =
expenseRepo.getExpensesDto(tripId, search, filter) expenseRepo.getExpensesDto(tripId, search, filter)
@RequiresApi(Build.VERSION_CODES.O) fun save(expense: Expense, trip: Trip, onComplete: (Int) -> Unit) {
fun save(expense: Expense, trip: Trip) {
viewModelScope.launch { viewModelScope.launch {
val rate = exchangeRateRepository.getRate( val rate = exchangeRateRepository.getRate(
Currencies.valueOf(expense.currency), Currencies.valueOf(expense.currency),
Currencies.valueOf(trip.currency), Currencies.valueOf(trip.currency),
expense.datetime.toLocalDate() expense.datetime.toLocalDate()
) )
expenseRepo.save(expense.copy(rate = rate)) val id = expenseRepo.save(expense.copy(rate = rate))
onComplete(id.toInt())
} }
} }
@@ -123,7 +132,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
suspend fun generateCSVToFile(tripId: Int, file: File) { suspend fun generateCSVToFile(tripId: Int, file: File) {
file.writer().use { writer -> file.writer().use { writer ->
CSVPrinter( CSVPrinter(
@@ -143,7 +151,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
fun getDailySums(tripId: Int, search: String, filter: Filter): Flow<Map<LocalDate, Double>> { fun getDailySums(tripId: Int, search: String, filter: Filter): Flow<Map<LocalDate, Double>> {
return getExpensesDto(tripId, search, filter) return getExpensesDto(tripId, search, filter)
.map { expenses -> .map { expenses ->
@@ -154,14 +161,12 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
fun getSummaryAmount(tripId: Int): Flow<Double> { fun getSummaryAmount(tripId: Int): Flow<Double> {
return getExpensesDto(tripId).map { list -> return getExpensesDto(tripId).map { list ->
list.sumOf { it.expense.amount * it.expense.rate } list.sumOf { it.expense.amount * it.expense.rate }
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategory>> { fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategory>> {
val tripFlow = tripRepo.getTrip(tripId) val tripFlow = tripRepo.getTrip(tripId)
val expensesFlow = getExpensesDto(tripId) val expensesFlow = getExpensesDto(tripId)
@@ -184,14 +189,37 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
} }
} }
@RequiresApi(Build.VERSION_CODES.O) fun getSummaryPerDay(tripId: Int): Flow<List<SummaryPerDay>> {
val tripFlow = tripRepo.getTrip(tripId)
val expensesFlow = getExpensesDto(tripId)
return tripFlow.combine(expensesFlow) { trip, expenses ->
val summaryPerDayRaw = expenses.groupBy { it.expense.datetime.toLocalDate() }
.map { (day, expensesForDay) ->
val total = expensesForDay.sumOf { it.expense.convertedAmount() }
SummaryPerDay(
amount = total,
day = day,
percent = 0.0f
)
}
.sortedByDescending { it.day }
val highestAmount =
if (summaryPerDayRaw.isEmpty()) 1.0 else summaryPerDayRaw.maxOf { it.amount }
summaryPerDayRaw.map {
it.copy(percent = ((it.amount / highestAmount)).toFloat())
}
}
}
fun clearOldRates() { fun clearOldRates() {
viewModelScope.launch { viewModelScope.launch {
exchangeRateRepository.clearOldRates() exchangeRateRepository.clearOldRates()
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
sealed class ExpenseListItemUi { sealed class ExpenseListItemUi {
data class Item(val expenseDto: ExpenseDto) : ExpenseListItemUi() data class Item(val expenseDto: ExpenseDto) : ExpenseListItemUi()
data class Header(val date: LocalDate, val sum: Double, val currency: String) : data class Header(val date: LocalDate, val sum: Double, val currency: String) :

View File

@@ -32,4 +32,17 @@
<string name="export_csv_subttext">Zapisz wydatki z %s do pliku</string> <string name="export_csv_subttext">Zapisz wydatki z %s do pliku</string>
<string name="add_new_category">Dodaj kategorie</string> <string name="add_new_category">Dodaj kategorie</string>
<string name="edit_category">Edytuj kategorie</string> <string name="edit_category">Edytuj kategorie</string>
<string name="archive">Archiwizuj</string>
<string name="you_want_archive">Chcesz zarchiwizować?</string>
<string name="archive_category_info">Żadne wydatki nie będą usunięte.</string>
<string name="delete_category_info">Wszystkie wydatki z kategorii %s zostaną usunięte.</string>
<string name="budget">Budżet</string>
<string name="money_left">Pozostałe środki</string>
<string name="add_expense_settings">Pokaż dodawanie wydatku na starcie</string>
<string name="yesterday">Wczoraj</string>
<string name="clear">Wyczyść</string>
<string name="no_expenses">Zacznij budżetowanie od dodania wydatków</string>
<string name="no_trip_picked">Wybierz wycieczkę żeby zobaczyć wydatki</string>
<string name="no_trip_added">Zacznij budżetowanie od dodania wycieczki</string>
<string name="no_expenses_summary">Brak wydatków do podsumowania</string>
</resources> </resources>

View File

@@ -35,10 +35,14 @@
<string name="archive">Archive</string> <string name="archive">Archive</string>
<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 %s will be removed.</string>
<string name="budget">Budget</string> <string name="budget">Budget</string>
<string name="money_left">Money left</string> <string name="money_left">Money left</string>
<string name="add_expense_settings">Open add expense form on startup</string> <string name="add_expense_settings">Open add expense form on startup</string>
<string name="yesterday">Yesterday</string> <string name="yesterday">Yesterday</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="no_expenses">Start budgeting by adding expenses</string>
<string name="no_trip_picked">Select trip to see expenses</string>
<string name="no_trip_added">Start budgeting by adding your trip</string>
<string name="no_expenses_summary">No expenses to summarize</string>
</resources> </resources>