init #48
@@ -30,6 +30,19 @@ android {
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
|
||||
// 🔑 Critical settings for Macrobenchmark
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
|
||||
// Use release signing if needed (optional for local)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
@@ -95,7 +108,7 @@ dependencies {
|
||||
|
||||
implementation("androidx.paging:paging-runtime:3.4.2")
|
||||
implementation("androidx.paging:paging-compose:3.4.2")
|
||||
implementation("androidx.datastore:datastore-preferences:1.2.1")
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
implementation(libs.icons.material.symbols.outlined.android)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TripMoney">
|
||||
<activity
|
||||
android:screenOrientation="portrait"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.TripMoney">
|
||||
|
||||
@@ -19,16 +19,12 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import cc.n0th1ng.tripmoney.navigation.BottomNavigation
|
||||
import cc.n0th1ng.tripmoney.navigation.CustomNavigationDrawer
|
||||
@@ -45,9 +41,7 @@ import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@@ -69,6 +63,8 @@ class MainActivity : ComponentActivity() {
|
||||
fun NavigationDrawer() {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val tripViewModel: TripViewModel = hiltViewModel()
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||
val navController = rememberNavController()
|
||||
@@ -76,7 +72,8 @@ fun NavigationDrawer() {
|
||||
val current = navBackStack?.destination?.route
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
var filter by remember { mutableStateOf("") }
|
||||
var search by remember { mutableStateOf("") }
|
||||
var filter by remember { mutableStateOf(Filter()) }
|
||||
val autoOpenPref by settingsViewModel.autoOpenStartupPref.collectAsState()
|
||||
var hasHandledStartupOpen by rememberSaveable { mutableStateOf(false) }
|
||||
val shouldTriggerAutoOpen = autoOpenPref == true && !hasHandledStartupOpen
|
||||
@@ -98,7 +95,11 @@ fun NavigationDrawer() {
|
||||
}
|
||||
},
|
||||
isSearchable = current == Screens.LIST_EXPENSE,
|
||||
onFilterChange = { newFilter -> filter = newFilter })
|
||||
onSearchChange = { newSearch -> search = newSearch },
|
||||
onFilterChange = { newFilter -> filter = newFilter },
|
||||
categories = categories,
|
||||
filter = filter
|
||||
)
|
||||
},
|
||||
|
||||
bottomBar = { BottomNavigation(navController) }) { innerPadding ->
|
||||
@@ -109,7 +110,7 @@ fun NavigationDrawer() {
|
||||
) {
|
||||
composable(Screens.LIST_EXPENSE) {
|
||||
ListExpenseScreen(
|
||||
filter,
|
||||
filter = filter, search = search,
|
||||
initialAutoOpen = shouldTriggerAutoOpen,
|
||||
onAutoOpenConsumed = { hasHandledStartupOpen = true })
|
||||
}
|
||||
@@ -128,5 +129,25 @@ fun NavigationDrawer() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Filter(
|
||||
val categories: List<Category> = emptyList(), val startAmount: Double = 0.0,
|
||||
val endAmount: Double = Double.MAX_VALUE
|
||||
) {
|
||||
fun with(category: Category): Filter {
|
||||
return this.copy(categories = categories + category)
|
||||
}
|
||||
|
||||
fun withStartAmount(amount: Double): Filter {
|
||||
return this.copy(startAmount = amount)
|
||||
}
|
||||
|
||||
fun withEndAmount(amount: Double): Filter {
|
||||
return this.copy(endAmount = amount)
|
||||
}
|
||||
|
||||
fun without(category: Category): Filter {
|
||||
return this.copy(categories = categories - category)
|
||||
}
|
||||
}
|
||||
@@ -6,55 +6,93 @@ import androidx.room.Delete
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategoryRaw
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface ExpenseDao {
|
||||
|
||||
@Upsert
|
||||
suspend fun insert(expense: Expense)
|
||||
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT expense.*, category.*
|
||||
FROM expense
|
||||
JOIN category ON expense.category_id = category.id
|
||||
WHERE expense.trip_id = :tripId
|
||||
AND
|
||||
(
|
||||
(:filter IS NULL OR category.name LIKE '%' || :filter || '%')
|
||||
OR (:filter IS NULL OR expense.note LIKE '%' || :filter || '%')
|
||||
SELECT * FROM expense
|
||||
JOIN category ON expense.category_id = category.id
|
||||
WHERE trip_id = :tripId
|
||||
AND (
|
||||
:search IS NULL
|
||||
OR category.name LIKE '%' || :search || '%'
|
||||
OR expense.note LIKE '%' || :search || '%'
|
||||
)
|
||||
ORDER BY expense.datetime DESC
|
||||
AND (
|
||||
:categoriesEmpty = 1
|
||||
OR expense.category_id IN (:categoryIds)
|
||||
)
|
||||
AND (
|
||||
:startAmount IS NULL OR expense.amount >= :startAmount
|
||||
)
|
||||
AND (
|
||||
:endAmount IS NULL OR expense.amount <= :endAmount
|
||||
)
|
||||
|
||||
ORDER BY expense.datetime DESC
|
||||
"""
|
||||
)
|
||||
fun expenseDtoPaged(tripId: Int, filter: String): PagingSource<Int, ExpenseDto>
|
||||
fun expenseDtoPaged(
|
||||
tripId: Int,
|
||||
search: String?,
|
||||
categoryIds: List<Int>,
|
||||
categoriesEmpty: Boolean,
|
||||
startAmount: Double?,
|
||||
endAmount: Double?
|
||||
): PagingSource<Int, ExpenseDto>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM expense
|
||||
JOIN category ON expense.category_id = category.id
|
||||
WHERE trip_id = :tripId
|
||||
AND
|
||||
(
|
||||
(:filter IS NULL OR category.name LIKE '%' || :filter || '%')
|
||||
OR (:filter IS NULL OR expense.note LIKE '%' || :filter || '%')
|
||||
"""
|
||||
SELECT * FROM expense
|
||||
JOIN category ON expense.category_id = category.id
|
||||
WHERE trip_id = :tripId
|
||||
AND (
|
||||
:search IS NULL
|
||||
OR category.name LIKE '%' || :search || '%'
|
||||
OR expense.note LIKE '%' || :search || '%'
|
||||
)
|
||||
AND (
|
||||
:categoriesEmpty = 1
|
||||
OR expense.category_id IN (:categoryIds)
|
||||
)
|
||||
AND (
|
||||
:startAmount IS NULL OR expense.amount >= :startAmount
|
||||
)
|
||||
AND (
|
||||
:endAmount IS NULL OR expense.amount <= :endAmount
|
||||
)
|
||||
ORDER BY expense.datetime DESC
|
||||
"""
|
||||
)
|
||||
fun expenseDto(tripId: Int, filter: String): Flow<List<ExpenseDto>>
|
||||
|
||||
@Query("""
|
||||
ORDER BY expense.datetime DESC
|
||||
"""
|
||||
)
|
||||
fun expenseDto(
|
||||
tripId: Int,
|
||||
search: String?,
|
||||
categoryIds: List<Int>,
|
||||
categoriesEmpty: Boolean,
|
||||
startAmount: Double?,
|
||||
endAmount: Double?
|
||||
): Flow<List<ExpenseDto>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT trip.budget - IFNULL(SUM(expense.amount * expense.rate), 0)
|
||||
FROM trip
|
||||
LEFT JOIN expense ON expense.trip_id = trip.id
|
||||
WHERE trip.id = :tripId
|
||||
""")
|
||||
"""
|
||||
)
|
||||
fun budgetLeft(tripId: Int): Double
|
||||
|
||||
@Delete
|
||||
|
||||
@@ -3,21 +3,16 @@ package cc.n0th1ng.tripmoney.data.repository
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import cc.n0th1ng.tripmoney.Filter
|
||||
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.ExpenseDto
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExpenseRepository @Inject constructor(
|
||||
@@ -39,15 +34,43 @@ class ExpenseRepository @Inject constructor(
|
||||
expenseDao.delete(expense)
|
||||
}
|
||||
|
||||
fun getExpensesDtoPaged(tripId: Int, filter: String): Flow<PagingData<ExpenseDto>> {
|
||||
fun getExpensesDtoPaged(
|
||||
tripId: Int,
|
||||
search: String,
|
||||
filter: Filter,
|
||||
): Flow<PagingData<ExpenseDto>> {
|
||||
return Pager(
|
||||
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
|
||||
pagingSourceFactory = { expenseDao.expenseDtoPaged(tripId, filter) }
|
||||
pagingSourceFactory = {
|
||||
val categoryIds = filter.categories.map { it.id }
|
||||
expenseDao.expenseDtoPaged(
|
||||
tripId = tripId,
|
||||
search = search.takeIf { it.isNotBlank() },
|
||||
categoryIds = categoryIds,
|
||||
categoriesEmpty = categoryIds.isEmpty(),
|
||||
startAmount = filter.startAmount,
|
||||
endAmount = filter.endAmount
|
||||
)
|
||||
|
||||
}
|
||||
).flow
|
||||
}
|
||||
|
||||
fun getExpensesDto(tripId: Int, filter: String = ""): Flow<List<ExpenseDto>> {
|
||||
return expenseDao.expenseDto(tripId, filter)
|
||||
fun getExpensesDto(
|
||||
tripId: Int,
|
||||
search: String = "",
|
||||
filter: Filter = Filter()
|
||||
): Flow<List<ExpenseDto>> {
|
||||
val categoryIds = filter.categories.map { it.id }
|
||||
|
||||
return expenseDao.expenseDto(
|
||||
tripId = tripId,
|
||||
search = search.takeIf { it.isNotBlank() },
|
||||
categoryIds = categoryIds,
|
||||
categoriesEmpty = categoryIds.isEmpty(),
|
||||
startAmount = filter.startAmount,
|
||||
endAmount = filter.endAmount
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
|
||||
@@ -2,25 +2,26 @@ package cc.n0th1ng.tripmoney.navigation
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -33,12 +34,17 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import cc.n0th1ng.tripmoney.Filter
|
||||
import cc.n0th1ng.tripmoney.R
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||
import com.composables.icons.materialsymbols.outlined.R.drawable
|
||||
@@ -50,21 +56,27 @@ fun TopBar(
|
||||
onDrawerClick: () -> Unit,
|
||||
title: String = "",
|
||||
isSearchable: Boolean = false,
|
||||
onFilterChange: (String) -> Unit
|
||||
onSearchChange: (String) -> Unit,
|
||||
filter: Filter,
|
||||
onFilterChange: (Filter) -> Unit,
|
||||
categories: List<Category>
|
||||
) {
|
||||
var isSearch by remember { mutableStateOf(false) }
|
||||
var showSearch by remember { mutableStateOf(false) }
|
||||
var showFilter by remember { mutableStateOf(false) }
|
||||
var value by remember { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
TopAppBar(
|
||||
title = {
|
||||
if (isSearch && isSearchable) {
|
||||
if (showSearch && isSearchable) {
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
OutlinedTextField(
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier.fillMaxWidth(0.9f).focusRequester(focusRequester),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.focusRequester(focusRequester),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
@@ -79,9 +91,9 @@ fun TopBar(
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
isSearch = false
|
||||
showSearch = false
|
||||
value = ""
|
||||
onFilterChange("")
|
||||
onSearchChange("")
|
||||
}),
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = null
|
||||
@@ -90,7 +102,7 @@ fun TopBar(
|
||||
)
|
||||
LaunchedEffect(key1 = value) {
|
||||
delay(1000)
|
||||
onFilterChange(value)
|
||||
onSearchChange(value)
|
||||
}
|
||||
} else {
|
||||
Text(title)
|
||||
@@ -102,7 +114,7 @@ fun TopBar(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (!isSearch && isSearchable) {
|
||||
if (!showSearch && isSearchable) {
|
||||
Row(
|
||||
modifier = Modifier.padding(end = 13.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(15.dp)
|
||||
@@ -111,14 +123,16 @@ fun TopBar(
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
painter = painterResource(drawable.materialsymbols_ic_filter_alt_outlined),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable(onClick = {})
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
showFilter = true
|
||||
})
|
||||
)
|
||||
Icon(
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
isSearch = true
|
||||
showSearch = true
|
||||
})
|
||||
)
|
||||
|
||||
@@ -127,13 +141,96 @@ fun TopBar(
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
if (showFilter) {
|
||||
FilterDialog(
|
||||
onDismiss = { showFilter = false },
|
||||
onSave = { newFilter ->
|
||||
onFilterChange(newFilter)
|
||||
showFilter = false
|
||||
},
|
||||
categories = categories,
|
||||
filter = filter
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilterDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (Filter) -> 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(
|
||||
onDismiss, {
|
||||
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(10.dp)) {
|
||||
categories.forEach {
|
||||
FilterChip(selected = filter.categories.contains(it), onClick = {
|
||||
filter = if (filter.categories.contains(it)) {
|
||||
filter.without(it)
|
||||
} else {
|
||||
filter.with(it)
|
||||
}
|
||||
}, label = { Text(text = it.name) })
|
||||
}
|
||||
}
|
||||
AmountTextField(label = "from", onValueChange = { newText ->
|
||||
fromAmountString = newText
|
||||
}, value = fromAmountString)
|
||||
AmountTextField(label = "to", onValueChange = { newText ->
|
||||
toAmountString = newText
|
||||
}, value = toAmountString)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AmountTextField(label: String, onValueChange: (String) -> Unit, value: String) {
|
||||
var value by remember { mutableStateOf(value) }
|
||||
OutlinedTextField(
|
||||
label = { Text(label) },
|
||||
value = if (value == Double.MAX_VALUE.toString()) "∞" else value,
|
||||
onValueChange = { newText ->
|
||||
if (newText == Double.MAX_VALUE.toString()) {
|
||||
value = "∞"
|
||||
return@OutlinedTextField
|
||||
}
|
||||
val regex = Regex("^\\d*\\.?\\d{0,2}$")
|
||||
if (regex.matches(newText)) {
|
||||
value = newText
|
||||
onValueChange(value)
|
||||
}
|
||||
},
|
||||
placeholder = { Text("0.00") },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Decimal,
|
||||
imeAction = ImeAction.Done
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopBarSettings(navController: NavHostController) {
|
||||
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings)) },
|
||||
navigationIcon = {
|
||||
@@ -151,7 +248,30 @@ fun PreviewTopBar() {
|
||||
TopBar(
|
||||
onDrawerClick = {},
|
||||
title = "Essa",
|
||||
onFilterChange = {}
|
||||
onSearchChange = {},
|
||||
onFilterChange = {},
|
||||
isSearchable = true,
|
||||
categories = categoriesToPreview,
|
||||
filter = Filter()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewFilterDialog() {
|
||||
TripMoneyTheme {
|
||||
FilterDialog(
|
||||
onDismiss = {},
|
||||
onSave = {},
|
||||
categories = categoriesToPreview,
|
||||
filter = Filter()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.safeToDouble(): Double {
|
||||
if(this == "∞") return Double.MAX_VALUE
|
||||
if(this.isEmpty()) return 0.0
|
||||
return this.toDouble()
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import cc.n0th1ng.tripmoney.Filter
|
||||
import cc.n0th1ng.tripmoney.R.string
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
@@ -80,16 +81,19 @@ import kotlin.random.Random
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ListExpenseScreen(filter: String,
|
||||
initialAutoOpen: Boolean,
|
||||
onAutoOpenConsumed: () -> Unit ) {
|
||||
fun ListExpenseScreen(
|
||||
filter: Filter,
|
||||
search: String,
|
||||
initialAutoOpen: Boolean,
|
||||
onAutoOpenConsumed: () -> Unit
|
||||
) {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val tripViewModel: TripViewModel = hiltViewModel()
|
||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val expensesFlow =
|
||||
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, filter)
|
||||
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, search, filter)
|
||||
val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState()
|
||||
|
||||
ListExpenseScreen(
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.insertSeparators
|
||||
import androidx.paging.map
|
||||
import cc.n0th1ng.tripmoney.Filter
|
||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
@@ -44,16 +45,17 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
return expenseRepo.getBudgetLeft(tripId)
|
||||
}
|
||||
|
||||
fun getExpensesDtoPaged(tripId: Int, filter: String = ""): Flow<PagingData<ExpenseDto>> =
|
||||
expenseRepo.getExpensesDtoPaged(tripId, filter).cachedIn(viewModelScope)
|
||||
fun getExpensesDtoPaged(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<PagingData<ExpenseDto>> =
|
||||
expenseRepo.getExpensesDtoPaged(tripId, search, filter).cachedIn(viewModelScope)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getExpensesWithHeadersPaged(
|
||||
tripId: Int,
|
||||
filter: String = ""
|
||||
search: String = "",
|
||||
filter: Filter
|
||||
): Flow<PagingData<ExpenseListItemUi>> {
|
||||
val pagingFlow = getExpensesDtoPaged(tripId, filter)
|
||||
val sumsFlow = getDailySums(tripId, filter)
|
||||
val pagingFlow = getExpensesDtoPaged(tripId, search, filter)
|
||||
val sumsFlow = getDailySums(tripId, search, filter)
|
||||
val tripFlow = tripRepo.getTrip(tripId)
|
||||
return combine(pagingFlow, sumsFlow, tripFlow) { pagingData, sums, trip ->
|
||||
val currency = trip?.currency ?: ""
|
||||
@@ -85,8 +87,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun getExpensesDto(tripId: Int, filter: String = ""): Flow<List<ExpenseDto>> =
|
||||
expenseRepo.getExpensesDto(tripId, filter)
|
||||
fun getExpensesDto(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<List<ExpenseDto>> =
|
||||
expenseRepo.getExpensesDto(tripId, search, filter)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun save(expense: Expense, trip: Trip) {
|
||||
@@ -143,8 +145,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getDailySums(tripId: Int, filter: String): Flow<Map<LocalDate, Double>> {
|
||||
return getExpensesDto(tripId, filter)
|
||||
fun getDailySums(tripId: Int, search: String, filter: Filter): Flow<Map<LocalDate, Double>> {
|
||||
return getExpensesDto(tripId, search, filter)
|
||||
.map { expenses ->
|
||||
expenses.groupBy { it.expense.datetime.toLocalDate() }
|
||||
.mapValues { (_, list) ->
|
||||
|
||||
@@ -39,6 +39,7 @@ dependencies {
|
||||
implementation(libs.androidx.espresso.core)
|
||||
implementation(libs.androidx.uiautomator)
|
||||
implementation(libs.androidx.benchmark.macro.junit4)
|
||||
implementation(libs.androidx.ui.test.junit4)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
|
||||
@@ -3,6 +3,7 @@ package cc.n0th1ng.baselineprofile
|
||||
import android.R.attr.contentDescription
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
@@ -16,7 +17,6 @@ import org.junit.runner.RunWith
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class BaselineProfileGenerator {
|
||||
|
||||
@get:Rule
|
||||
val rule = BaselineProfileRule()
|
||||
|
||||
@@ -29,12 +29,8 @@ class BaselineProfileGenerator {
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
|
||||
|
||||
// Give Compose time to render
|
||||
Thread.sleep(500)
|
||||
|
||||
val listNav = device.wait(Until.findObject(By.desc("listExpenseScreen")), 10000)
|
||||
listNav?.click() ?: throw RuntimeException("listExpenseScreen not found or not clickable")
|
||||
device.waitForIdle()
|
||||
device.wait(Until.hasObject(By.desc("list screen")), 10_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[versions]
|
||||
agp = "8.13.2"
|
||||
datastorePreferences = "1.2.1"
|
||||
iconsMaterialSymbolsOutlinedAndroid = "2.2.1"
|
||||
kotlin = "2.2.21"
|
||||
coreKtx = "1.10.1"
|
||||
@@ -15,9 +16,11 @@ uiautomator = "2.3.0"
|
||||
benchmarkMacroJunit4 = "1.2.4"
|
||||
baselineprofile = "1.2.4"
|
||||
profileinstaller = "1.3.1"
|
||||
uiTestJunit4 = "1.10.6"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
|
||||
icons-material-symbols-outlined-android = { module = "com.composables:icons-material-symbols-outlined-android", version.ref = "iconsMaterialSymbolsOutlinedAndroid" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
@@ -37,6 +40,7 @@ androidx-compose-foundation-layout = { group = "androidx.compose.foundation", na
|
||||
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
||||
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
|
||||
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "uiTestJunit4" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user