Compare commits

..

1 commit

Author SHA1 Message Date
41f3523a5a
wip departures + refactor 2026-06-22 00:14:19 +10:00
2 changed files with 183 additions and 207 deletions

View file

@ -1,11 +1,11 @@
[versions] [versions]
agp = "9.1.0" agp = "9.1.0"
android-compileSdk = "37" android-compileSdk = "36"
android-minSdk = "24" android-minSdk = "24"
android-targetSdk = "37" android-targetSdk = "36"
androidx-activity= "1.13.0" androidx-activity= "1.13.0"
androidx-lifecycle = "2.10.0" androidx-lifecycle = "2.10.0"
compose-multiplatform = "1.12.0-alpha02" compose-multiplatform = "1.11.0-alpha04"
composeunstyled = "1.49.6" composeunstyled = "1.49.6"
coroutines = "1.10.2" coroutines = "1.10.2"
geo = "0.8.0" geo = "0.8.0"
@ -19,7 +19,7 @@ ktor = "3.4.1"
logback = "1.5.32" logback = "1.5.32"
maplibre = "0.12.1" maplibre = "0.12.1"
material = "1.7.3" material = "1.7.3"
material3 = "1.11.0-alpha07" material3 = "1.11.0-alpha04"
okio = "3.17.0" okio = "3.17.0"
playServicesLocation = "21.3.0" playServicesLocation = "21.3.0"
secretsGradlePlugin = "2.0.1" secretsGradlePlugin = "2.0.1"

View file

@ -1,9 +1,12 @@
package moe.lava.banksia.ui.layout.info package moe.lava.banksia.ui.layout.info
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -12,12 +15,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -31,8 +34,10 @@ import androidx.compose.material3.SegmentedListItem
import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -80,213 +85,25 @@ data class StopInfoPanelState(
) )
} }
@Composable
private fun listColors() = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
selectedContainerColor = MaterialTheme.colorScheme.primary,
selectedContentColor = MaterialTheme.colorScheme.onPrimary,
)
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun MonoPlatform(
state: StopInfoPanelState.DeparturePlatforms
) {
val departures = state.departures
val lazyState = LazyListState(firstVisibleItemIndex =
departures.indexOfFirst {
it.time > Clock.System.now()
}.coerceAtLeast(0)
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
state = lazyState,
) {
itemsIndexed(departures) { idx, dep ->
SegmentedListItem(
onClick = {},
colors = listColors(),
shapes = ListItemDefaults.segmentedShapes(
idx,
departures.size,
),
supportingContent = {
dep.description?.let { Text(dep.description) }
},
trailingContent = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy((-4).dp)
) {
Text(
text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
style = MaterialTheme.typography.headlineSmallEmphasized,
)
Text(
text = "mn",
style = MaterialTheme.typography.labelSmallEmphasized,
)
}
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Box(
Modifier
.clip(ShapeDefaults.ExtraSmall)
.background(dep.routeColour ?: MaterialTheme.colorScheme.surface)
.padding(vertical = 2.dp, horizontal = 4.dp)
) {
Text(
text = dep.routeName,
style = MaterialTheme.typography.labelSmallEmphasized,
color = MaterialTheme.colorScheme.surface,
)
}
Text(
text = dep.headsign,
style = MaterialTheme.typography.labelLargeEmphasized,
)
}
}
}
}
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun ManyPlatforms(
state: List<StopInfoPanelState.DeparturePlatforms>,
) {
val expandedList = remember { mutableStateListOf(*Array(state.size) { true }) }
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
state.forEachIndexed { idx, depInfo ->
val (platform, departures) = depInfo
val expanded = expandedList[idx]
stickyHeader(key = "header_${depInfo.hashCode()}") {
val base = ListItemDefaults.segmentedShapes(0, 2)
val large = MaterialTheme.shapes.large
Box(
Modifier
.animateItem()
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.padding(bottom = ListItemDefaults.SegmentedGap)
) {
SegmentedListItem(
onClick = { expandedList[idx] = !expandedList[idx] },
colors = listColors(),
shapes = if (expanded) base else base.copy(shape = large),
trailingContent = {
Icon(
painterResource(if (expanded) Res.drawable.arrow_drop_up else Res.drawable.arrow_drop_down),
contentDescription = null,
modifier = Modifier
.background(
if (expanded) MaterialTheme.colorScheme.surface else Color.Transparent,
shape = RoundedCornerShape(100)
)
.padding(6.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
},
) {
Text(
text = platform,
style = MaterialTheme.typography.labelLarge,
)
}
}
}
if (expanded) {
item(key = "items_${depInfo.hashCode()}") {
Column(
modifier = Modifier.animateItem(),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
departures.filter { it.time > Clock.System.now() }.take(5)
.forEachIndexed { idx, dep ->
SegmentedListItem(
onClick = {},
colors = listColors(),
shapes = ListItemDefaults.segmentedShapes(
idx + 1,
(departures.size + 1).coerceAtMost(6),
),
supportingContent = {
dep.description?.let { Text(dep.description) }
},
trailingContent = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy((-4).dp)
) {
Text(
text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
style = MaterialTheme.typography.headlineSmallEmphasized,
)
Text(
text = "mn",
style = MaterialTheme.typography.labelSmallEmphasized,
)
}
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Box(
Modifier
.clip(ShapeDefaults.ExtraSmall)
.background(
dep.routeColour
?: MaterialTheme.colorScheme.surface
)
.padding(vertical = 2.dp, horizontal = 4.dp)
) {
Text(
text = dep.routeName,
style = MaterialTheme.typography.labelSmallEmphasized,
color = MaterialTheme.colorScheme.surface,
)
}
Text(
text = dep.headsign,
style = MaterialTheme.typography.labelLargeEmphasized,
)
}
}
}
}
}
}
item(key = "spacer_${depInfo.hashCode()}") {
Spacer(
modifier = Modifier.animateItem().height(10.dp)
)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
internal fun StopInfoPanel( internal fun StopInfoPanel(
state: StopInfoPanelState, state: StopInfoPanelState,
onEvent: (StopInfoPanelEvent) -> Unit, onEvent: (StopInfoPanelEvent) -> Unit,
) { ) {
val colors = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
selectedContainerColor = MaterialTheme.colorScheme.primary,
selectedContentColor = MaterialTheme.colorScheme.onPrimary,
)
// val spec = fadeIn(MaterialTheme.motionScheme.defaultEffectsSpec())
// .togetherWith(fadeOut(MaterialTheme.motionScheme.defaultEffectsSpec()))
val spec = fadeIn(tween(300, 300)) togetherWith fadeOut(tween(300)) val spec = fadeIn(tween(300, 300)) togetherWith fadeOut(tween(300))
AnimatedContent( AnimatedContent(
targetState = state, targetState = state,
contentKey = { it.id }, contentKey = { it.id },
// transitionSpec = { spec },
transitionSpec = { spec }, transitionSpec = { spec },
) { state -> ) { state ->
Column(Modifier.fillMaxWidth().fillMaxHeight()) { Column(Modifier.fillMaxWidth().fillMaxHeight()) {
@ -319,10 +136,169 @@ internal fun StopInfoPanel(
transitionSpec = { spec }, transitionSpec = { spec },
) { departures -> ) { departures ->
departures?.let { departurePlatforms -> departures?.let { departurePlatforms ->
if (departurePlatforms.size > 1) { val lazyState = if (departurePlatforms.size == 1) {
ManyPlatforms(departurePlatforms) LazyListState(firstVisibleItemIndex =
} else if (departurePlatforms.size == 1) { departurePlatforms[0].departures.indexOfFirst {
MonoPlatform(departurePlatforms[0]) it.time > Clock.System.now()
}.coerceAtLeast(0)
)
} else LazyListState()
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
state = lazyState,
) {
if (departurePlatforms.size > 1) {
items(departurePlatforms) { (platform, departures) ->
// departurePlatforms.forEach { (platform, departures) ->
var expanded by rememberSaveable { mutableStateOf(true) }
val base = ListItemDefaults.segmentedShapes(0, 2)
val large = MaterialTheme.shapes.large
if (departurePlatforms.size > 1) {
SegmentedListItem(
onClick = { expanded = !expanded },
colors = colors,
shapes = if (expanded) base else base.copy(shape = large),
trailingContent = {
Icon(
painterResource(if (expanded) Res.drawable.arrow_drop_up else Res.drawable.arrow_drop_down),
contentDescription = null,
modifier = Modifier
.background(
if (expanded) MaterialTheme.colorScheme.surface else Color.Transparent,
shape = RoundedCornerShape(100)
)
.padding(6.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
},
) {
Text(
text = platform,
style = MaterialTheme.typography.labelLarge,
)
}
}
AnimatedVisibility(
visible = expanded,
enter = expandVertically(MaterialTheme.motionScheme.fastSpatialSpec()),
exit = shrinkVertically(MaterialTheme.motionScheme.fastSpatialSpec()),
) {
Column(
modifier = Modifier.height(200.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap)
) {
departures
.filter { it.time > Clock.System.now() }
.take(5)
.forEachIndexed { idx, dep ->
SegmentedListItem(
onClick = {},
colors = colors,
shapes = ListItemDefaults.segmentedShapes(
idx + if (departurePlatforms.size > 1) 1 else 0,
departures.size + 1
),
supportingContent = {
dep.description?.let { Text(dep.description) }
},
trailingContent = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy((-4).dp)
) {
Text(
text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
style = MaterialTheme.typography.headlineSmallEmphasized,
)
Text(
text = "mn",
style = MaterialTheme.typography.labelSmallEmphasized,
)
}
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Box(
Modifier
.clip(ShapeDefaults.ExtraSmall)
.background(dep.routeColour ?: MaterialTheme.colorScheme.surface)
.padding(vertical = 2.dp, horizontal = 4.dp)
) {
Text(
text = dep.routeName,
style = MaterialTheme.typography.labelSmallEmphasized,
color = MaterialTheme.colorScheme.surface,
)
}
Text(
text = dep.headsign,
style = MaterialTheme.typography.labelLargeEmphasized,
)
}
}
}
}
}
Spacer(modifier = Modifier.height(10.dp))
}
} else if (departurePlatforms.size == 1) {
itemsIndexed(departurePlatforms[0].departures) { idx, dep ->
// departurePlatforms[0].departures.forEachIndexed { idx, dep ->
SegmentedListItem(
onClick = {},
colors = colors,
shapes = ListItemDefaults.segmentedShapes(
idx,
departurePlatforms[0].departures.size,
),
supportingContent = {
dep.description?.let { Text(dep.description) }
},
trailingContent = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy((-4).dp)
) {
Text(
text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
style = MaterialTheme.typography.headlineSmallEmphasized,
)
Text(
text = "mn",
style = MaterialTheme.typography.labelSmallEmphasized,
)
}
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Box(
Modifier
.clip(ShapeDefaults.ExtraSmall)
.background(dep.routeColour ?: MaterialTheme.colorScheme.surface)
.padding(vertical = 2.dp, horizontal = 4.dp)
) {
Text(
text = dep.routeName,
style = MaterialTheme.typography.labelSmallEmphasized,
color = MaterialTheme.colorScheme.surface,
)
}
Text(
text = dep.headsign,
style = MaterialTheme.typography.labelLargeEmphasized,
)
}
}
}
}
} }
} }
} }