Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation(libs.glance.appwidget); implementation(libs.glance.material3); implementation(libs.coil.compose)
testImplementation(libs.junit5); testImplementation(libs.junit.vintage); testImplementation(libs.coroutines.test); testImplementation(libs.turbine)
testImplementation(libs.truth); testImplementation(libs.robolectric); testImplementation(libs.room.testing)
testImplementation(libs.test.core)
testImplementation(libs.test.core); testImplementation(libs.compose.ui.test.junit4)
debugImplementation(libs.compose.ui.test.manifest)
}
tasks.withType<Test> { useJUnitPlatform() }
117 changes: 85 additions & 32 deletions app/src/main/java/com/splitsmart/ui/screens/ExpenseEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import android.provider.MediaStore
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.OpenInFull
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
Expand Down Expand Up @@ -147,6 +149,7 @@ private class LongMapField(
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExpenseEditorScreen(
gid: Long,
Expand Down Expand Up @@ -591,41 +594,13 @@ fun ExpenseEditorScreen(
label = { Text(t.name.lowercase().replaceFirstChar { it.uppercase() }) })
}
}
ExpenseMembersPicker(rows, itemized, type, preview, cur) { touch("rows") }
LazyColumn(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
items(rows, key = { it.id }) { r ->
// In itemized mode the per-row number is the participant's own item
// subtotal; outside it, a percent or shares split enters its own basis.
// Same field, so one branch picks the label.
val rowLabel =
when {
itemized -> "items"
type == SplitType.PERCENT -> "%"
type != SplitType.EQUAL -> "amt"
else -> null
ExpenseMemberRow(
r, itemized, type, preview?.get(r.id)?.let { money(it, cur) } ?: "") {
touch("rows")
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Checkbox(
r.included,
{
r.included = it
touch("rows")
})
Text(r.name, modifier = Modifier.weight(1f).padding(top = 12.dp))
if (rowLabel != null)
OutlinedTextField(
r.text,
{
r.text = it
touch("rows")
},
label = { Text(rowLabel) },
singleLine = true,
modifier = Modifier.width(110.dp))
else
Text(
preview?.get(r.id)?.let { money(it, cur) } ?: "",
modifier = Modifier.padding(top = 12.dp))
}
}
}
// In itemized mode the check keys off the rows having entries at all, not off
Expand Down Expand Up @@ -720,3 +695,81 @@ fun ExpenseEditorScreen(
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ExpenseMembersPicker(
rows: List<SplitRow>,
itemized: Boolean,
type: SplitType,
preview: Map<Long, Long>?,
currency: String,
onChanged: () -> Unit
) {
var expanded by rememberSaveable { mutableStateOf(false) }
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) {
Text("Members", style = MaterialTheme.typography.labelLarge)
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.OpenInFull, contentDescription = "Expand members")
}
}
if (expanded) {
ModalBottomSheet(
onDismissRequest = { expanded = false },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)) {
Column(
Modifier.fillMaxWidth().fillMaxHeight(0.9f).padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) {
Text("Split members", style = MaterialTheme.typography.titleLarge)
TextButton(onClick = { expanded = false }) { Text("Done") }
}
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)) {
items(rows, key = { it.id }) { r ->
ExpenseMemberRow(
r, itemized, type, preview?.get(r.id)?.let { money(it, currency) } ?: "") {
onChanged()
}
}
}
}
}
}
}

@Composable
private fun ExpenseMemberRow(
row: SplitRow,
itemized: Boolean,
type: SplitType,
previewAmount: String,
onChanged: () -> Unit
) {
val rowLabel =
when {
itemized -> "items"
type == SplitType.PERCENT -> "%"
type != SplitType.EQUAL -> "amt"
else -> null
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Checkbox(row.included, { row.included = it; onChanged() })
Text(row.name, modifier = Modifier.weight(1f).padding(top = 12.dp))
if (rowLabel != null)
OutlinedTextField(
row.text,
{ row.text = it; onChanged() },
label = { Text(rowLabel) },
singleLine = true,
modifier = Modifier.width(110.dp))
else Text(previewAmount, modifier = Modifier.padding(top = 12.dp))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.splitsmart.ui.screens

import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasScrollToIndexAction
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import com.splitsmart.data.SplitType
import com.splitsmart.ui.vm.SplitRow
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [35])
class ExpenseMembersPickerTest {
@get:Rule val compose = createComposeRule()

@Test
fun expandedPickerScrollsThroughAllMembers() {
val members = (1..30).map { SplitRow(it.toLong(), "Member $it") }
compose.setContent {
MaterialTheme {
ExpenseMembersPicker(members, false, SplitType.EQUAL, null, "USD") {}
}
}

compose.onNodeWithContentDescription("Expand members").performClick()
compose.onNodeWithText("Split members").assertIsDisplayed()
compose.onNode(hasScrollToIndexAction()).performScrollToIndex(29)
compose.onNodeWithText("Member 30").assertIsDisplayed()
compose.onNodeWithText("Done").performClick()
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeB
compose-ui = { module = "androidx.compose.ui:ui" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-icons = { module = "androidx.compose.material:material-icons-extended" }
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
window-size = { module = "androidx.compose.material3:material3-window-size-class" }
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
Expand Down