diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 29f280c9e..5227cca3e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -144,6 +144,7 @@
android:label="@string/result" />
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/CustomSelectorScreen.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/CustomSelectorScreen.kt
index 26724fe65..4fa3bbdca 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/CustomSelectorScreen.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/CustomSelectorScreen.kt
@@ -63,6 +63,7 @@ fun CustomSelectorScreen(
uiState: CustomSelectorState,
onEvent: (CustomSelectorEvent)-> Unit,
selectedImageIds: ()-> Set,
+ onViewImage: (id: Long)-> Unit,
hasPartialAccess: Boolean = false
) {
val adaptiveInfo = currentWindowAdaptiveInfo()
@@ -96,6 +97,7 @@ fun CustomSelectorScreen(
selectedFolder = folder,
selectedImages = selectedImageIds,
onNavigateBack = { navigator.navigateBack() },
+ onViewImage = onViewImage,
onEvent = onEvent,
adaptiveInfo = adaptiveInfo,
hasPartialAccess = hasPartialAccess
@@ -264,6 +266,7 @@ private fun CustomSelectorScreenPreview() {
CommonsTheme {
CustomSelectorScreen(
uiState = CustomSelectorState(),
+ onViewImage = { },
onEvent = { },
selectedImageIds = { emptySet() },
hasPartialAccess = true
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/ImagesPane.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/ImagesPane.kt
index a984323b1..77c32a24c 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/ImagesPane.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/ImagesPane.kt
@@ -68,6 +68,7 @@ fun ImagesPane(
selectedFolder: Folder,
selectedImages: () -> Set,
onNavigateBack: () -> Unit,
+ onViewImage: (id: Long)-> Unit,
onEvent: (CustomSelectorEvent) -> Unit,
adaptiveInfo: WindowAdaptiveInfo,
hasPartialAccess: Boolean = false
@@ -128,7 +129,7 @@ fun ImagesPane(
}
LazyVerticalGrid(
- columns = GridCells.Adaptive(116.dp),
+ columns = GridCells.Adaptive(96.dp),
modifier = Modifier
.fillMaxSize()
.imageGridDragHandler(
@@ -159,6 +160,8 @@ fun ImagesPane(
onClick = {
if (uiState.inSelectionMode) {
onEvent(CustomSelectorEvent.OnImageSelection(image.id))
+ } else {
+ onViewImage(image.id)
}
},
onLongClick = {
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/ViewImageScreen.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/ViewImageScreen.kt
new file mode 100644
index 000000000..90b9b641c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/screens/ViewImageScreen.kt
@@ -0,0 +1,179 @@
+package fr.free.nrw.commons.customselector.ui.screens
+
+import androidx.compose.foundation.gestures.detectTransformGestures
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+import fr.free.nrw.commons.customselector.model.Image
+import kotlin.math.abs
+
+@Composable
+fun ViewImageScreen(
+ currentImageIndex: Int,
+ imageList: List,
+) {
+ var imageScale by remember { mutableFloatStateOf(1f) }
+ var imageOffset by remember { mutableStateOf(Offset.Zero) }
+ var imageSize by remember { mutableStateOf(IntSize.Zero) }
+ var containerSize by remember { mutableStateOf(IntSize.Zero) }
+ val coroutineScope = rememberCoroutineScope()
+
+ LaunchedEffect(imageSize) {
+ println("Image Size : $imageSize")
+ }
+
+ val pagerState = rememberPagerState(initialPage = currentImageIndex) { imageList.size }
+
+ val scrollConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return if (imageScale > 1f) {
+ // If zoomed in, consume the scroll for panning
+ println("Consuming for panning...")
+ available
+ } else if (
+ source == NestedScrollSource.UserInput && abs(pagerState.currentPageOffsetFraction) > 1e-6
+ ) {
+ println("Handling swipe gestures...")
+ // Handle page swipes only if the image isn't zoomed
+ val delta = available.x
+ val consumed = -pagerState.dispatchRawDelta(-delta)
+ Offset(consumed, 0f)
+ } else {
+ println("Just passing the as it is...")
+ Offset.Zero
+ }
+ }
+ }
+
+ HorizontalPager(
+ state = pagerState,
+ key = { imageList[it].id },
+ pageSpacing = 16.dp
+ ) {
+ BoxWithConstraints(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged {
+ containerSize = it
+ },
+ contentAlignment = Alignment.Center
+ ) {
+// val state = rememberTransformableState { zoomChange, panChange, _ ->
+// imageScale = (imageScale * zoomChange).coerceIn(1f, 7f)
+//
+// val imageWidth = imageSize.width * imageScale
+// val imageHeight = imageSize.height * imageScale
+//
+// val extraWidth = (imageWidth - constraints.maxWidth).coerceAtLeast(0f)
+// val extraHeight = (imageHeight - constraints.maxHeight).coerceAtLeast(0f)
+//
+// val maxX = extraWidth / 2
+// val maxY = extraHeight / 2
+//
+// imageOffset = Offset(
+// x = (imageOffset.x + imageScale * panChange.x).coerceIn(-maxX, maxX),
+// y = (imageOffset.y + imageScale * panChange.y).coerceIn(-maxY, maxY)
+// )
+// }
+
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(imageList[it].uri)
+ .build(),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .onSizeChanged { imageSize = it }
+// .pointerInput(Unit) {
+// detectTransformGestures { centroid, pan, zoom, _ ->
+// imageScale = (imageScale * zoom).coerceIn(1f, 7f)
+//
+// val imageWidth = imageSize.width * imageScale
+// val imageHeight = imageSize.height * imageScale
+//
+// val extraWidth = (imageWidth-constraints.maxWidth).coerceAtLeast(0f)
+// val extraHeight = (imageHeight-constraints.maxHeight).coerceAtLeast(0f)
+//
+// val maxX = extraWidth / 2
+// val maxY = extraHeight / 2
+//
+// imageOffset = Offset(
+// x = (imageOffset.x + imageScale * pan.x).coerceIn(
+// -maxX,
+// maxX
+// ),
+// y = (imageOffset.y + imageScale * pan.y).coerceIn(-maxY, maxY)
+// )
+// }
+// }
+ .graphicsLayer {
+ scaleX = imageScale
+ scaleY = imageScale
+ translationX = imageOffset.x
+ translationY = imageOffset.y
+ }
+ )
+ }
+ }
+}
+
+suspend fun PointerInputScope.detectDragAndZoomGestures(
+ onZoom: (Float) -> Unit,
+ onDrag: (Offset) -> Unit
+) {
+ detectTransformGestures { _, pan, zoom, _ ->
+ onZoom(zoom)
+ onDrag(pan)
+ }
+}
+
+fun Offset.calculateNewOffset(
+ centroid: Offset,
+ pan: Offset,
+ zoom: Float,
+ gestureZoom: Float,
+ size: IntSize
+): Offset {
+ val newScale = maxOf(1f, zoom * gestureZoom)
+ val newOffset = (this + centroid / zoom) -
+ (centroid / newScale + pan / zoom)
+ return Offset(
+ newOffset.x.coerceIn(0f, (size.width / zoom) * (zoom - 1f)),
+ newOffset.y.coerceIn(0f, (size.height / zoom) * (zoom - 1f))
+ )
+}
+
+fun calculateDoubleTapOffset(
+ zoom: Float,
+ size: IntSize,
+ tapOffset: Offset
+): Offset {
+ val newOffset = Offset(tapOffset.x, tapOffset.y)
+ return Offset(
+ newOffset.x.coerceIn(0f, (size.width / zoom) * (zoom - 1f)),
+ newOffset.y.coerceIn(0f, (size.height / zoom) * (zoom - 1f))
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
index 47e139147..1d9fcd118 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
@@ -6,6 +6,7 @@ import android.app.Dialog
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
@@ -14,6 +15,9 @@ import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -22,6 +26,9 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.data.MediaReader
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
@@ -31,10 +38,10 @@ import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.screens.CustomSelectorScreen
+import fr.free.nrw.commons.customselector.ui.screens.ViewImageScreen
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
-import fr.free.nrw.commons.filepicker.Constants
import fr.free.nrw.commons.media.ZoomableActivity
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.ui.theme.CommonsTheme
@@ -129,7 +136,7 @@ class CustomSelectorActivity :
private var showPartialAccessIndicator by mutableStateOf(false)
- private val startForResult = registerForActivityResult(StartActivityForResult()){ result ->
+ private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result ->
onFullScreenDataReceived(result)
}
@@ -137,6 +144,7 @@ class CustomSelectorActivity :
* onCreate Activity, sets theme, initialises the view model, setup view.
*/
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
ContextCompat.checkSelfPermission(
@@ -182,12 +190,30 @@ class CustomSelectorActivity :
val uiState by csViewModel.uiState.collectAsStateWithLifecycle()
CommonsTheme {
- CustomSelectorScreen(
- uiState = uiState,
- onEvent = csViewModel::onEvent,
- selectedImageIds = { uiState.selectedImageIds },
- hasPartialAccess = showPartialAccessIndicator
- )
+ val navController = rememberNavController()
+
+ NavHost(navController = navController, startDestination = "main") {
+ composable(route = "main") {
+ CustomSelectorScreen(
+ uiState = uiState,
+ onEvent = csViewModel::onEvent,
+ onViewImage = { navController.navigate("view_image/$it") },
+ selectedImageIds = { uiState.selectedImageIds },
+ hasPartialAccess = showPartialAccessIndicator
+ )
+ }
+
+ composable(route = "view_image/{imageId}") { backStackEntry->
+ val imageId = backStackEntry.arguments?.getString("imageId")?.toLongOrNull()
+ val imageUri = uiState.filteredImages.find { it.id == imageId }?.uri ?: Uri.EMPTY
+ val imageIndex = uiState.filteredImages.indexOfFirst { it.id == imageId }
+
+ ViewImageScreen(
+ currentImageIndex = imageIndex,
+ imageList = uiState.filteredImages
+ )
+ }
+ }
}
}