Compare commits

..

1 commit

Author SHA1 Message Date
8b3016004b
feat: basic departures support
also a huge refactor to simplify modules
2026-06-23 00:07:10 +10:00
2 changed files with 207 additions and 183 deletions

View file

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

View file

@ -1,12 +1,9 @@
package moe.lava.banksia.ui.layout.info
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -15,12 +12,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@ -34,10 +31,8 @@ import androidx.compose.material3.SegmentedListItem
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -85,25 +80,213 @@ 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)
@Composable
internal fun StopInfoPanel(
state: StopInfoPanelState,
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))
AnimatedContent(
targetState = state,
contentKey = { it.id },
// transitionSpec = { spec },
transitionSpec = { spec },
) { state ->
Column(Modifier.fillMaxWidth().fillMaxHeight()) {
@ -136,169 +319,10 @@ internal fun StopInfoPanel(
transitionSpec = { spec },
) { departures ->
departures?.let { departurePlatforms ->
val lazyState = if (departurePlatforms.size == 1) {
LazyListState(firstVisibleItemIndex =
departurePlatforms[0].departures.indexOfFirst {
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))
}
ManyPlatforms(departurePlatforms)
} 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,
)
}
}
}
}
MonoPlatform(departurePlatforms[0])
}
}
}