diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4b62647..a76750c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,10 +1,12 @@
+
-
+
+
-
+
@@ -49,6 +52,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/DragAndDrop.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/DragAndDrop.kt
new file mode 100644
index 0000000..9551ccf
--- /dev/null
+++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/DragAndDrop.kt
@@ -0,0 +1,278 @@
+package com.aiosman.riderpro.ui.composables
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
+import androidx.compose.foundation.lazy.grid.LazyGridItemScope
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toOffset
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.zIndex
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun DraggableGrid(
+ items: List,
+ onMove: (Int, Int) -> Unit,
+ onDragModeStart: () -> Unit, // New parameter for drag start
+ onDragModeEnd: () -> Unit, // New parameter for drag end,
+ additionalItems: List<@Composable () -> Unit> = emptyList(), // New parameter for additional items
+ lockedIndices: List = emptyList(), // New parameter for locked indices
+ content: @Composable (T, Boolean) -> Unit,
+) {
+
+ val gridState = rememberLazyGridState()
+ val dragDropState =
+ rememberGridDragDropState(gridState, onMove, onDragModeStart, onDragModeEnd, lockedIndices)
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ modifier = Modifier.dragContainer(dragDropState),
+ state = gridState,
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+
+ ) {
+ itemsIndexed(items, key = { _, item -> item }) { index, item ->
+ DraggableItem(dragDropState, index) { isDragging ->
+ content(item, isDragging)
+ }
+ }
+ additionalItems.forEach { additionalItem ->
+ item {
+ additionalItem()
+ }
+ }
+
+ }
+}
+
+fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier {
+ return pointerInput(dragDropState) {
+ detectDragGesturesAfterLongPress(
+ onDrag = { change, offset ->
+ change.consume()
+ dragDropState.onDrag(offset = offset)
+ },
+ onDragStart = { offset -> dragDropState.onDragStart(offset) },
+ onDragEnd = { dragDropState.onDragInterrupted() },
+ onDragCancel = { dragDropState.onDragInterrupted() }
+ )
+ }
+}
+
+@ExperimentalFoundationApi
+@Composable
+fun LazyGridItemScope.DraggableItem(
+ dragDropState: GridDragDropState,
+ index: Int,
+ modifier: Modifier = Modifier,
+ content: @Composable (isDragging: Boolean) -> Unit,
+) {
+ val dragging = index == dragDropState.draggingItemIndex
+ val draggingModifier = if (dragging) {
+ Modifier
+ .zIndex(1f)
+ .graphicsLayer {
+ translationX = dragDropState.draggingItemOffset.x
+ translationY = dragDropState.draggingItemOffset.y
+ }
+ } else if (index == dragDropState.previousIndexOfDraggedItem) {
+ Modifier
+ .zIndex(1f)
+ .graphicsLayer {
+ translationX = dragDropState.previousItemOffset.value.x
+ translationY = dragDropState.previousItemOffset.value.y
+ }
+ } else {
+ Modifier.animateItemPlacement()
+ }
+ Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) {
+ content(dragging)
+ }
+}
+
+
+@Composable
+fun rememberGridDragDropState(
+ gridState: LazyGridState,
+ onMove: (Int, Int) -> Unit,
+ onDragModeStart: () -> Unit,
+ onDragModeEnd: () -> Unit,
+ lockedIndices: List // New parameter for locked indices
+): GridDragDropState {
+ val scope = rememberCoroutineScope()
+ val state = remember(gridState) {
+ GridDragDropState(
+ state = gridState,
+ onMove = onMove,
+ scope = scope,
+ onDragModeStart = onDragModeStart,
+ onDragModeEnd = onDragModeEnd,
+ lockedIndices = lockedIndices // Pass the locked indices
+ )
+ }
+ LaunchedEffect(state) {
+ while (true) {
+ val diff = state.scrollChannel.receive()
+ gridState.scrollBy(diff)
+ }
+ }
+ return state
+}
+
+class GridDragDropState internal constructor(
+ private val state: LazyGridState,
+ private val scope: CoroutineScope,
+ private val onMove: (Int, Int) -> Unit,
+ private val onDragModeStart: () -> Unit,
+ private val onDragModeEnd: () -> Unit,
+ private val lockedIndices: List // New parameter for locked indices
+) {
+ var draggingItemIndex by mutableStateOf(null)
+ private set
+
+ internal val scrollChannel = Channel()
+
+ private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
+ private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
+ internal val draggingItemOffset: Offset
+ get() = draggingItemLayoutInfo?.let { item ->
+ draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset()
+ } ?: Offset.Zero
+
+ private val draggingItemLayoutInfo: LazyGridItemInfo?
+ get() = state.layoutInfo.visibleItemsInfo
+ .firstOrNull { it.index == draggingItemIndex }
+
+ internal var previousIndexOfDraggedItem by mutableStateOf(null)
+ private set
+ internal var previousItemOffset = Animatable(Offset.Zero, Offset.VectorConverter)
+ private set
+
+ internal fun onDragStart(offset: Offset) {
+ state.layoutInfo.visibleItemsInfo
+ .firstOrNull { item ->
+ offset.x.toInt() in item.offset.x..item.offsetEnd.x &&
+ offset.y.toInt() in item.offset.y..item.offsetEnd.y
+ }?.also {
+ if (it.index !in lockedIndices) { // Check if the item is not locked
+ draggingItemIndex = it.index
+ draggingItemInitialOffset = it.offset.toOffset()
+ onDragModeStart() // Notify drag start
+ }
+ }
+ }
+
+ internal fun onDragInterrupted() {
+ if (draggingItemIndex != null) {
+ previousIndexOfDraggedItem = draggingItemIndex
+ val startOffset = draggingItemOffset
+ scope.launch {
+ previousItemOffset.snapTo(startOffset)
+ previousItemOffset.animateTo(
+ Offset.Zero,
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = Offset.VisibilityThreshold
+ )
+ )
+ previousIndexOfDraggedItem = null
+ }
+ }
+ draggingItemDraggedDelta = Offset.Zero
+ draggingItemIndex = null
+ draggingItemInitialOffset = Offset.Zero
+ onDragModeEnd() // Notify drag end
+ }
+
+ internal fun onDrag(offset: Offset) {
+ draggingItemDraggedDelta += offset
+
+ val draggingItem = draggingItemLayoutInfo ?: return
+ val startOffset = draggingItem.offset.toOffset() + draggingItemOffset
+ val endOffset = startOffset + draggingItem.size.toSize()
+ val middleOffset = startOffset + (endOffset - startOffset) / 2f
+
+ val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
+ middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
+ middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
+ draggingItem.index != item.index &&
+ item.index !in lockedIndices // Check if the target item is not locked
+ }
+ if (targetItem != null) {
+ val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
+ draggingItem.index
+ } else if (draggingItem.index == state.firstVisibleItemIndex) {
+ targetItem.index
+ } else {
+ null
+ }
+ if (scrollToIndex != null) {
+ scope.launch {
+ state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
+ onMove.invoke(draggingItem.index, targetItem.index)
+ }
+ } else {
+ onMove.invoke(draggingItem.index, targetItem.index)
+ }
+ draggingItemIndex = targetItem.index
+ } else {
+ val overscroll = when {
+ draggingItemDraggedDelta.y > 0 ->
+ (endOffset.y - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
+
+ draggingItemDraggedDelta.y < 0 ->
+ (startOffset.y - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
+
+ else -> 0f
+ }
+ if (overscroll != 0f) {
+ scrollChannel.trySend(overscroll)
+ }
+ }
+ }
+
+ private val LazyGridItemInfo.offsetEnd: IntOffset
+ get() = this.offset + this.size
+}
+
+operator fun IntOffset.plus(size: IntSize): IntOffset {
+ return IntOffset(x + size.width, y + size.height)
+}
+
+operator fun Offset.plus(size: Size): Offset {
+ return Offset(x + size.width, y + size.height)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt
index d316790..4a5ddaf 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt
@@ -1,12 +1,12 @@
package com.aiosman.riderpro.ui.post
-import android.app.Activity
-import android.content.Intent
-import android.util.Log
+import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -14,10 +14,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -25,9 +25,12 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.LinearProgressIndicator
+import androidx.compose.material.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
@@ -36,12 +39,15 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.draw.clip
import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
@@ -50,17 +56,24 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.core.content.FileProvider
import androidx.lifecycle.viewModelScope
-import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
+import com.aiosman.riderpro.ui.composables.DraggableGrid
import com.aiosman.riderpro.ui.composables.RelPostCard
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
+import org.burnoutcrew.reorderable.NoDragCancelledAnimation
+import org.burnoutcrew.reorderable.ReorderableItem
+import org.burnoutcrew.reorderable.detectReorderAfterLongPress
+import org.burnoutcrew.reorderable.rememberReorderableLazyGridState
+import org.burnoutcrew.reorderable.reorderable
+import java.io.File
@Preview
@@ -76,7 +89,9 @@ fun NewPostScreen() {
}
StatusBarMaskLayout(
darkIcons = true,
- modifier = Modifier.fillMaxSize().background(Color.White)
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
) {
Column(
modifier = Modifier
@@ -94,14 +109,19 @@ fun NewPostScreen() {
NewPostTextField("Share your adventure…", NewPostViewModel.textContent) {
NewPostViewModel.textContent = it
}
- Column (
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
) {
model.relMoment?.let {
Text("Share with")
Spacer(modifier = Modifier.height(8.dp))
Box(
- modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(color = Color(0xFFEEEEEE)).padding(24.dp)
+ modifier = Modifier
+ .clip(RoundedCornerShape(8.dp))
+ .background(color = Color(0xFFEEEEEE))
+ .padding(24.dp)
) {
RelPostCard(
momentEntity = it,
@@ -188,6 +208,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
}
}
}
+
@Composable
fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Unit) {
@@ -211,7 +232,48 @@ fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Uni
}
}
-@OptIn(ExperimentalLayoutApi::class)
+@Composable
+private fun VerticalGrid(
+ modifier: Modifier = Modifier,
+) {
+ var data by remember { mutableStateOf((0 until 20).toList()) }
+ val state = rememberReorderableLazyGridState(onMove = { from, to ->
+ data = data.toMutableList().apply {
+ add(to.index, removeAt(from.index))
+ }
+ })
+
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(4),
+ state = state.gridState,
+ contentPadding = PaddingValues(horizontal = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = modifier.reorderable(state)
+ ) {
+ items(data.size, { it }) { item ->
+ val imageItem = data[item]
+ ReorderableItem(state, item) { isDragging ->
+ val elevation = animateDpAsState(if (isDragging) 8.dp else 0.dp)
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .detectReorderAfterLongPress(state)
+ .shadow(elevation.value)
+ .aspectRatio(1f)
+ .background(MaterialTheme.colors.primary)
+ ) {
+ Text(
+ text = imageItem.toString(),
+ color = Color.White
+ )
+ }
+ }
+ }
+ }
+}
+
+
@Composable
fun AddImageGrid() {
val navController = LocalNavController.current
@@ -226,44 +288,104 @@ fun AddImageGrid() {
}
}
+ val takePictureLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.TakePicture()
+ ) { success ->
+ if (success) {
+ model.imageUriList += model.currentPhotoUri.toString()
+ }
+ }
+ val state =
+ rememberReorderableLazyGridState(
+ onMove = { from, to ->
+ model.imageUriList = model.imageUriList.toMutableList().apply {
+ add(to.index, removeAt(from.index))
+ }
+ })
+
val stroke = Stroke(
width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
- Box(
- modifier = Modifier.fillMaxWidth()
- ) {
- FlowRow(
- modifier = Modifier
- .fillMaxWidth()
- .padding(18.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- model.imageUriList.forEach {
- CustomAsyncImage(
- context,
- it,
- contentDescription = "Image",
- modifier = Modifier
- .size(110.dp)
+ DraggableGrid(
+ items = NewPostViewModel.imageUriList,
+ onMove = { from, to ->
+ NewPostViewModel.imageUriList = NewPostViewModel.imageUriList.toMutableList().apply {
+ add(to, removeAt(from))
+ }
+ },
+ lockedIndices = listOf(
- .drawBehind {
- drawRoundRect(color = Color(0xFF999999), style = stroke)
- }.noRippleClickable {
- navController.navigate(NavigationRoute.NewPostImageGrid.route)
- },
- contentScale = ContentScale.Crop
+ ),
+ onDragModeEnd = {},
+ onDragModeStart = {},
+ additionalItems = listOf(
+
+ ),
+
+ ) { item, isDrag ->
+ Box(
+ modifier = Modifier
+ ) {
+ CustomAsyncImage(
+ LocalContext.current,
+ item,
+ contentDescription = "Image",
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ .noRippleClickable {
+ navController.navigate(NavigationRoute.NewPostImageGrid.route)
+ },
+ contentScale = ContentScale.Crop
+ )
+ if (isDrag) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color(0x66000000))
)
}
+ }
+ }
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(18.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+// items(model.imageUriList.size) { index ->
+// val uri = model.imageUriList[index]
+// Box(
+// modifier = Modifier
+// .drawBehind {
+// drawRoundRect(color = Color(0xFF999999), style = stroke)
+// }
+// ) {
+// CustomAsyncImage(
+// context,
+// uri,
+// contentDescription = "Image",
+// modifier = Modifier
+// .fillMaxWidth().aspectRatio(1f)
+// .noRippleClickable {
+// navController.navigate(NavigationRoute.NewPostImageGrid.route)
+// },
+// contentScale = ContentScale.Crop
+// )
+// }
+// }
+ item {
Box(
modifier = Modifier
- .size(110.dp)
-
+ .fillMaxWidth()
+ .aspectRatio(1f)
.drawBehind {
drawRoundRect(color = Color(0xFF999999), style = stroke)
}
- .noRippleClickable{
+ .noRippleClickable {
pickImagesLauncher.launch("image/*")
},
) {
@@ -275,10 +397,37 @@ fun AddImageGrid() {
.align(Alignment.Center)
)
}
-
+ }
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ .drawBehind {
+ drawRoundRect(color = Color(0xFF999999), style = stroke)
+ }
+ .noRippleClickable {
+ val photoFile = File(context.cacheDir, "photo.jpg")
+ val photoUri: Uri = FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.fileprovider",
+ photoFile
+ )
+ model.currentPhotoUri = photoUri
+ takePictureLauncher.launch(photoUri)
+ },
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.rider_pro_camera),
+ contentDescription = "Take Photo",
+ modifier = Modifier
+ .size(48.dp)
+ .align(Alignment.Center),
+ colorFilter = ColorFilter.tint(Color.Gray)
+ )
+ }
}
}
-
}
@OptIn(ExperimentalMaterial3Api::class)
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt
index e28049f..e827171 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt
@@ -29,6 +29,7 @@ object NewPostViewModel : ViewModel() {
var imageUriList by mutableStateOf(listOf())
var relPostId by mutableStateOf(null)
var relMoment by mutableStateOf(null)
+ var currentPhotoUri: Uri? = null
fun asNewPost() {
textContent = ""
searchPlaceAddressResult = null