2025-04-13 00:51:32 +10:00
|
|
|
package moe.lava.banksia
|
|
|
|
|
|
2025-04-14 02:02:06 +10:00
|
|
|
import androidx.compose.foundation.layout.Box
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
2025-04-14 02:02:06 +10:00
|
|
|
import androidx.compose.material3.BottomSheetScaffold
|
|
|
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
2025-04-14 23:40:54 +10:00
|
|
|
import androidx.compose.material3.FloatingActionButton
|
|
|
|
|
import androidx.compose.material3.Icon
|
2025-04-14 13:35:26 +10:00
|
|
|
import androidx.compose.material3.MaterialTheme
|
2025-05-01 19:32:28 +10:00
|
|
|
import androidx.compose.material3.SearchBarDefaults
|
2025-04-14 02:02:06 +10:00
|
|
|
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
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
2025-04-14 23:40:54 +10:00
|
|
|
import androidx.compose.ui.platform.LocalDensity
|
2025-04-14 23:47:50 +10:00
|
|
|
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
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
2025-04-29 16:13:29 +10:00
|
|
|
import moe.lava.banksia.native.BanksiaTheme
|
2025-04-13 01:27:49 +10:00
|
|
|
import moe.lava.banksia.native.maps.Maps
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
|
|
|
2025-04-14 02:02:06 +10:00
|
|
|
val scaffoldState = rememberBottomSheetScaffoldState(
|
|
|
|
|
bottomSheetState = rememberStandardBottomSheetState(
|
2025-04-30 00:13:23 +10:00
|
|
|
initialValue = SheetValue.Hidden,
|
2025-04-14 02:02:06 +10:00
|
|
|
skipHiddenState = false
|
|
|
|
|
)
|
|
|
|
|
)
|
2025-04-14 13:35:26 +10:00
|
|
|
|
2025-04-14 23:40:54 +10:00
|
|
|
val sheetState = scaffoldState.bottomSheetState
|
|
|
|
|
val extInsets = if (
|
|
|
|
|
sheetState.currentValue != SheetValue.Hidden ||
|
|
|
|
|
sheetState.targetValue != SheetValue.Hidden
|
|
|
|
|
) {
|
2025-04-14 23:47:50 +10:00
|
|
|
val offset = runCatching { sheetState.requireOffset() }
|
|
|
|
|
val scaffoldOffset = offset.getOrDefault(0.0f).roundToInt()
|
2025-04-14 23:40:54 +10:00
|
|
|
(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-04-14 23:40:54 +10:00
|
|
|
}
|
|
|
|
|
|
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) }
|
|
|
|
|
|
2025-04-29 16:13:29 +10:00
|
|
|
BanksiaTheme {
|
2025-04-14 02:02:06 +10:00
|
|
|
BottomSheetScaffold(
|
|
|
|
|
scaffoldState = scaffoldState,
|
2025-04-30 00:11:21 +10:00
|
|
|
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
2025-04-14 23:40:54 +10:00
|
|
|
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,
|
2025-04-14 02:02:06 +10:00
|
|
|
) {
|
|
|
|
|
Maps(
|
|
|
|
|
modifier = Modifier.fillMaxSize(),
|
2025-07-28 01:39:31 +10:00
|
|
|
cameraPositionFlow = viewModel.cameraChangeEmitter,
|
2025-05-01 19:32:28 +10:00
|
|
|
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 02:02:06 +10:00
|
|
|
)
|
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,
|
2025-04-29 23:03:22 +10:00
|
|
|
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-14 23:40:54 +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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-14 23:40:54 +10:00
|
|
|
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() },
|
2025-04-14 23:40:54 +10:00
|
|
|
) {
|
|
|
|
|
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
|
|
|
}
|