mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 21:03:54 +01:00
create image grid screen/pane to display images from any folder
This commit is contained in:
parent
9f7ae759a2
commit
0ff2880ba9
1 changed files with 260 additions and 0 deletions
|
|
@ -0,0 +1,260 @@
|
||||||
|
package fr.free.nrw.commons.customselector.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
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.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Check
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.round
|
||||||
|
import androidx.compose.ui.unit.toIntRect
|
||||||
|
import androidx.window.core.layout.WindowWidthSizeClass
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorTopBar
|
||||||
|
import fr.free.nrw.commons.customselector.ui.components.PartialStorageAccessDialog
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ImagesPane(
|
||||||
|
selectedFolder: Folder,
|
||||||
|
selectedImages: List<Long>,
|
||||||
|
imageList: List<Image>,
|
||||||
|
onNavigateBack: ()-> Unit,
|
||||||
|
onToggleImageSelection: (Long) -> Unit,
|
||||||
|
adaptiveInfo: WindowAdaptiveInfo
|
||||||
|
) {
|
||||||
|
val inSelectionMode by remember { derivedStateOf { selectedImages.isNotEmpty() } }
|
||||||
|
val lazyGridState = rememberLazyGridState()
|
||||||
|
var autoScrollSpeed by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
LaunchedEffect(autoScrollSpeed) {
|
||||||
|
if (autoScrollSpeed != 0f) {
|
||||||
|
while (isActive) {
|
||||||
|
lazyGridState.scrollBy(autoScrollSpeed)
|
||||||
|
delay(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CustomSelectorTopBar(
|
||||||
|
primaryText = selectedFolder.bucketName,
|
||||||
|
secondaryText = "${selectedFolder.itemsCount} images",
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
|
showNavigationIcon = adaptiveInfo.windowSizeClass
|
||||||
|
.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding->
|
||||||
|
Column(modifier = Modifier.padding(innerPadding)) {
|
||||||
|
PartialStorageAccessDialog(
|
||||||
|
isVisible = true,
|
||||||
|
onManage = { /*TODO*/ },
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(116.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.imageGridDragHandler(
|
||||||
|
gridState = lazyGridState,
|
||||||
|
imageList = imageList,
|
||||||
|
selectedImageIds = { selectedImages },
|
||||||
|
onImageSelect = { onToggleImageSelection(it) },
|
||||||
|
autoScrollThreshold = with(LocalDensity.current) { 40.dp.toPx() },
|
||||||
|
setAutoScrollSpeed = { autoScrollSpeed = it }
|
||||||
|
),
|
||||||
|
state = lazyGridState,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
contentPadding = PaddingValues(8.dp)
|
||||||
|
) {
|
||||||
|
items(imageList, key = { it.id }) { image->
|
||||||
|
val isSelected by remember {
|
||||||
|
derivedStateOf { selectedImages.contains(image.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageItem(
|
||||||
|
imagePainter = rememberAsyncImagePainter(model = image.uri),
|
||||||
|
isSelected = isSelected,
|
||||||
|
inSelectionMode = inSelectionMode,
|
||||||
|
modifier = Modifier.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
if(inSelectionMode) {
|
||||||
|
onToggleImageSelection(image.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
onToggleImageSelection(image.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageItem(
|
||||||
|
imagePainter: Painter,
|
||||||
|
isSelected: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
inSelectionMode: Boolean = false
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier.clip(RoundedCornerShape(12.dp))) {
|
||||||
|
Image(
|
||||||
|
painter = imagePainter,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
if(inSelectionMode) {
|
||||||
|
if(isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(bottomEnd = 12.dp))
|
||||||
|
.background(color = MaterialTheme.colorScheme.primary)
|
||||||
|
.padding(2.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(bottomEnd = 12.dp))
|
||||||
|
.background(color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f))
|
||||||
|
.padding(2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.imageGridDragHandler(
|
||||||
|
gridState: LazyGridState,
|
||||||
|
imageList: List<Image>,
|
||||||
|
selectedImageIds:()-> List<Long>,
|
||||||
|
autoScrollThreshold: Float,
|
||||||
|
onImageSelect: (Long) -> Unit = { },
|
||||||
|
setAutoScrollSpeed: (Float) -> Unit = { },
|
||||||
|
) = pointerInput(autoScrollThreshold, setAutoScrollSpeed, onImageSelect) {
|
||||||
|
|
||||||
|
fun imageIndexAtOffset(hitPoint: Offset): Int? =
|
||||||
|
gridState.layoutInfo.visibleItemsInfo.find { itemInfo ->
|
||||||
|
itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
|
||||||
|
}?.index
|
||||||
|
|
||||||
|
var dragStartIndex: Int? = null
|
||||||
|
var currentDragIndex: Int? = null
|
||||||
|
var isSelecting = true
|
||||||
|
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { offset->
|
||||||
|
imageIndexAtOffset(offset)?.let {
|
||||||
|
val imageId = imageList[it].id
|
||||||
|
if(!selectedImageIds().contains(imageId)) {
|
||||||
|
dragStartIndex = it
|
||||||
|
currentDragIndex = it
|
||||||
|
onImageSelect(imageList[it].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = { setAutoScrollSpeed(0f); dragStartIndex = null },
|
||||||
|
onDragCancel = { setAutoScrollSpeed(0f); dragStartIndex = null },
|
||||||
|
onDrag = { change, _->
|
||||||
|
dragStartIndex?.let { startIndex->
|
||||||
|
currentDragIndex?.let { endIndex->
|
||||||
|
val start = minOf(startIndex, endIndex)
|
||||||
|
val end = maxOf(start, endIndex)
|
||||||
|
|
||||||
|
(start..end).forEach { index->
|
||||||
|
val imageId = imageList[index].id
|
||||||
|
val ifContains = selectedImageIds().contains(imageId)
|
||||||
|
if (isSelecting && !selectedImageIds().contains(imageId)) {
|
||||||
|
println("Selecting...")
|
||||||
|
println("contains: $ifContains")
|
||||||
|
onImageSelect(imageId)
|
||||||
|
} else if (!isSelecting && selectedImageIds().contains(imageId)) {
|
||||||
|
onImageSelect(imageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Set<Int>.addUpTo(
|
||||||
|
initialKey: Int?,
|
||||||
|
pointerKey: Int?
|
||||||
|
): Set<Int> {
|
||||||
|
return if(initialKey == null || pointerKey == null) {
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
this.plus(initialKey..pointerKey)
|
||||||
|
.plus(pointerKey..initialKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Set<Int>.removeUpTo(
|
||||||
|
initialKey: Int?,
|
||||||
|
previousPointerKey: Int?
|
||||||
|
): Set<Int> {
|
||||||
|
return if(initialKey == null || previousPointerKey == null) {
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
this.minus(initialKey..previousPointerKey)
|
||||||
|
.minus(previousPointerKey..initialKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue