diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt b/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt index d6fdc03fb4..87d11d411b 100644 --- a/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt +++ b/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt @@ -8,18 +8,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import app.organicmaps.R @Composable fun BackButton( modifier: Modifier = Modifier, + size: Dp? = null, onBackClick: () -> Unit, tint: Color = MaterialTheme.colorScheme.onBackground ) { Icon( modifier = Modifier - .size(24.dp) + .size(size ?: 24.dp) .clickable { onBackClick() } .then(modifier), painter = painterResource(id = R.drawable.back), diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt index 78fdf23190..282ab0121a 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt @@ -1,5 +1,6 @@ package app.tourism.ui.screens.main +import FullscreenImageScreen import android.content.Context import android.content.Intent import androidx.compose.runtime.Composable @@ -54,6 +55,9 @@ object PersonalData @Serializable data class PlaceDetails(val id: Long) +@Serializable +data class FullscreenImageViewer(val selectedImageUrl: String, val imageUrls: List) + @Composable fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel) { val context = LocalContext.current @@ -63,6 +67,9 @@ fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel val onPlaceClick: (id: Long) -> Unit = { id -> rootNavController.navigate(PlaceDetails(id = id)) } + val onPlaceImageClick: (String, List) -> Unit = { selectedImage, imageUrls -> + rootNavController.navigate(FullscreenImageViewer(selectedImage, imageUrls)) + } val onSearchClick: (q: String) -> Unit = { q -> rootNavController.navigate(Search(query = q)) } @@ -100,6 +107,7 @@ fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel val placeDetails = backStackEntry.toRoute() PlaceDetailsScreen( id = placeDetails.id, + onPlaceImageClick = onPlaceImageClick, onBackClick = onBackClick, onMapClick = onMapClick, onCreateRoute = { placeLocation -> @@ -107,6 +115,14 @@ fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel } ) } + composable { backStackEntry -> + val fullscreenImageViewer = backStackEntry.toRoute() + FullscreenImageScreen( + onBackClick = onBackClick, + selectedImageUrl = fullscreenImageViewer.selectedImageUrl, + imageUrls = fullscreenImageViewer.imageUrls + ) + } composable { backStackEntry -> val search = backStackEntry.toRoute() SearchScreen( diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt index aea465da8a..26c8125085 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.launch @Composable fun PlaceDetailsScreen( id: Long, + onPlaceImageClick: (selectedImage: String, imageUrls: List) -> Unit, onBackClick: () -> Unit, onMapClick: () -> Unit, onCreateRoute: (PlaceLocation) -> Unit, @@ -95,11 +96,21 @@ fun PlaceDetailsScreen( } 1 -> { - GalleryNavigation(urls = place.pics) + GalleryNavigation( + urls = place.pics, + onItemClick = { item -> + onPlaceImageClick(item, place.pics) + }, + ) } 2 -> { - ReviewsNavigation(placeId = place.id, rating = place.rating) + ReviewsNavigation( + placeId = place.id, rating = place.rating, + onImageClick = { selectedImageUrl, imageUrls -> + onPlaceImageClick(selectedImageUrl, imageUrls) + }, + ) } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/AllGalleryScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/AllGalleryScreen.kt index a8617cf76a..0605749fe4 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/AllGalleryScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/AllGalleryScreen.kt @@ -1,5 +1,6 @@ package app.tourism.ui.screens.main.place_details.gallery +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding @@ -15,7 +16,7 @@ import app.tourism.ui.common.LoadImg import app.tourism.ui.common.nav.BackButtonWithText @Composable -fun AllGalleryScreen(urls: List, onBackClick: () -> Unit) { +fun AllGalleryScreen(urls: List, onItemClick: (String) -> Unit, onBackClick: () -> Unit) { Scaffold( topBar = { BackButtonWithText { onBackClick() } @@ -30,7 +31,7 @@ fun AllGalleryScreen(urls: List, onBackClick: () -> Unit) { ) { items(urls) { LoadImg( - modifier = Modifier.propertiesForSmallImage(), url = it + modifier = Modifier.clickable { onItemClick(it) }.propertiesForSmallImage(), url = it ) } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/FullscreenImageViewer.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/FullscreenImageViewer.kt new file mode 100644 index 0000000000..64adaebd9e --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/FullscreenImageViewer.kt @@ -0,0 +1,120 @@ +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +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.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import app.tourism.ui.common.nav.BackButton +import coil.compose.AsyncImage +import kotlinx.coroutines.launch + +@Composable +fun FullscreenImageScreen( + onBackClick: () -> Unit, + selectedImageUrl: String, + imageUrls: List +) { + val indexOfSelectedImage = imageUrls.indexOfFirst { it == selectedImageUrl } + val pagerState = + rememberPagerState(initialPage = if (indexOfSelectedImage != -1) indexOfSelectedImage else 0) { imageUrls.size } + val scope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize()) { + + + HorizontalPager( + pageSize = PageSize.Fill, + state = pagerState, + modifier = Modifier.fillMaxSize() + ) { page -> + var scale by remember { mutableStateOf(1f) } + var offset by remember { mutableStateOf(Offset.Zero) } + var zooming by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + detectTransformGestures { centroid, pan, zoom, _ -> + if (zoom != 1f) zooming = true + scale = (scale * zoom).coerceIn(1f, 3f) + if (scale > 1f) { + val newOffset = offset + pan + val maxX = size.width * (scale - 1) / 2 + val maxY = size.height * (scale - 1) / 2 + offset = Offset( + newOffset.x.coerceIn(-maxX, maxX), + newOffset.y.coerceIn(-maxY, maxY) + ) + } else { + offset = Offset.Zero + if (!zooming) { + // Allow swiping when not zoomed + scope.launch { + if (pan.x > 0 && pagerState.currentPage > 0) { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } else if (pan.x < 0 && pagerState.currentPage < imageUrls.size - 1) { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + } + } + zooming = false + } + } + ) { + AsyncImage( + model = imageUrls[page], + contentDescription = "Full screen image", + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxSize() + .graphicsLayer( + scaleX = scale, + scaleY = scale, + translationX = offset.x, + translationY = offset.y + ) + ) + } + } + Box(Modifier.padding(16.dp)) { + BackButton( + size = 30.dp, + onBackClick = onBackClick, + ) + } + // Page indicator + Row( + Modifier + .height(50.dp) + .fillMaxWidth() + .align(Alignment.BottomCenter), + horizontalArrangement = Arrangement.Center + ) { + repeat(imageUrls.size) { iteration -> + val color = + if (pagerState.currentPage == iteration) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.primary.copy(alpha = 0.25f) + Box( + modifier = Modifier + .padding(2.dp) + .clip(CircleShape) + .background(color) + .size(8.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryNavigation.kt index ea34349ece..a7b0c0ae87 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryNavigation.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryNavigation.kt @@ -13,13 +13,14 @@ object Gallery object AllGallery @Composable -fun GalleryNavigation(urls: List) { +fun GalleryNavigation(urls: List, onItemClick: (String) -> Unit) { val navController = rememberNavController() NavHost(navController = navController, startDestination = Gallery) { composable { GalleryScreen( urls = urls, + onItemClick = onItemClick, onMoreClick = { navController.navigate(AllGallery) }, @@ -28,6 +29,7 @@ fun GalleryNavigation(urls: List) { composable { AllGalleryScreen( urls = urls, + onItemClick = onItemClick, onBackClick = { navController.navigateUp() }, diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryScreen.kt index 6d80d9cc2b..3012075622 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryScreen.kt @@ -23,13 +23,16 @@ import app.tourism.ui.common.VerticalSpace import app.tourism.ui.theme.TextStyles @Composable -fun GalleryScreen(urls: List, onMoreClick: () -> Unit) { +fun GalleryScreen(urls: List, onItemClick: (String) -> Unit, onMoreClick: () -> Unit) { Column(Modifier.padding(Constants.SCREEN_PADDING)) { if (urls.isNotEmpty()) { LoadImg( modifier = Modifier .fillMaxWidth() .height(200.dp) + .clickable { + onItemClick(urls.first()) + } .clip(imageShape), url = urls.first(), ) @@ -40,6 +43,7 @@ fun GalleryScreen(urls: List, onMoreClick: () -> Unit) { LoadImg( modifier = Modifier .weight(1f) + .clickable { onItemClick(urls[1]) } .propertiesForSmallImage(), url = urls[1], ) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/AllReviewsScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/AllReviewsScreen.kt index b540fefd7d..4dca4716d3 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/AllReviewsScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/AllReviewsScreen.kt @@ -13,10 +13,10 @@ import app.tourism.Constants import app.tourism.ui.common.nav.BackButtonWithText import app.tourism.ui.screens.main.place_details.reviews.components.Review - @Composable fun AllReviewsScreen( reviewsVM: ReviewsViewModel = hiltViewModel(), + onImageClick: (selectedImage: String, imageUrls: List) -> Unit, onBackClick: () -> Unit, onMoreClick: (picsUrls: List) -> Unit, ) { @@ -32,7 +32,7 @@ fun AllReviewsScreen( contentPadding = PaddingValues(Constants.SCREEN_PADDING), ) { items(reviews) { - Review(review = it, onMoreClick = onMoreClick) + Review(review = it, onMoreClick = onMoreClick, onImageClick = onImageClick) } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewPicsScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewPicsScreen.kt index cc8758222d..6f41caf8be 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewPicsScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewPicsScreen.kt @@ -1,5 +1,6 @@ package app.tourism.ui.screens.main.place_details.reviews +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding @@ -16,7 +17,11 @@ import app.tourism.ui.common.nav.BackButtonWithText import app.tourism.ui.screens.main.place_details.gallery.propertiesForSmallImage @Composable -fun ReviewPicsScreen(urls: List, onBackClick: () -> Unit) { +fun ReviewPicsScreen( + urls: List, + onImageClick: (selectedImage: String, imageUrls: List) -> Unit, + onBackClick: () -> Unit +) { Scaffold( topBar = { BackButtonWithText { onBackClick() } @@ -31,7 +36,9 @@ fun ReviewPicsScreen(urls: List, onBackClick: () -> Unit) { ) { items(urls) { LoadImg( - modifier = Modifier.propertiesForSmallImage(), url = it + modifier = Modifier + .clickable { onImageClick(it, urls) } + .propertiesForSmallImage(), url = it ) } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt index ac2a680cc0..5952ebd902 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt @@ -22,6 +22,7 @@ data class ReviewPics(val urls: List) fun ReviewsNavigation( placeId: Long, rating: Double?, + onImageClick: (selectedImage: String, imageUrls: List) -> Unit, reviewsVM: ReviewsViewModel = hiltViewModel(), ) { val navController = rememberNavController() @@ -40,6 +41,7 @@ fun ReviewsNavigation( ReviewsScreen( placeId, rating, + onImageClick = onImageClick, onSeeAllClick = { navController.navigate(AllReviews) }, @@ -50,6 +52,7 @@ fun ReviewsNavigation( composable { AllReviewsScreen( reviewsVM = reviewsVM, + onImageClick = onImageClick, onBackClick = onBackClick, onMoreClick = onMoreClick ) @@ -58,6 +61,7 @@ fun ReviewsNavigation( val reviewPics = navBackStackEntry.toRoute() ReviewPicsScreen( urls = reviewPics.urls, + onImageClick = onImageClick, onBackClick = onBackClick ) } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt index 7da0d9cfe9..a0a5b75932 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.launch fun ReviewsScreen( placeId: Long, rating: Double?, + onImageClick: (selectedImage: String, imageUrls: List) -> Unit, onSeeAllClick: () -> Unit, onMoreClick: (picsUrls: List) -> Unit, reviewsVM: ReviewsViewModel = hiltViewModel(), @@ -60,7 +61,8 @@ fun ReviewsScreen( val userReview = reviewsVM.userReview.collectAsState().value val reviews = reviewsVM.reviews.collectAsState().value - val isThereReviewPlannedToPublish = reviewsVM.isThereReviewPlannedToPublish.collectAsState().value + val isThereReviewPlannedToPublish = + reviewsVM.isThereReviewPlannedToPublish.collectAsState().value ObserveAsEvents(flow = reviewsVM.uiEventsChannelFlow) { event -> if (event is UiEvent.ShowToast) context.showToast(event.message) @@ -123,6 +125,7 @@ fun ReviewsScreen( item { Review( review = it, + onImageClick = onImageClick, onMoreClick = onMoreClick, onDeleteClick = { showYesNoAlertDialog( @@ -137,7 +140,11 @@ fun ReviewsScreen( if (reviews.firstOrNull() != null) item { - Review(review = reviews[0], onMoreClick = onMoreClick) + Review( + review = reviews[0], + onMoreClick = onMoreClick, + onImageClick = onImageClick, + ) } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt index 1ce0f9dd97..1f7e670c3a 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider @@ -34,7 +33,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -45,10 +43,8 @@ import app.tourism.domain.models.details.User import app.tourism.ui.common.HorizontalSpace import app.tourism.ui.common.LoadImg import app.tourism.ui.common.VerticalSpace -import app.tourism.ui.common.special.CountryAsLabel import app.tourism.ui.common.special.CountryFlag import app.tourism.ui.common.special.RatingBar -import app.tourism.ui.screens.main.place_details.gallery.imageShape import app.tourism.ui.theme.TextStyles import app.tourism.ui.theme.getHintColor @@ -57,6 +53,7 @@ import app.tourism.ui.theme.getHintColor fun Review( modifier: Modifier = Modifier, review: Review, + onImageClick: (selectedImage: String, imageUrls: List) -> Unit, onMoreClick: (picsUrls: List) -> Unit, onDeleteClick: (() -> Unit)? = null, ) { @@ -101,14 +98,19 @@ fun Review( remaining = review.picsUrls.size - 3 ) } else { - ReviewPic(url = url) + ReviewPic(modifier = Modifier.clickable { + onImageClick( + url, + review.picsUrls + ) + }, url = url) } } } VerticalSpace(height = 16.dp) } - if(!review.comment.isNullOrBlank()) { + if (!review.comment.isNullOrBlank()) { Comment(comment = review.comment) VerticalSpace(height = 16.dp) } @@ -150,7 +152,9 @@ fun User(modifier: Modifier = Modifier, user: User) { ) } Text( - modifier = Modifier.weight(1f).width(IntrinsicSize.Min), + modifier = Modifier + .weight(1f) + .width(IntrinsicSize.Min), text = user.name, style = TextStyles.h4, fontWeight = FontWeight.W600, @@ -203,7 +207,7 @@ fun ReviewPic(modifier: Modifier = Modifier, url: String) { modifier = Modifier .width(73.dp) .height(65.dp) - .clip(imageShape) + .clip(localImageShape) .then(modifier), url = url, ) @@ -223,7 +227,7 @@ fun ShowMore(url: String, onClick: () -> Unit, remaining: Int) { .fillMaxSize() .background( color = Color.Black.copy(alpha = 0.5f), - shape = imageShape + shape = localImageShape ), ) Text( @@ -239,6 +243,6 @@ fun Modifier.getImageProperties() = this .width(73.dp) .height(65.dp) - .clip(imageShape) + .clip(localImageShape) -val imageShape = RoundedCornerShape(4.dp) \ No newline at end of file +val localImageShape = RoundedCornerShape(4.dp) \ No newline at end of file diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift index 79d1959dd6..423f984aab 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift @@ -21,7 +21,7 @@ struct ReviewsScreen: View { var body: some View { ScrollView { VStack { - // overal rating + // overall rating HStack(alignment: .center) { Image(systemName: "star.fill") .resizable()