From e74bd7c09737a41728217372aacf94bdea7a8c24 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Tue, 8 Aug 2023 15:59:30 +0200 Subject: [PATCH] Add map feature --- .../rplexicon/facotry/MarqueeParser.kt | 8 +- .../rplexicon/ui/navigation/HomeNavHost.kt | 20 +- .../rplexicon/ui/navigation/ScreenNavHost.kt | 5 +- .../ui/navigation/pages/ComposableLexicon.kt | 4 +- .../ui/navigation/pages/ComposableLocation.kt | 4 +- .../screens/ComposableLocationDetail.kt | 55 +++ .../ui/screens/location/detail/FantasyMap.kt | 183 ++++++++++ .../screens/location/detail/LocationDetail.kt | 321 ++++++++++++++++++ .../detail/LocationDetailViewModel.kt | 38 +++ .../screens/location/list/LocationScreen.kt | 3 +- .../main/res/drawable/ic_baseline_add_24.xml | 5 + .../res/drawable/ic_baseline_remove_24.xml | 5 + .../drawable/ic_baseline_zoom_in_map_24.xml | 5 + 13 files changed, 640 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLocationDetail.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/FantasyMap.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt create mode 100644 app/src/main/res/drawable/ic_baseline_add_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_remove_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_zoom_in_map_24.xml diff --git a/app/src/main/java/com/pixelized/rplexicon/facotry/MarqueeParser.kt b/app/src/main/java/com/pixelized/rplexicon/facotry/MarqueeParser.kt index 6f0ec58..7385abe 100644 --- a/app/src/main/java/com/pixelized/rplexicon/facotry/MarqueeParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/facotry/MarqueeParser.kt @@ -22,8 +22,12 @@ class MarqueeParser @Inject constructor() { item is List<*> -> { val map = item.getOrNull(structure.map) as? String val name = item.getOrNull(structure.name) as? String - val x = (item.getOrNull(structure.x) as? String)?.toFloatOrNull() - val y = (item.getOrNull(structure.y) as? String)?.toFloatOrNull() + val x = (item.getOrNull(structure.x) as? String) + ?.replace(oldValue = ",", newValue = ".") + ?.toFloatOrNull() + val y = (item.getOrNull(structure.y) as? String) + ?.replace(oldValue = ",", newValue = ".") + ?.toFloatOrNull() val description = item.getOrNull(structure.description) as? String? if (map != null && name != null && x != null && y != null) { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt index 6490c59..87d524b 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt @@ -30,7 +30,9 @@ import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.pixelized.rplexicon.LocalSnack import com.pixelized.rplexicon.R -import com.pixelized.rplexicon.ui.navigation.pages.LEXICON_ROUTE +import com.pixelized.rplexicon.ui.navigation.pages.LEXICON_LIST_ROUTE +import com.pixelized.rplexicon.ui.navigation.pages.LOCATION_LIST_ROUTE +import com.pixelized.rplexicon.ui.navigation.pages.QUEST_LIST_ROUTE import com.pixelized.rplexicon.ui.navigation.pages.composableLexicon import com.pixelized.rplexicon.ui.navigation.pages.composableLocations import com.pixelized.rplexicon.ui.navigation.pages.composableQuests @@ -50,7 +52,7 @@ fun HomeNavHost( lexiconListState: LazyListState, questListState: LazyListState, locationListState: LazyListState, - startDestination: String = LEXICON_ROUTE + startDestination: String = LOCATION_LIST_ROUTE ) { CompositionLocalProvider( LocalSnack provides remember { SnackbarHostState() }, @@ -69,15 +71,17 @@ fun HomeNavHost( ) }, bottomBar = { - val selectedIndex = rememberSaveable { mutableStateOf(0) } - + val selectedIndex = rememberSaveable { + mutableStateOf(bottomBarItems.indexOfFirst { it.route == startDestination }) + } BottomAppBar( containerColor = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp, ) { bottomBarItems.forEachIndexed { index, item -> - val selectedItem = - remember { derivedStateOf { selectedIndex.value == index } } + val selectedItem = remember { + derivedStateOf { selectedIndex.value == index } + } NavigationBarItem( selected = selectedItem.value, onClick = { @@ -118,6 +122,7 @@ fun HomeNavHost( @Stable class BottomBarItem( + val route: String, val icon: Int, val label: Int, val onClick: () -> Unit, @@ -132,16 +137,19 @@ private fun rememberBottomBarItems( val option = navHostController.pageOption() listOf( BottomBarItem( + route = LEXICON_LIST_ROUTE, icon = R.drawable.ic_outline_account_circle_24, label = R.string.home_lexicon, onClick = { navHostController.navigateToLexicon(option) } ), BottomBarItem( + route = QUEST_LIST_ROUTE, icon = R.drawable.ic_outline_scroll_24, label = R.string.home_quest_log, onClick = { navHostController.navigateToQuestList(option) } ), BottomBarItem( + route = LOCATION_LIST_ROUTE, icon = R.drawable.ic_outline_map_24, label = R.string.home_location, onClick = { navHostController.navigateToLocation(option) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt index 7a1aba4..f91bb3d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt @@ -1,7 +1,6 @@ package com.pixelized.rplexicon.ui.navigation import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -16,6 +15,7 @@ import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication import com.pixelized.rplexicon.ui.navigation.screens.composableHome import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconDetail import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconSearch +import com.pixelized.rplexicon.ui.navigation.screens.composableLocationDetail import com.pixelized.rplexicon.ui.navigation.screens.composableQuestDetail val LocalScreenNavHost = staticCompositionLocalOf { @@ -45,11 +45,10 @@ fun ScreenNavHost( questListState = questListState, locationListState = locationListState ) - composableLexiconDetail() composableLexiconSearch() - composableQuestDetail() + composableLocationDetail() } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLexicon.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLexicon.kt index b068fd3..8a703a4 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLexicon.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLexicon.kt @@ -10,13 +10,13 @@ import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconScreen private const val ROUTE = "lexicon" -const val LEXICON_ROUTE = ROUTE +const val LEXICON_LIST_ROUTE = ROUTE fun NavGraphBuilder.composableLexicon( lazyListState: LazyListState, ) { animatedComposable( - route = LEXICON_ROUTE, + route = LEXICON_LIST_ROUTE, animation = NavigationAnimation.NONE, ) { LexiconScreen( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLocation.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLocation.kt index afb85de..7ef74e0 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLocation.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLocation.kt @@ -10,13 +10,13 @@ import com.pixelized.rplexicon.ui.screens.location.list.LocationScreen private const val ROUTE = "locations" -const val LOCATION_ROUTE = ROUTE +const val LOCATION_LIST_ROUTE = ROUTE fun NavGraphBuilder.composableLocations( lazyListState: LazyListState, ) { animatedComposable( - route = LOCATION_ROUTE, + route = LOCATION_LIST_ROUTE, animation = NavigationAnimation.NONE, ) { LocationScreen( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLocationDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLocationDetail.kt new file mode 100644 index 0000000..76aeb63 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLocationDetail.kt @@ -0,0 +1,55 @@ +package com.pixelized.rplexicon.ui.navigation.screens + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.NavType +import androidx.navigation.navArgument +import com.pixelized.rplexicon.ui.navigation.NavigationAnimation +import com.pixelized.rplexicon.ui.navigation.animatedComposable +import com.pixelized.rplexicon.ui.screens.location.detail.LocationDetail +import com.pixelized.rplexicon.utilitary.extentions.ARG + +private const val ROUTE = "LocationDetail" +private const val ARG_ID = "id" + +val LOCATION_DETAIL_ROUTE = ROUTE + + "?${ARG_ID.ARG}" + +@Stable +@Immutable +data class LocationDetailArgument( + val id: Int, +) + +val SavedStateHandle.locationDetailArgument: LocationDetailArgument + get() = LocationDetailArgument( + id = get(ARG_ID) ?: error("CharacterDetailArgument argument: $ARG_ID"), + ) + +fun NavGraphBuilder.composableLocationDetail() { + animatedComposable( + route = LOCATION_DETAIL_ROUTE, + arguments = listOf( + navArgument(name = ARG_ID) { + type = NavType.IntType + }, + ), + animation = NavigationAnimation.Push, + ) { + LocationDetail() + } +} + +fun NavHostController.navigateToLocationDetail( + id: Int, + option: NavOptionsBuilder.() -> Unit = {}, +) { + val route = ROUTE + + "?$ARG_ID=$id" + + navigate(route = route, builder = option) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/FantasyMap.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/FantasyMap.kt new file mode 100644 index 0000000..c9557bb --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/FantasyMap.kt @@ -0,0 +1,183 @@ +package com.pixelized.rplexicon.ui.screens.location.detail + +import androidx.annotation.DrawableRes +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateOffsetAsState +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.ui.composable.AsyncImage +import com.skydoves.landscapist.ImageOptions + +@Composable +fun FantasyMap( + modifier: Modifier = Modifier, + state: FantasyMapState, + imageOptions: ImageOptions = ImageOptions(), + @DrawableRes previewPlaceholder: Int, + item: State, + selectedItem: State, + onMarquee: (LocationDetailUio.MarqueeUio) -> Unit, +) { + val animatedScale = animateFloatAsState(targetValue = state.scale, label = "ScaleAnimation") + val animatedOffset = animateOffsetAsState(targetValue = state.offset, label = "OffsetAnimation") + + LaunchedEffect(key1 = "CenterOnMarquee:${selectedItem.value}") { + item.value.marquees.getOrNull(selectedItem.value)?.position + ?.let { state.pan(state.computeMarqueeOffset(it)) } + } + + Box( + modifier = modifier + .pointerInput("DetectTransformGestures") { + detectTransformGestures(panZoomLock = true) { _, pan, zoom, _ -> + state.scale( + scale = state.scale * zoom, + ) + state.pan( + offset = Offset( + x = state.offset.x + pan.x, + y = state.offset.y + pan.y, + ), + ) + } + } + .graphicsLayer { + scaleX = animatedScale.value + scaleY = animatedScale.value + translationX = animatedOffset.value.x + translationY = animatedOffset.value.y + }, + contentAlignment = Alignment.Center, + ) { + AsyncImage( + modifier = Modifier + .onSizeChanged { state.imageSize = it } + .drawWithContent { + drawContent() + item.value.marquees.forEachIndexed { index, item -> + drawCircle( + color = when (selectedItem.value) { + index -> Color.Cyan + else -> Color.White + }, + radius = 12.dp.toPx() / animatedScale.value, + style = Stroke( + width = 2.dp.toPx() / animatedScale.value + ), + center = Offset( + x = size.width * item.position.x, + y = size.height * item.position.y + ) + ) + } + } + .pointerInput("DetectTapGestures") { + detectTapGestures( + onTap = { tap -> + val marquee = item.value.marquees + .asReversed() + .firstOrNull { item -> + val radius = 24.dp.toPx() * animatedScale.value + (size.width * item.position.x).let { tap.x in (it - radius)..(it + radius) } && + (size.height * item.position.y).let { tap.y in (it - radius)..(it + radius) } + } + marquee?.let(onMarquee) + } + ) + }, + imageModel = { item.value.map }, + imageOptions = imageOptions, + previewPlaceholder = previewPlaceholder, + ) + } +} + +@Composable +@Stable +fun rememberFantasyMapState( + initialScale: Float = 1f, + initialOffset: Offset = Offset.Zero, + minScale: Float = 1f, + maxScale: Float = 5f, +): FantasyMapState { + return remember { + FantasyMapState( + initialScale = initialScale, + initialOffset = initialOffset, + minScale = minScale, + maxScale = maxScale, + ) + } +} + +@Stable +class FantasyMapState( + initialScale: Float = 1f, + initialOffset: Offset = Offset.Zero, + val minScale: Float = 1f, + val maxScale: Float = 5f, +) { + private val maxX by derivedStateOf { imageSize.width * scale / 2f } + private val minX by derivedStateOf { -maxX } + private val maxY by derivedStateOf { imageSize.height * scale / 2f } + private val minY by derivedStateOf { -maxY } + + private val _scale: MutableState = mutableStateOf(initialScale) + val scale: Float by _scale + + private val _offset: MutableState = mutableStateOf(initialOffset) + val offset: Offset by _offset + + private val _imageSize: MutableState = mutableStateOf(IntSize.Zero) + var imageSize: IntSize by _imageSize + + @Stable + fun scale( + scale: Float, + ) { + _scale.value = maxOf(minScale, minOf(scale, maxScale)) + } + + @Stable + fun pan( + offset: Offset, + ) { + _offset.value = Offset( + x = maxOf(minX, minOf(maxX, offset.x)), + y = maxOf(minY, minOf(maxY, offset.y)), + ) + } + + @Stable + fun computeMarqueeOffset( + origin: Offset, + size: IntSize = imageSize, + ): Offset { + return Offset( + x = (size.width / 2f - origin.x * size.width) * scale, + y = (size.height / 2f - origin.y * size.height) * scale, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt new file mode 100644 index 0000000..c1165bf --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt @@ -0,0 +1,321 @@ +package com.pixelized.rplexicon.ui.screens.location.detail + +import android.content.res.Configuration +import android.net.Uri +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.skydoves.landscapist.ImageOptions +import kotlinx.coroutines.launch +import kotlin.math.max + +@Stable +data class LocationDetailUio( + val name: String, + val map: Uri, + val marquees: List, +) { + @Stable + data class MarqueeUio( + val name: String, + val position: Offset, + val description: String?, + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LocationDetail( + viewModel: LocationDetailViewModel = hiltViewModel() +) { + val scope = rememberCoroutineScope() + val pager = rememberPagerState() + val fantasy = rememberFantasyMapState() + val screen = LocalScreenNavHost.current + + val selectedIndex = remember { mutableStateOf(0) } + + Surface { + LocationContent( + modifier = Modifier.fillMaxSize(), + pagerState = pager, + fantasyMapState = fantasy, + item = viewModel.location, + selectedIndex = selectedIndex, + onBack = { + screen.popBackStack() + }, + onMarquee = { + scope.launch { + val index = max(viewModel.location.value.marquees.indexOf(it), 0) + selectedIndex.value = index + pager.animateScrollToPage(page = index) + } + }, + onCenter = { + fantasy.scale(scale = 1f) + fantasy.pan(offset = Offset.Zero) + }, + onZoomIn = { + fantasy.scale(fantasy.scale + 1) + fantasy.pan(offset = fantasy.offset) + }, + onZoomOut = { + fantasy.scale(fantasy.scale - 1) + fantasy.pan(offset = fantasy.offset) + }, + ) + + HandlePagerScroll( + pagerState = pager, + index = selectedIndex, + onIndexChange = { page -> + pager.animateScrollToPage(page = page) + }, + onPageChange = { page -> + selectedIndex.value = page + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +private fun LocationContent( + modifier: Modifier, + pagerState: PagerState, + fantasyMapState: FantasyMapState, + item: State, + selectedIndex: State, + onBack: () -> Unit, + onMarquee: (LocationDetailUio.MarqueeUio) -> Unit, + onCenter: () -> Unit, + onZoomIn: () -> Unit, + onZoomOut: () -> Unit, +) { + Scaffold( + modifier = modifier, + containerColor = Color.Transparent, + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back_ios_new_24), + contentDescription = null + ) + } + }, + title = { + Text(text = stringResource(id = R.string.detail_title)) + }, + ) + }, + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues = paddingValues), + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .weight(weight = 2f), + tonalElevation = 2.dp, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(shape = RectangleShape), + ) { + FantasyMap( + modifier = Modifier.align(alignment = Alignment.Center), + state = fantasyMapState, + previewPlaceholder = R.drawable.im_brulkhai, + imageOptions = ImageOptions(contentScale = ContentScale.Fit), + item = item, + selectedItem = selectedIndex, + onMarquee = onMarquee, + ) + Column( + modifier = Modifier + .align(alignment = Alignment.BottomEnd) + .padding(all = 16.dp), + ) { + val colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ) + FilledIconButton( + onClick = onZoomOut, + colors = colors, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_remove_24), + contentDescription = null + ) + } + FilledIconButton( + onClick = onZoomIn, + colors = colors, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_add_24), + contentDescription = null + ) + } + FilledIconButton( + onClick = onCenter, + colors = colors, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_zoom_in_map_24), + contentDescription = null + ) + } + } + } + } + HorizontalPager( + modifier = Modifier.weight(weight = 1f), + state = pagerState, + pageCount = item.value.marquees.size, + contentPadding = PaddingValues(all = 16.dp), + pageSpacing = 8.dp, + ) { + item.value.marquees.getOrNull(it)?.let { marquee -> + Marquee( + modifier = Modifier.fillMaxSize(), + marquee = marquee, + ) + } + } + } + } +} + +@Composable +private fun Marquee( + modifier: Modifier = Modifier, + marquee: LocationDetailUio.MarqueeUio, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(space = 8.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.headlineSmall, + text = marquee.name, + ) + marquee.description?.let { + Text( + style = MaterialTheme.typography.bodyMedium, + text = it, + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun HandlePagerScroll( + pagerState: PagerState, + index: State, + onIndexChange: suspend (page: Int) -> Unit, + onPageChange: suspend (page: Int) -> Unit, +) { + if (!pagerState.isScrollInProgress) { + LaunchedEffect(Unit) { + onPageChange.invoke(pagerState.currentPage) + } + } + LaunchedEffect(key1 = index.value) { + onIndexChange.invoke(index.value) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun LocationPreview() { + LexiconTheme { + Surface { + LocationContent( + modifier = Modifier.fillMaxSize(), + pagerState = rememberPagerState(), + fantasyMapState = rememberFantasyMapState(), + item = remember { + mutableStateOf( + LocationDetailUio( + name = "Daggerfall", + map = Uri.parse("https://i.pinimg.com/originals/6d/56/cd/6d56cd9358cc94a7077157ea3c1b5842.jpg"), + marquees = listOf( + LocationDetailUio.MarqueeUio( + name = "start", + position = Offset.Zero, + description = "Marquee en haut à gauche." + ), + LocationDetailUio.MarqueeUio( + name = "end", + position = Offset(1f, 1f), + description = "Marquee en bas à droite." + ), + ), + ) + ) + }, + selectedIndex = remember { mutableStateOf(0) }, + onBack = { }, + onMarquee = { }, + onCenter = { }, + onZoomIn = { }, + onZoomOut = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt new file mode 100644 index 0000000..a45cffd --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt @@ -0,0 +1,38 @@ +package com.pixelized.rplexicon.ui.screens.location.detail + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.pixelized.rplexicon.repository.LocationRepository +import com.pixelized.rplexicon.ui.navigation.screens.locationDetailArgument +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class LocationDetailViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + repository: LocationRepository, +) : ViewModel() { + val location: State + + init { + val argument = savedStateHandle.locationDetailArgument + val source = repository.data.value[argument.id] + + location = mutableStateOf( + LocationDetailUio( + name = source.name, + map = source.uri, + marquees = source.marquees.map { marquee -> + LocationDetailUio.MarqueeUio( + name = marquee.name, + position = Offset(x = marquee.x, y = marquee.y), + description = marquee.description, + ) + } + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt index 57446c7..f720fb8 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt @@ -28,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.rplexicon.ui.composable.Loader import com.pixelized.rplexicon.ui.composable.error.HandleFetchError import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost +import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocationDetail import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.cell import com.pixelized.rplexicon.utilitary.extentions.lexicon @@ -59,7 +60,7 @@ fun LocationScreen( refreshState = refresh, refreshing = viewModel.isLoading, onItem = { -// screen.navigateToQuestDetail(id = it.id) + screen.navigateToLocationDetail(id = it.id) }, ) diff --git a/app/src/main/res/drawable/ic_baseline_add_24.xml b/app/src/main/res/drawable/ic_baseline_add_24.xml new file mode 100644 index 0000000..89633bb --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_add_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_remove_24.xml b/app/src/main/res/drawable/ic_baseline_remove_24.xml new file mode 100644 index 0000000..3e0b776 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_remove_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_zoom_in_map_24.xml b/app/src/main/res/drawable/ic_baseline_zoom_in_map_24.xml new file mode 100644 index 0000000..08ef24c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_zoom_in_map_24.xml @@ -0,0 +1,5 @@ + + +