diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 483c5d5..02ca8cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] agp = "9.1.0" -android-compileSdk = "37" +android-compileSdk = "36" android-minSdk = "24" -android-targetSdk = "37" +android-targetSdk = "36" androidx-activity= "1.13.0" androidx-lifecycle = "2.10.0" -compose-multiplatform = "1.12.0-alpha02" +compose-multiplatform = "1.11.0-alpha04" 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-alpha07" +material3 = "1.11.0-alpha04" okio = "3.17.0" playServicesLocation = "21.3.0" secretsGradlePlugin = "2.0.1" diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt index 369721c..dc4452b 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt @@ -1,9 +1,12 @@ 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 @@ -12,12 +15,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 @@ -31,8 +34,10 @@ import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +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.Modifier 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, -) { - 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()) { @@ -319,10 +136,169 @@ internal fun StopInfoPanel( transitionSpec = { spec }, ) { departures -> departures?.let { departurePlatforms -> - if (departurePlatforms.size > 1) { - ManyPlatforms(departurePlatforms) - } else if (departurePlatforms.size == 1) { - MonoPlatform(departurePlatforms[0]) + 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)) + } + } 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, + ) + } + } + } + } } } }