Banksia/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt

192 lines
8.2 KiB
Kotlin
Raw Normal View History

2025-04-13 00:51:32 +10:00
package moe.lava.banksia
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.add
2025-04-13 01:27:49 +10:00
import androidx.compose.foundation.layout.fillMaxSize
2025-04-30 00:11:21 +10:00
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
2025-04-30 00:11:21 +10:00
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
2025-04-14 13:35:26 +10:00
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberStandardBottomSheetState
2025-04-14 13:35:26 +10:00
import androidx.compose.runtime.Composable
2025-04-15 17:25:47 +10:00
import androidx.compose.runtime.LaunchedEffect
2025-04-14 13:35:26 +10:00
import androidx.compose.runtime.getValue
2025-04-29 15:01:28 +10:00
import androidx.compose.runtime.mutableFloatStateOf
2025-04-14 13:35:26 +10:00
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
2025-07-28 01:39:31 +10:00
import androidx.compose.runtime.saveable.rememberSaveable
2025-04-14 13:35:26 +10:00
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
2025-04-29 15:01:28 +10:00
import androidx.compose.ui.ExperimentalComposeUiApi
2025-04-13 00:51:32 +10:00
import androidx.compose.ui.Modifier
2025-04-29 15:01:28 +10:00
import androidx.compose.ui.backhandler.PredictiveBackHandler
2025-04-30 00:11:21 +10:00
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
2025-07-28 01:39:31 +10:00
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import dev.icerock.moko.geo.compose.BindLocationTrackerEffect
import dev.icerock.moko.geo.compose.LocationTrackerAccuracy
import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
import kotlinx.coroutines.launch
import moe.lava.banksia.native.BanksiaTheme
2025-04-13 01:27:49 +10:00
import moe.lava.banksia.native.maps.Maps
import moe.lava.banksia.native.maps.Point
import moe.lava.banksia.native.maps.getScreenHeight
import moe.lava.banksia.resources.Res
import moe.lava.banksia.resources.my_location_24
2025-07-28 01:39:31 +10:00
import moe.lava.banksia.ui.BanksiaViewModel
2025-04-14 13:35:26 +10:00
import moe.lava.banksia.ui.Searcher
2025-04-30 00:11:21 +10:00
import moe.lava.banksia.ui.StopInfoPanel
import org.jetbrains.compose.resources.painterResource
2025-04-14 13:35:26 +10:00
import org.jetbrains.compose.ui.tooling.preview.Preview
2025-04-29 15:01:28 +10:00
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.roundToInt
2025-04-13 00:51:32 +10:00
2025-07-28 01:39:31 +10:00
val MELBOURNE = Point(-37.8136, 144.9631)
2025-04-15 17:25:47 +10:00
2025-04-29 15:01:28 +10:00
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
2025-04-13 00:51:32 +10:00
@Composable
@Preview
2025-07-28 01:39:31 +10:00
fun App(
viewModel: BanksiaViewModel = viewModel()
) {
val scope = rememberCoroutineScope()
val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best)
val locationTracker = remember { locationFactory.createLocationTracker() }
BindLocationTrackerEffect(locationTracker)
viewModel.bindTracker(locationTracker)
scope.launch { locationTracker.startTracking() }
val state by viewModel.state.collectAsStateWithLifecycle()
2025-04-15 17:25:47 +10:00
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState(
2025-04-30 00:13:23 +10:00
initialValue = SheetValue.Hidden,
skipHiddenState = false
)
)
2025-04-14 13:35:26 +10:00
val sheetState = scaffoldState.bottomSheetState
val extInsets = if (
sheetState.currentValue != SheetValue.Hidden ||
sheetState.targetValue != SheetValue.Hidden
) {
val offset = runCatching { sheetState.requireOffset() }
val scaffoldOffset = offset.getOrDefault(0.0f).roundToInt()
(getScreenHeight() - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0)
} else 0
2025-07-28 01:39:31 +10:00
LaunchedEffect(state.stopState) {
val isShown = state.stopState != null
if (isShown)
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
else
scope.launch { scaffoldState.bottomSheetState.hide() }
}
2025-07-28 01:39:31 +10:00
var searchTextState by rememberSaveable { mutableStateOf("") }
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
var sheetSwipeEnabled by rememberSaveable { mutableStateOf(true) }
2025-04-30 00:11:21 +10:00
var handleHeight by remember { mutableStateOf(0.dp) }
var peekHeight by remember { mutableStateOf(0.dp) }
2025-04-29 15:01:28 +10:00
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
BanksiaTheme {
BottomSheetScaffold(
scaffoldState = scaffoldState,
2025-04-30 00:11:21 +10:00
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
modifier = Modifier.fillMaxSize(),
2025-07-28 01:39:31 +10:00
sheetContent = {
state.stopState?.let { stopState ->
StopInfoPanel(stopState) { peekHeight = it }
2025-04-30 00:11:21 +10:00
}
2025-07-28 01:39:31 +10:00
},
2025-04-30 00:11:21 +10:00
sheetDragHandle = {
val density = LocalDensity.current
Box(
Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp)
.onSizeChanged {
handleHeight = with(density) { it.height.toDp() }
}
) {
BottomSheetDefaults.DragHandle(modifier = Modifier.align(Alignment.Center))
}
},
2025-04-29 15:01:28 +10:00
sheetSwipeEnabled = sheetSwipeEnabled,
) {
Maps(
modifier = Modifier.fillMaxSize(),
2025-07-28 01:39:31 +10:00
cameraPositionFlow = viewModel.cameraChangeEmitter,
extInsets = WindowInsets(top = with(LocalDensity.current) {
SearchBarDefaults.InputFieldHeight.roundToPx()
}, bottom = extInsets),
2025-07-28 01:39:31 +10:00
markers = state.markers,
setLastKnownLocation = viewModel::setLastKnownLocation,
polylines = state.polylines,
)
2025-04-14 13:35:26 +10:00
Searcher(
2025-07-28 01:39:31 +10:00
selectedRoute = state.routeState?.route,
routes = state.routes,
2025-04-14 13:35:26 +10:00
expanded = searchExpandedState,
onExpandedChange = {
searchExpandedState = it
if (it)
scope.launch { scaffoldState.bottomSheetState.hide() }
},
2025-04-14 13:35:26 +10:00
text = searchTextState,
onTextChange = { searchTextState = it },
2025-07-28 01:39:31 +10:00
onRouteChange = { viewModel.switchRoute(it) }
2025-04-14 13:35:26 +10:00
)
2025-04-29 15:01:28 +10:00
PredictiveBackHandler(scaffoldState.bottomSheetState.currentValue != SheetValue.Hidden) { progress ->
sheetSwipeEnabled = false
try {
progress.collect { backEvent ->
if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded) {
peekHeightMultiplier = 1F - backEvent.progress
}
}
if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded)
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
else if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded)
scope.launch {
scaffoldState.bottomSheetState.hide()
peekHeightMultiplier = 1F
}
} catch (_: CancellationException) {
peekHeightMultiplier = 1F
}
sheetSwipeEnabled = true
}
Box(
Modifier.windowInsetsPadding(WindowInsets.safeContent.add(WindowInsets(bottom = extInsets))),
contentAlignment = Alignment.BottomEnd
) {
FloatingActionButton(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
2025-07-28 01:39:31 +10:00
onClick = { viewModel.centreCameraToLocation() },
) {
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
}
}
2025-04-13 00:51:32 +10:00
}
}
2025-04-14 21:53:07 +10:00
}