diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5c8cbf6..f567213 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -101,6 +101,5 @@ dependencies {
implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-perf")
implementation("com.google.firebase:firebase-messaging-ktx")
-
}
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/MainActivity.kt b/app/src/main/java/com/aiosman/riderpro/MainActivity.kt
index b982779..c2af4b6 100644
--- a/app/src/main/java/com/aiosman/riderpro/MainActivity.kt
+++ b/app/src/main/java/com/aiosman/riderpro/MainActivity.kt
@@ -101,9 +101,15 @@ class MainActivity : ComponentActivity() {
val postId = intent.getStringExtra("POST_ID")
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
- navController.navigate(NavigationRoute.Post.route.replace("{id}", postId))
+ navController.navigate(
+ NavigationRoute.Post.route.replace(
+ "{id}",
+ postId
+ )
+ )
}
}
+
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt
index 8226e4a..fc9a8d2 100644
--- a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt
+++ b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt
@@ -460,11 +460,17 @@ class AccountServiceImpl : AccountService {
}
override suspend fun resetPassword(email: String) {
- ApiClient.api.resetPassword(
+ val resp = ApiClient.api.resetPassword(
ResetPasswordRequestBody(
username = email
)
)
+ if (!resp.isSuccessful) {
+ parseErrorResponse(resp.errorBody())?.let {
+ throw it.toServiceException()
+ }
+ throw ServiceException("Failed to reset password")
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt
index b8076e3..cf290b9 100644
--- a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt
+++ b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt
@@ -155,7 +155,8 @@ class CommentRemoteDataSource(
postUser: Int?,
selfNotice: Boolean?,
order: String?,
- parentCommentId: Int?
+ parentCommentId: Int?,
+ pageSize: Int? = 20
): ListContainer {
return commentService.getComments(
pageNumber,
@@ -163,7 +164,8 @@ class CommentRemoteDataSource(
postUser = postUser,
selfNotice = selfNotice,
order = order,
- parentCommentId = parentCommentId
+ parentCommentId = parentCommentId,
+ pageSize = pageSize
)
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt
index fea49c0..627aeef 100644
--- a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt
+++ b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt
@@ -29,7 +29,9 @@ data class Moment(
@SerializedName("commentCount")
val commentCount: Long,
@SerializedName("time")
- val time: String
+ val time: String,
+ @SerializedName("isFollowed")
+ val isFollowed: Boolean,
) {
fun toMomentItem(): MomentEntity {
return MomentEntity(
@@ -38,7 +40,7 @@ data class Moment(
nickname = user.nickName,
location = "Worldwide",
time = ApiClient.dateFromApiString(time),
- followStatus = false,
+ followStatus = isFollowed,
momentTextContent = textContent,
momentPicture = R.drawable.default_moment_img,
likeCount = likeCount.toInt(),
diff --git a/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt b/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt
index 73a737c..f734c0e 100644
--- a/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt
+++ b/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt
@@ -46,14 +46,15 @@ class CommentPagingSource(
postUser = postUser,
selfNotice = selfNotice,
order = order,
- parentCommentId = parentCommentId
+ parentCommentId = parentCommentId,
+ pageSize = params.loadSize
)
LoadResult.Page(
data = comments.list,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (comments.list.isEmpty()) null else comments.page + 1
)
- } catch (exception: IOException) {
+ } catch (exception: Exception) {
return LoadResult.Error(exception)
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt b/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt
index 5979544..04a8a28 100644
--- a/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt
+++ b/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt
@@ -8,6 +8,7 @@ import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.ServiceException
import com.aiosman.riderpro.data.UploadImage
import com.aiosman.riderpro.data.api.ApiClient
+import com.aiosman.riderpro.data.parseErrorResponse
import com.aiosman.riderpro.entity.MomentEntity
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
@@ -165,8 +166,13 @@ class MomentBackend {
suspend fun getMomentById(id: Int): MomentEntity {
var resp = ApiClient.api.getPost(id)
- var body = resp.body()?.data ?: throw ServiceException("Failed to get moment")
- return body.toMomentItem()
+ if (!resp.isSuccessful) {
+ parseErrorResponse(resp.errorBody())?.let {
+ throw it.toServiceException()
+ }
+ throw ServiceException("Failed to get moment")
+ }
+ return resp.body()?.data?.toMomentItem() ?: throw ServiceException("Failed to get moment")
}
suspend fun likeMoment(id: Int) {
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/account/ResetPassword.kt b/app/src/main/java/com/aiosman/riderpro/ui/account/ResetPassword.kt
index 82eef66..94086fa 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/account/ResetPassword.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/account/ResetPassword.kt
@@ -49,8 +49,17 @@ fun ResetPasswordScreen() {
var isSendSuccess by remember { mutableStateOf(null) }
var isLoading by remember { mutableStateOf(false) }
val navController = LocalNavController.current
-
+ var usernameError by remember { mutableStateOf(null) }
+ fun validate(): Boolean {
+ if (username.isEmpty()) {
+ usernameError = context.getString(R.string.text_error_email_required)
+ return false
+ }
+ usernameError = null
+ return true
+ }
fun resetPassword() {
+ if (!validate()) return
scope.launch {
isLoading = true
try {
@@ -78,7 +87,7 @@ fun ResetPasswordScreen() {
)
) {
NoticeScreenHeader(
- "RECOVER ACCOUNT",
+ stringResource(R.string.recover_account_upper),
moreIcon = false
)
}
@@ -93,7 +102,7 @@ fun ResetPasswordScreen() {
if (isSendSuccess!!) {
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "Reset password email has been sent to your email address",
+ text = stringResource(R.string.reset_mail_send_success),
style = TextStyle(
color = Color(0xFF333333),
fontSize = 14.sp,
@@ -103,7 +112,7 @@ fun ResetPasswordScreen() {
} else {
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "Failed to send reset password email",
+ text = stringResource(R.string.reset_mail_send_failed),
style = TextStyle(
color = Color(0xFF333333),
fontSize = 14.sp,
@@ -138,7 +147,8 @@ fun ResetPasswordScreen() {
onValueChange = { username = it },
label = stringResource(R.string.login_email_label),
hint = stringResource(R.string.text_hint_email),
- enabled = !isLoading
+ enabled = !isLoading,
+ error = usernameError
)
Spacer(modifier = Modifier.height(72.dp))
if (isLoading) {
@@ -148,7 +158,7 @@ fun ResetPasswordScreen() {
modifier = Modifier
.width(345.dp)
.height(48.dp),
- text = "Recover Account",
+ text = stringResource(R.string.recover),
backgroundImage = R.mipmap.rider_pro_signup_red_bg
) {
resetPassword()
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt b/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt
index e2ebbbf..b277865 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt
@@ -1,6 +1,5 @@
package com.aiosman.riderpro.ui.comment
-import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -14,11 +13,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ModalBottomSheet
@@ -30,17 +26,13 @@ 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.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -48,15 +40,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.paging.compose.collectAsLazyPagingItems
-import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.CommentEntity
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal
-import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.CommentContent
import com.aiosman.riderpro.ui.post.CommentMenuModal
-import com.aiosman.riderpro.ui.post.CommentsSection
import com.aiosman.riderpro.ui.post.CommentsViewModel
import com.aiosman.riderpro.ui.post.OrderSelectionComponent
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/CustomClickableText.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/CustomClickableText.kt
new file mode 100644
index 0000000..b0c0d47
--- /dev/null
+++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/CustomClickableText.kt
@@ -0,0 +1,50 @@
+package com.aiosman.riderpro.ui.composables
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+
+@Composable
+fun CustomClickableText(
+ text: AnnotatedString,
+ modifier: Modifier = Modifier,
+ style: TextStyle = TextStyle.Default,
+ softWrap: Boolean = true,
+ overflow: TextOverflow = TextOverflow.Clip,
+ maxLines: Int = Int.MAX_VALUE,
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+ onLongPress: () -> Unit = {},
+ onClick: (Int) -> Unit
+) {
+ val layoutResult = remember { mutableStateOf(null) }
+ val pressIndicator = Modifier.pointerInput(onClick) {
+ detectTapGestures(
+ onLongPress = { onLongPress() }
+ ) { pos ->
+ layoutResult.value?.let { layoutResult ->
+ onClick(layoutResult.getOffsetForPosition(pos))
+ }
+ }
+ }
+
+ BasicText(
+ text = text,
+ modifier = modifier.then(pressIndicator),
+ style = style,
+ softWrap = softWrap,
+ overflow = overflow,
+ maxLines = maxLines,
+ onTextLayout = {
+ layoutResult.value = it
+ onTextLayout(it)
+ }
+ )
+}
\ 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/composables/EditCommentBottomModal.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt
index f4a6bbc..3fd49c8 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt
@@ -1,5 +1,7 @@
package com.aiosman.riderpro.ui.composables
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -24,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -74,16 +77,25 @@ fun EditCommentBottomModal(
fontSize = 20.sp,
fontStyle = FontStyle.Italic
)
- Image(
- painter = painterResource(id = R.drawable.rider_pro_send),
- contentDescription = "Send",
- modifier = Modifier
- .size(32.dp)
- .noRippleClickable {
- onSend(text)
- text = ""
- },
- )
+ Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty ->
+ Image(
+ painter = rememberUpdatedState(
+ if (isNotEmpty) painterResource(id = R.drawable.rider_pro_send) else painterResource(
+ id = R.drawable.rider_pro_send_disable
+ )
+ ).value,
+ contentDescription = "Send",
+ modifier = Modifier
+ .size(32.dp)
+ .noRippleClickable {
+ if (text.isNotEmpty()){
+ onSend(text)
+ text = ""
+ }
+
+ },
+ )
+ }
}
Spacer(modifier = Modifier.height(16.dp))
if (replyComment != null) {
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/FollowButton.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/FollowButton.kt
new file mode 100644
index 0000000..53db789
--- /dev/null
+++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/FollowButton.kt
@@ -0,0 +1,57 @@
+package com.aiosman.riderpro.ui.composables
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.aiosman.riderpro.R
+import com.aiosman.riderpro.ui.modifiers.noRippleClickable
+
+@Composable
+fun FollowButton(
+ isFollowing: Boolean,
+ fontSize: TextUnit = 12.sp,
+ imageModifier: Modifier = Modifier,
+ onFollowClick: () -> Unit,
+){
+ Box(
+ modifier = Modifier
+ .wrapContentWidth()
+ .padding(start = 6.dp)
+ .noRippleClickable {
+ onFollowClick()
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ modifier = imageModifier,
+ painter = painterResource(id = R.drawable.follow_bg),
+ contentDescription = "",
+ contentScale = ContentScale.FillWidth
+ )
+ Text(
+ text = if (isFollowing) stringResource(R.string.following_upper) else stringResource(
+ R.string.follow_upper
+ ),
+ fontSize = fontSize,
+ color = Color.White,
+ style = TextStyle(fontWeight = FontWeight.Bold)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/TextInputField.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/TextInputField.kt
index de547dc..16cb1b8 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/composables/TextInputField.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/TextInputField.kt
@@ -1,5 +1,9 @@
package com.aiosman.riderpro.ui.composables
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -112,16 +116,23 @@ fun TextInputField(
.height(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
- if (error != null) {
- Image(
- painter = painterResource(id = R.mipmap.rider_pro_input_error),
- contentDescription = "Error",
- modifier = Modifier.size(8.dp)
- )
- Spacer(modifier = Modifier.size(4.dp))
- Text(error, color = Color(0xFFE53935), fontSize = 12.sp)
+ AnimatedVisibility(
+ visible = error != null,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Image(
+ painter = painterResource(id = R.mipmap.rider_pro_input_error),
+ contentDescription = "Error",
+ modifier = Modifier.size(8.dp)
+ )
+ Spacer(modifier = Modifier.size(4.dp))
+ AnimatedContent(targetState = error) { targetError ->
+ Text(targetError ?: "", color = Color(0xFFE53935), fontSize = 12.sp)
+ }
+ }
}
-
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt
index e7d1b2a..511d38b 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt
@@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController
@@ -47,7 +48,8 @@ fun FavouriteListPage() {
Box(
modifier = Modifier
.fillMaxWidth()
- .weight(1f).pullRefresh(state)
+ .weight(1f)
+ .pullRefresh(state)
) {
Column(
modifier = Modifier.fillMaxSize()
@@ -57,7 +59,7 @@ fun FavouriteListPage() {
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
) {
- NoticeScreenHeader("Favourite")
+ NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false)
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeScreen.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeScreen.kt
index 16af645..af8367e 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeScreen.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeScreen.kt
@@ -30,6 +30,7 @@ fun FavouriteNoticeScreen() {
var dataFlow = model.favouriteItemsFlow
var favourites = dataFlow.collectAsLazyPagingItems()
LaunchedEffect(Unit) {
+ model.reload()
model.updateNotice()
}
StatusBarMaskLayout(
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeViewModel.kt
index 436e3df..92adfbc 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeViewModel.kt
@@ -26,8 +26,13 @@ object FavouriteNoticeViewModel : ViewModel() {
private val _favouriteItemsFlow =
MutableStateFlow>(PagingData.empty())
val favouriteItemsFlow = _favouriteItemsFlow.asStateFlow()
+ var isFirstLoad = true
- init {
+ fun reload(force: Boolean = false) {
+ if (!isFirstLoad && !force) {
+ return
+ }
+ isFirstLoad = false
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerList.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerList.kt
index 7efa5c1..c2ed905 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerList.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerList.kt
@@ -47,7 +47,11 @@ fun FollowerListScreen(userId: Int) {
isFollowing = user.isFollowing
) {
scope.launch {
- model.followUser(user.id)
+ if (user.isFollowing) {
+ model.unFollowUser(user.id)
+ } else {
+ model.followUser(user.id)
+ }
}
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerListViewModel.kt
index 46f9a0a..78aab56 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerListViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerListViewModel.kt
@@ -43,11 +43,11 @@ object FollowerListViewModel : ViewModel() {
}
}
- private fun updateIsFollow(id: Int) {
+ private fun updateIsFollow(id: Int, isFollow: Boolean = true) {
val currentPagingData = usersFlow.value
val updatedPagingData = currentPagingData.map { user ->
if (user.id == id) {
- user.copy(isFollowing = true)
+ user.copy(isFollowing = isFollow)
} else {
user
}
@@ -60,4 +60,9 @@ object FollowerListViewModel : ViewModel() {
updateIsFollow(userId)
}
+ suspend fun unFollowUser(userId: Int) {
+ userService.unFollowUser(userId.toString())
+ updateIsFollow(userId, false)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNotice.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNotice.kt
index 988f1a5..dc433cb 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNotice.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNotice.kt
@@ -25,12 +25,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems
+import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
+import com.aiosman.riderpro.ui.composables.FollowButton
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@@ -47,11 +49,14 @@ fun FollowerNoticeScreen() {
var dataFlow = model.followerItemsFlow
var followers = dataFlow.collectAsLazyPagingItems()
Box(
- modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp)
) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
}
LaunchedEffect(Unit) {
+ model.reload()
model.updateNotice()
}
LazyColumn(
@@ -114,30 +119,43 @@ fun FollowItem(
) {
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
- if (!isFollowing) {
- Box(
- modifier = Modifier.noRippleClickable {
- onFollow()
- }
- ) {
- Image(
- painter = painterResource(id = R.drawable.follow_bg),
- contentDescription = "Follow",
- modifier = Modifier
- .width(79.dp)
- .height(24.dp)
- )
- Text(
- "FOLLOW",
- fontSize = 14.sp,
- color = Color(0xFFFFFFFF),
- modifier = Modifier.align(
- Alignment.Center
- )
- )
- }
+ if (userId != AppState.UserId) {
+ FollowButton(
+ isFollowing = isFollowing,
+ onFollowClick = onFollow,
+ fontSize = 14.sp,
+ imageModifier = Modifier
+ .width(100.dp)
+ .height(24.dp)
+ )
}
+// Box(
+// modifier = Modifier.noRippleClickable {
+// onFollow()
+// }
+// ) {
+// Image(
+// painter = painterResource(id = R.drawable.follow_bg),
+// contentDescription = "Follow",
+// modifier = Modifier
+// .width(79.dp)
+// .height(24.dp)
+// )
+// Text(
+// text = if (isFollowing) {
+// stringResource(R.string.following_upper)
+// } else {
+// stringResource(R.string.follow_upper)
+// },
+// fontSize = 14.sp,
+// color = Color(0xFFFFFFFF),
+// modifier = Modifier.align(
+// Alignment.Center
+// )
+// )
+// }
+
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNoticeViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNoticeViewModel.kt
index ef252e9..cb72ba3 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNoticeViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNoticeViewModel.kt
@@ -30,8 +30,13 @@ object FollowerNoticeViewModel : ViewModel() {
private val _followerItemsFlow =
MutableStateFlow>(PagingData.empty())
val followerItemsFlow = _followerItemsFlow.asStateFlow()
+ var isFirstLoad = true
- init {
+ fun reload(force: Boolean = false) {
+ if (!isFirstLoad && !force) {
+ return
+ }
+ isFirstLoad = false
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingList.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingList.kt
index e5ce050..201170c 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingList.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingList.kt
@@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
@Composable
fun FollowingListScreen(userId: Int) {
- val model = FollowerListViewModel
+ val model = FollowingListViewModel
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
model.loadData(userId)
@@ -47,7 +47,11 @@ fun FollowingListScreen(userId: Int) {
isFollowing = user.isFollowing
) {
scope.launch {
- model.followUser(user.id)
+ if (user.isFollowing) {
+ model.unfollowUser(user.id)
+ } else {
+ model.followUser(user.id)
+ }
}
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingListViewModel.kt
index 6cc2f8e..fa25a5e 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingListViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingListViewModel.kt
@@ -24,9 +24,6 @@ object FollowingListViewModel : ViewModel() {
val usersFlow = _usersFlow.asStateFlow()
private var userId by mutableStateOf(null)
fun loadData(id: Int) {
- if (userId == id) {
- return
- }
userId = id
viewModelScope.launch {
Pager(
@@ -34,7 +31,7 @@ object FollowingListViewModel : ViewModel() {
pagingSourceFactory = {
AccountPagingSource(
userService,
- followerId = id
+ followingId = id
)
}
).flow.cachedIn(viewModelScope).collectLatest {
@@ -43,11 +40,11 @@ object FollowingListViewModel : ViewModel() {
}
}
- private fun updateIsFollow(id: Int) {
+ private fun updateIsFollow(id: Int, isFollow: Boolean = true) {
val currentPagingData = usersFlow.value
val updatedPagingData = currentPagingData.map { user ->
if (user.id == id) {
- user.copy(isFollowing = true)
+ user.copy(isFollowing = isFollow)
} else {
user
}
@@ -60,4 +57,9 @@ object FollowingListViewModel : ViewModel() {
updateIsFollow(userId)
}
+ suspend fun unfollowUser(userId: Int) {
+ userService.unFollowUser(userId.toString())
+ updateIsFollow(userId, false)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt b/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt
index 06a7f66..07e168f 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt
@@ -20,11 +20,8 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
-import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -35,30 +32,28 @@ 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.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import coil.compose.AsyncImage
-import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController
-import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.utils.File.saveImageToGallery
-import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
-@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class,
- ExperimentalMaterial3Api::class
+@OptIn(
+ ExperimentalFoundationApi::class,
)
@Composable
fun ImageViewer() {
@@ -72,8 +67,11 @@ fun ImageViewer() {
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.dp
val scope = rememberCoroutineScope()
val showRawImageStates = remember { mutableStateListOf(*Array(images.size) { false }) }
- var showBottomSheet by remember { mutableStateOf(false) }
var isDownloading by remember { mutableStateOf(false) }
+ var currentPage by remember { mutableStateOf(model.initialIndex) }
+ LaunchedEffect(pagerState) {
+ currentPage = pagerState.currentPage
+ }
StatusBarMaskLayout(
modifier = Modifier.background(Color.Black),
) {
@@ -84,7 +82,7 @@ fun ImageViewer() {
) {
HorizontalPager(
state = pagerState,
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier.fillMaxSize(),
) { page ->
val zoomState = rememberZoomState()
CustomAsyncImage(
@@ -102,6 +100,23 @@ fun ImageViewer() {
contentScale = ContentScale.Fit,
)
}
+ if (images.size > 1) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .clip(RoundedCornerShape(16.dp))
+ .background(Color(0xff333333).copy(alpha = 0.6f))
+ .padding(vertical = 4.dp, horizontal = 24.dp)
+ ) {
+ Text(
+ text = "${pagerState.currentPage + 1}/${images.size}",
+ color = Color.White,
+
+ )
+ }
+ }
+
+
Box(
modifier = Modifier
@@ -142,7 +157,7 @@ fun ImageViewer() {
modifier = Modifier.size(32.dp),
color = Color.White
)
- }else{
+ } else {
Icon(
painter = painterResource(id = R.drawable.rider_pro_download),
contentDescription = "",
@@ -153,7 +168,7 @@ fun ImageViewer() {
Spacer(modifier = Modifier.height(4.dp))
Text(
- "Download",
+ stringResource(R.string.download),
color = Color.White
)
}
@@ -174,7 +189,7 @@ fun ImageViewer() {
)
Spacer(modifier = Modifier.height(4.dp))
Text(
- "Original",
+ stringResource(R.string.original),
color = Color.White
)
}
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt
index a221b94..43aaec1 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt
@@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
@@ -43,6 +44,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope
+import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
@@ -52,6 +54,9 @@ import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
+import com.aiosman.riderpro.ui.favourite.FavouriteNoticeViewModel
+import com.aiosman.riderpro.ui.follower.FollowerNoticeViewModel
+import com.aiosman.riderpro.ui.like.LikeNoticeViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.PostViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -70,7 +75,7 @@ fun NotificationsScreen() {
var comments = dataFlow.collectAsLazyPagingItems()
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch {
- MessageListViewModel.initData()
+ MessageListViewModel.initData(force = true)
}
})
LaunchedEffect(Unit) {
@@ -81,10 +86,12 @@ fun NotificationsScreen() {
modifier = Modifier.fillMaxSize()
) {
StatusBarSpacer()
- Box(modifier = Modifier
- .fillMaxWidth()
- .weight(1f)
- .pullRefresh(state)) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .pullRefresh(state)
+ ) {
Column(
modifier = Modifier.fillMaxSize(),
) {
@@ -99,6 +106,13 @@ fun NotificationsScreen() {
R.drawable.rider_pro_like,
stringResource(R.string.like_upper)
) {
+ if (MessageListViewModel.likeNoticeCount > 0) {
+ // 刷新点赞消息列表
+ LikeNoticeViewModel.isFirstLoad = true
+ // 清除点赞消息数量
+ MessageListViewModel.clearLikeNoticeCount()
+ }
+
navController.navigate(NavigationRoute.Likes.route)
}
NotificationIndicator(
@@ -106,6 +120,11 @@ fun NotificationsScreen() {
R.drawable.rider_pro_followers,
stringResource(R.string.followers_upper)
) {
+ if (MessageListViewModel.followNoticeCount > 0) {
+ // 刷新关注消息列表
+ FollowerNoticeViewModel.isFirstLoad = true
+ MessageListViewModel.clearFollowNoticeCount()
+ }
navController.navigate(NavigationRoute.Followers.route)
}
NotificationIndicator(
@@ -113,36 +132,101 @@ fun NotificationsScreen() {
R.drawable.rider_pro_favoriate,
stringResource(R.string.favourites_upper)
) {
+ if (MessageListViewModel.favouriteNoticeCount > 0) {
+ // 刷新收藏消息列表
+ FavouriteNoticeViewModel.isFirstLoad = true
+ MessageListViewModel.clearFavouriteNoticeCount()
+ }
navController.navigate(NavigationRoute.FavouritesScreen.route)
}
}
HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp))
NotificationCounterItem(MessageListViewModel.commentNoticeCount)
- LazyColumn(
- modifier = Modifier
- .weight(1f)
- .fillMaxSize()
- ) {
- items(comments.itemCount) { index ->
- comments[index]?.let { comment ->
- CommentNoticeItem(comment) {
- MessageListViewModel.updateReadStatus(comment.id)
- MessageListViewModel.viewModelScope.launch {
-// PostViewModel.postId = comment.postId.toString()
-// PostViewModel.initData()
- navController.navigate(
- NavigationRoute.Post.route.replace(
- "{id}",
- comment.postId.toString()
- )
- )
- }
+ if (comments.loadState.refresh is LoadState.Loading) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(bottom = 48.dp)
+ ,
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Loading",
+ fontSize = 18.sp
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ LinearProgressIndicator(
+ modifier = Modifier.width(160.dp),
+ color = Color(0xFFDA3832)
+ )
+ }
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxSize()
+ ) {
+ items(comments.itemCount) { index ->
+ comments[index]?.let { comment ->
+ CommentNoticeItem(comment) {
+ MessageListViewModel.updateReadStatus(comment.id)
+ MessageListViewModel.viewModelScope.launch {
+ navController.navigate(
+ NavigationRoute.Post.route.replace(
+ "{id}",
+ comment.postId.toString()
+ )
+ )
+ }
+ }
}
}
- }
- item {
- Spacer(modifier = Modifier.height(72.dp))
+ // handle load error
+ when {
+ comments.loadState.append is LoadState.Loading -> {
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(64.dp)
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ LinearProgressIndicator(
+ modifier = Modifier.width(160.dp),
+ color = Color(0xFFDA3832)
+ )
+ }
+ }
+ }
+
+ comments.loadState.append is LoadState.Error -> {
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(64.dp)
+ .noRippleClickable {
+ comments.retry()
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Load comment error, click to retry",
+ )
+ }
+ }
+ }
+ }
+ item {
+ Spacer(modifier = Modifier.height(72.dp))
+ }
}
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt
index c442e21..bd9fee6 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt
@@ -31,9 +31,15 @@ object MessageListViewModel : ViewModel() {
private val _commentItemsFlow = MutableStateFlow>(PagingData.empty())
val commentItemsFlow = _commentItemsFlow.asStateFlow()
var isLoading by mutableStateOf(false)
-
- suspend fun initData() {
- isLoading = true
+ var isFirstLoad = true
+ suspend fun initData(force: Boolean = false) {
+ if (!isFirstLoad && !force) {
+ return
+ }
+ if (force) {
+ isLoading = true
+ }
+ isFirstLoad = false
val info = accountService.getMyNoticeInfo()
noticeInfo = info
viewModelScope.launch {
@@ -43,7 +49,7 @@ object MessageListViewModel : ViewModel() {
CommentPagingSource(
CommentRemoteDataSource(commentService),
selfNotice = true,
- order="latest"
+ order = "latest"
)
}
).flow.cachedIn(viewModelScope).collectLatest {
@@ -51,6 +57,7 @@ object MessageListViewModel : ViewModel() {
}
}
isLoading = false
+
}
val likeNoticeCount
@@ -80,4 +87,15 @@ object MessageListViewModel : ViewModel() {
updateIsRead(id)
}
}
+
+ fun clearLikeNoticeCount() {
+ noticeInfo = noticeInfo?.copy(likeCount = 0)
+ }
+
+ fun clearFollowNoticeCount() {
+ noticeInfo = noticeInfo?.copy(followCount = 0)
+ }
+ fun clearFavouriteNoticeCount() {
+ noticeInfo = noticeInfo?.copy(favoriteCount = 0)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt
index de97358..1d379d6 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt
@@ -37,26 +37,36 @@ object MyProfileViewModel : ViewModel() {
private var _momentsFlow = MutableStateFlow>(PagingData.empty())
var momentsFlow = _momentsFlow.asStateFlow()
var refreshing by mutableStateOf(false)
+ var firstLoad = true
fun loadProfile(pullRefresh: Boolean = false) {
+ if (!firstLoad && !pullRefresh) {
+ return
+ }
viewModelScope.launch {
if (pullRefresh){
refreshing = true
}
+ firstLoad = false
profile = accountService.getMyAccountProfile()
val profile = accountService.getMyAccountProfile()
refreshing = false
- Pager(
- config = PagingConfig(pageSize = 5, enablePlaceholders = false),
- pagingSourceFactory = {
- MomentPagingSource(
- MomentRemoteDataSource(momentService),
- author = profile.id
- )
+ try {
+ Pager(
+ config = PagingConfig(pageSize = 5, enablePlaceholders = false),
+ pagingSourceFactory = {
+ MomentPagingSource(
+ MomentRemoteDataSource(momentService),
+ author = profile.id
+ )
+ }
+ ).flow.cachedIn(viewModelScope).collectLatest {
+ _momentsFlow.value = it
}
- ).flow.cachedIn(viewModelScope).collectLatest {
- _momentsFlow.value = it
+ }catch (e: Exception){
+ Log.e("MyProfileViewModel", "loadProfile: ", e)
}
+
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt
index 50750de..7b482a3 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt
@@ -162,8 +162,6 @@ fun ProfilePage() {
)
}
}
-
-
Box(
modifier = Modifier
.align(Alignment.TopEnd)
@@ -174,10 +172,14 @@ fun ProfilePage() {
)
) {
Box(
- modifier = Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)).shadow(
- elevation = 20.dp
- ).background(Color.White.copy(alpha = 0.7f))
- ){
+ modifier = Modifier
+ .padding(16.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .shadow(
+ elevation = 20.dp
+ )
+ .background(Color.White.copy(alpha = 0.7f))
+ ) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
@@ -191,7 +193,7 @@ fun ProfilePage() {
com.aiosman.riderpro.ui.composables.DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
- width = 300,
+ width = 250,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
@@ -217,7 +219,7 @@ fun ProfilePage() {
}
},
MenuItem(
- "Favourite",
+ stringResource(R.string.favourites),
R.drawable.rider_pro_favourite
) {
expanded = false
@@ -227,7 +229,7 @@ fun ProfilePage() {
}
),
- )
+ )
}
}
Spacer(modifier = Modifier.height(32.dp))
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/like/LikeNoticeViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/like/LikeNoticeViewModel.kt
index 6a76b56..e82bbcd 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/like/LikeNoticeViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/like/LikeNoticeViewModel.kt
@@ -23,8 +23,12 @@ object LikeNoticeViewModel : ViewModel() {
private val accountService: AccountService = AccountServiceImpl()
private val _likeItemsFlow = MutableStateFlow>(PagingData.empty())
val likeItemsFlow = _likeItemsFlow.asStateFlow()
-
- init {
+ var isFirstLoad = true
+ fun reload(force: Boolean = false) {
+ if (!isFirstLoad && !force) {
+ return
+ }
+ isFirstLoad = false
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt
index d875630..265996f 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt
@@ -56,8 +56,10 @@ fun LikeNoticeScreen() {
var dataFlow = model.likeItemsFlow
var likes = dataFlow.collectAsLazyPagingItems()
LaunchedEffect(Unit) {
+ model.reload()
model.updateNotice()
}
+
StatusBarMaskLayout(
darkIcons = true,
maskBoxBackgroundColor = Color(0xFFFFFFFF)
@@ -125,7 +127,7 @@ fun ActionPostNoticeItem(
val context = LocalContext.current
val navController = LocalNavController.current
Box(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)
+ modifier = Modifier.padding(vertical = 16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -188,7 +190,7 @@ fun LikeCommentNoticeItem(
val navController = LocalNavController.current
val context = LocalContext.current
Box(
- modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp).noRippleClickable {
+ modifier = Modifier.padding(vertical = 16.dp).noRippleClickable {
item.comment?.postId.let {
navController.navigate(
NavigationRoute.Post.route.replace(
@@ -261,7 +263,8 @@ fun LikeCommentNoticeItem(
Text(
text = item.comment?.content ?: "",
fontSize = 12.sp,
- color = Color(0x99000000)
+ color = Color(0x99000000),
+ maxLines = 2
)
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt b/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt
index 7f16145..55d8cce 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt
@@ -53,9 +53,9 @@ import kotlinx.coroutines.launch
@Composable
fun EmailSignupScreen() {
- var email by remember { mutableStateOf("takayamaaren@gmail.com") }
- var password by remember { mutableStateOf("Dzh17217.") }
- var confirmPassword by remember { mutableStateOf("Dzh17217.") }
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var confirmPassword by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) }
var acceptTerms by remember { mutableStateOf(false) }
var acceptPromotions by remember { mutableStateOf(false) }
@@ -68,7 +68,6 @@ fun EmailSignupScreen() {
var confirmPasswordError by remember { mutableStateOf(null) }
var termsError by remember { mutableStateOf(false) }
var promotionsError by remember { mutableStateOf(false) }
-
fun validateForm(): Boolean {
emailError = when {
// 非空
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/login/userauth.kt b/app/src/main/java/com/aiosman/riderpro/ui/login/userauth.kt
index 6015312..5a1fb08 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/login/userauth.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/login/userauth.kt
@@ -93,7 +93,13 @@ fun UserAuthScreen() {
}
} catch (e: ServiceException) {
// handle error
- Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
+
+ if (e.code == 12005) {
+ emailError = context.getString(R.string.error_invalidate_username_password)
+ passwordError = context.getString(R.string.error_invalidate_username_password)
+ } else {
+ Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
+ }
}
}
@@ -181,26 +187,47 @@ fun UserAuthScreen() {
Row(
verticalAlignment = Alignment.CenterVertically
) {
- CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
- Checkbox(
- checked = rememberMe,
- onCheckedChange = {
- rememberMe = it
- },
- colors = CheckboxDefaults.colors(
- checkedColor = Color.Black
- ),
- )
- Text(
- stringResource(R.string.remember_me),
- modifier = Modifier.padding(start = 8.dp),
- fontSize = 12.sp
- )
- Spacer(modifier = Modifier.weight(1f))
- Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable {
+ com.aiosman.riderpro.ui.composables.Checkbox(
+ checked = rememberMe,
+ onCheckedChange = {
+ rememberMe = it
+ },
+ size = 18
+ )
+ Text(
+ stringResource(R.string.remember_me),
+ modifier = Modifier.padding(start = 8.dp),
+ fontSize = 12.sp
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ stringResource(R.string.forgot_password),
+ fontSize = 12.sp,
+ modifier = Modifier.noRippleClickable {
navController.navigate(NavigationRoute.ResetPassword.route)
- })
- }
+ }
+ )
+// CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
+// Checkbox(
+// checked = rememberMe,
+// onCheckedChange = {
+// rememberMe = it
+// },
+// colors = CheckboxDefaults.colors(
+// checkedColor = Color.Black
+// ),
+// )
+// Text(
+// stringResource(R.string.remember_me),
+// modifier = Modifier.padding(start = 8.dp),
+// fontSize = 12.sp
+// )
+// Spacer(modifier = Modifier.weight(1f))
+// Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable {
+// navController.navigate(NavigationRoute.ResetPassword.route)
+// })
+// }
+
}
Spacer(modifier = Modifier.height(64.dp))
ActionButton(
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt
index eb8d546..0f59d4d 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt
@@ -29,6 +29,7 @@ class CommentsViewModel(
val commentsFlow = _commentsFlow.asStateFlow()
var order: String by mutableStateOf("like")
var addedCommentList by mutableStateOf>(emptyList())
+ var subCommentLoadingMap by mutableStateOf(mutableMapOf())
/**
* 预加载,在跳转到 PostScreen 之前设置好内容
@@ -49,15 +50,19 @@ class CommentsViewModel(
fun reloadComment() {
viewModelScope.launch {
- Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
- pagingSourceFactory = {
- CommentPagingSource(
- CommentRemoteDataSource(commentService),
- postId = postId.toInt(),
- order = order
- )
- }).flow.cachedIn(viewModelScope).collectLatest {
- _commentsFlow.value = it
+ try {
+ Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
+ pagingSourceFactory = {
+ CommentPagingSource(
+ CommentRemoteDataSource(commentService),
+ postId = postId.toInt(),
+ order = order
+ )
+ }).flow.cachedIn(viewModelScope).collectLatest {
+ _commentsFlow.value = it
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
}
}
}
@@ -144,7 +149,12 @@ class CommentsViewModel(
fun deleteComment(commentId: Int) {
viewModelScope.launch {
commentService.DeleteComment(commentId)
- reloadComment()
+ // 如果是刚刚创建的评论,则从addedCommentList中删除
+ if (addedCommentList.any { it.id == commentId }) {
+ addedCommentList = addedCommentList.filter { it.id != commentId }
+ } else {
+ reloadComment()
+ }
}
}
@@ -153,16 +163,23 @@ class CommentsViewModel(
val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) {
- val subCommentList = commentService.getComments(
- postId = postId.toInt(),
- parentCommentId = commentId,
- pageNumber = comment.replyPage + 1,
- pageSize = 3,
- ).list
- return@map comment.copy(
- reply = comment.reply.plus(subCommentList),
- replyPage = comment.replyPage + 1
- )
+ try {
+ subCommentLoadingMap[commentId] = true
+ val subCommentList = commentService.getComments(
+ postId = postId.toInt(),
+ parentCommentId = commentId,
+ pageNumber = comment.replyPage + 1,
+ pageSize = 3,
+ ).list
+ return@map comment.copy(
+ reply = comment.reply.plus(subCommentList),
+ replyPage = comment.replyPage + 1
+ )
+ } catch (e: Exception) {
+ return@map comment.copy()
+ } finally {
+ subCommentLoadingMap[commentId] = false
+ }
}
comment
}
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..8cca3d9 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,11 @@
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.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -14,10 +13,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 +24,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
@@ -41,7 +43,9 @@ 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 +54,19 @@ 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 java.io.File
@Preview
@@ -76,7 +82,9 @@ fun NewPostScreen() {
}
StatusBarMaskLayout(
darkIcons = true,
- modifier = Modifier.fillMaxSize().background(Color.White)
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
) {
Column(
modifier = Modifier
@@ -94,14 +102,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 +201,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
}
}
}
+
@Composable
fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Unit) {
@@ -211,7 +225,8 @@ fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Uni
}
}
-@OptIn(ExperimentalLayoutApi::class)
+
+
@Composable
fun AddImageGrid() {
val navController = LocalNavController.current
@@ -226,44 +241,97 @@ fun AddImageGrid() {
}
}
+ val takePictureLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.TakePicture()
+ ) { success ->
+ if (success) {
+ model.imageUriList += model.currentPhotoUri.toString()
+ }
+ }
+
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 +343,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
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt
index e92b84e..40a0a41 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt
@@ -1,7 +1,9 @@
package com.aiosman.riderpro.ui.post
-import android.util.Log
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -14,7 +16,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -24,15 +25,11 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.ClickableText
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
@@ -46,22 +43,23 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
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.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -69,7 +67,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.paging.compose.LazyPagingItems
+import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalAnimatedContentScope
@@ -82,12 +80,14 @@ import com.aiosman.riderpro.entity.MomentImageEntity
import com.aiosman.riderpro.exp.formatPostTime
import com.aiosman.riderpro.exp.timeAgo
import com.aiosman.riderpro.ui.NavigationRoute
-import com.aiosman.riderpro.ui.comment.CommentModalViewModel
+import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
+import com.aiosman.riderpro.ui.composables.CustomClickableText
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal
+import com.aiosman.riderpro.ui.composables.FollowButton
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
@@ -113,28 +113,67 @@ fun PostScreen(
var contextComment by remember { mutableStateOf(null) }
var replyComment by remember { mutableStateOf(null) }
var showCommentModal by remember { mutableStateOf(false) }
+ var commentModalState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ var editCommentModalState = rememberModalBottomSheetState(
+ skipPartiallyExpanded = true
+ )
LaunchedEffect(Unit) {
viewModel.initData()
}
+
if (showCommentMenu) {
ModalBottomSheet(
onDismissRequest = {
showCommentMenu = false
},
containerColor = Color.White,
- sheetState = rememberModalBottomSheetState(
- skipPartiallyExpanded = true
- ),
+ sheetState = commentModalState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
CommentMenuModal(
onDeleteClick = {
- showCommentMenu = false
+ scope.launch {
+ commentModalState.hide()
+ showCommentMenu = false
+ }
contextComment?.let {
viewModel.deleteComment(it.id)
}
+
+ },
+ commentEntity = contextComment,
+ onCloseClick = {
+ scope.launch {
+ commentModalState.hide()
+ showCommentMenu = false
+ }
+ },
+ isSelf = AppState.UserId?.toLong() == contextComment?.author,
+ onLikeClick = {
+ scope.launch {
+ commentModalState.hide()
+ showCommentMenu = false
+ }
+ contextComment?.let {
+ viewModel.viewModelScope.launch {
+ if (it.liked) {
+ viewModel.unlikeComment(it.id)
+ } else {
+ viewModel.likeComment(it.id)
+ }
+ }
+ }
+
+ },
+ onReplyClick = {
+ scope.launch {
+ commentModalState.hide()
+ showCommentMenu = false
+ replyComment = contextComment
+ showCommentModal = true
+ }
}
)
}
@@ -177,6 +216,8 @@ fun PostScreen(
content = it
)
}
+
+ editCommentModalState.hide()
showCommentModal = false
}
@@ -186,31 +227,34 @@ fun PostScreen(
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
- PostBottomBar(
- onLikeClick = {
- scope.launch {
- if (viewModel.moment?.liked == true) {
- viewModel.dislikeMoment()
- } else {
- viewModel.likeMoment()
+ if (!viewModel.isError) {
+ PostBottomBar(
+ onLikeClick = {
+ scope.launch {
+ if (viewModel.moment?.liked == true) {
+ viewModel.dislikeMoment()
+ } else {
+ viewModel.likeMoment()
+ }
}
- }
- },
- onCreateCommentClick = {
- replyComment = null
- showCommentModal = true
- },
- onFavoriteClick = {
- scope.launch {
- if (viewModel.moment?.isFavorite == true) {
- viewModel.unfavoriteMoment()
- } else {
- viewModel.favoriteMoment()
+ },
+ onCreateCommentClick = {
+ replyComment = null
+ showCommentModal = true
+ },
+ onFavoriteClick = {
+ scope.launch {
+ if (viewModel.moment?.isFavorite == true) {
+ viewModel.unfavoriteMoment()
+ } else {
+ viewModel.favoriteMoment()
+ }
}
- }
- },
- momentEntity = viewModel.moment
- )
+ },
+ momentEntity = viewModel.moment
+ )
+ }
+
}
) {
it
@@ -220,96 +264,119 @@ fun PostScreen(
.background(Color.White)
) {
StatusBarSpacer()
- Header(
- avatar = viewModel.avatar,
- nickname = viewModel.nickname,
- userId = viewModel.moment?.authorId,
- isFollowing = viewModel.accountProfileEntity?.isFollowing ?: false,
- onFollowClick = {
- scope.launch {
- if (viewModel.accountProfileEntity?.isFollowing == true) {
- viewModel.unfollowUser()
- } else {
- viewModel.followUser()
- }
- }
- },
- onDeleteClick = {
- viewModel.deleteMoment {
- navController.popBackStack()
- }
+ if (viewModel.isError) {
+ Box(
+ modifier = Modifier.fillMaxWidth().padding(16.dp)
+ ) {
+ NoticeScreenHeader("Post", moreIcon = false)
}
- )
- LazyColumn(
- modifier = Modifier
- .fillMaxWidth()
- .weight(1f)
- ) {
- item {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(383f / 527f)
- ) {
- PostImageView(
- viewModel.moment?.images ?: emptyList()
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Umm, post are not found.",
+ style = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
)
-
- }
- PostDetails(
- viewModel.moment
- )
- Spacer(modifier = Modifier.height(16.dp))
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp)
- .height(1.dp)
- .background(Color(0xFFF7F7F7))
- ) {
-
- }
- Spacer(modifier = Modifier.height(24.dp))
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = stringResource(
- R.string.comment_count,
- (viewModel.moment?.commentCount ?: 0)
- ), fontSize = 14.sp
- )
- Spacer(modifier = Modifier.weight(1f))
- OrderSelectionComponent() {
- commentsViewModel.order = it
- viewModel.reloadComment()
- }
- }
-
- Spacer(modifier = Modifier.height(16.dp))
- }
- item {
- CommentContent(
- viewModel = commentsViewModel,
- onLongClick = {
- showCommentMenu = true
- contextComment = it
- },
- onReply = { parentComment, _, _, _ ->
- replyComment = parentComment
- showCommentModal = true
- }
)
}
- item {
- Spacer(modifier = Modifier.height(120.dp))
- }
+ }else{
+ Header(
+ avatar = viewModel.avatar,
+ nickname = viewModel.nickname,
+ userId = viewModel.moment?.authorId,
+ isFollowing = viewModel.moment?.followStatus == true,
+ onFollowClick = {
+ scope.launch {
+ if (viewModel.moment?.followStatus == true) {
+ viewModel.unfollowUser()
+ } else {
+ viewModel.followUser()
+ }
+ }
+ },
+ onDeleteClick = {
+ viewModel.deleteMoment {
+ navController.popBackStack()
+ }
+ }
+ )
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ ) {
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(383f / 527f)
+ ) {
+ PostImageView(
+ viewModel.moment?.images ?: emptyList()
+ )
+ }
+ PostDetails(
+ viewModel.moment
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .height(1.dp)
+ .background(Color(0xFFF7F7F7))
+ ) {
+
+ }
+ Spacer(modifier = Modifier.height(24.dp))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(
+ R.string.comment_count,
+ (viewModel.moment?.commentCount ?: 0)
+ ), fontSize = 14.sp
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ OrderSelectionComponent() {
+ commentsViewModel.order = it
+ viewModel.reloadComment()
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ item {
+ CommentContent(
+ viewModel = commentsViewModel,
+ onLongClick = { comment ->
+ showCommentMenu = true
+ contextComment = comment
+ },
+ onReply = { parentComment, _, _, _ ->
+ replyComment = parentComment
+ showCommentModal = true
+ }
+ )
+ }
+ item {
+ Spacer(modifier = Modifier.height(120.dp))
+ }
+
+ }
}
+
}
}
@@ -327,41 +394,43 @@ fun CommentContent(
}
for (item in addedTopLevelComment) {
- Box(
- modifier = Modifier.padding(horizontal = 16.dp)
+ AnimatedVisibility(
+ visible = true,
+ enter = fadeIn() + slideInVertically()
) {
- CommentItem(
- item,
- onLike = { comment ->
- viewModel.viewModelScope.launch {
- if (comment.liked) {
- viewModel.unlikeComment(comment.id)
- } else {
- viewModel.likeComment(comment.id)
+ Box(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ CommentItem(
+ item,
+ onLike = { comment ->
+ viewModel.viewModelScope.launch {
+ if (comment.liked) {
+ viewModel.unlikeComment(comment.id)
+ } else {
+ viewModel.likeComment(comment.id)
+ }
}
- }
- },
- onLongClick = {
- if (AppState.UserId != item.id) {
- return@CommentItem
- }
- onLongClick(item)
- },
- onReply = { parentComment, _, _, _ ->
- onReply(
- parentComment,
- parentComment.author,
- parentComment.name,
- parentComment.avatar
- )
- },
- onLoadMoreSubComments = {
- viewModel.viewModelScope.launch {
- viewModel.loadMoreSubComments(it.id)
- }
- },
- addedCommentList = viewModel.addedCommentList
- )
+ },
+ onLongClick = { comment ->
+ onLongClick(comment)
+ },
+ onReply = { parentComment, _, _, _ ->
+ onReply(
+ parentComment,
+ parentComment.author,
+ parentComment.name,
+ parentComment.avatar
+ )
+ },
+ onLoadMoreSubComments = {
+ viewModel.viewModelScope.launch {
+ viewModel.loadMoreSubComments(it.id)
+ }
+ },
+ addedCommentList = viewModel.addedCommentList
+ )
+ }
}
}
@@ -381,11 +450,8 @@ fun CommentContent(
}
}
},
- onLongClick = {
- if (AppState.UserId != item.id) {
- return@CommentItem
- }
- onLongClick(item)
+ onLongClick = { comment ->
+ onLongClick(comment)
},
onReply = { parentComment, _, _, _ ->
onReply(
@@ -404,6 +470,67 @@ fun CommentContent(
)
}
}
+ if (commentsPagging.loadState.refresh is LoadState.Loading) {
+ Box(
+ modifier = Modifier.fillMaxSize().height(120.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ LinearProgressIndicator(
+ modifier = Modifier.width(160.dp),
+ color = Color(0xFFDA3832)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "Loading...",
+ fontSize = 14.sp
+ )
+ }
+
+ }
+ return
+ }
+ if (commentsPagging.loadState.append is LoadState.Loading) {
+ Box(
+ modifier = Modifier.fillMaxSize().height(64.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ LinearProgressIndicator(
+ modifier = Modifier.width(160.dp),
+ color = Color(0xFFDA3832)
+ )
+ }
+ }
+ if (commentsPagging.loadState.refresh is LoadState.Error) {
+ Box(
+ modifier = Modifier.fillMaxSize().height(120.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Failed to load comments,click to retry",
+ fontSize = 14.sp,
+ modifier = Modifier.noRippleClickable {
+ viewModel.reloadComment()
+ }
+ )
+ }
+ }
+ if (commentsPagging.loadState.append is LoadState.Error) {
+ Box(
+ modifier = Modifier.fillMaxSize().height(64.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Failed to load more comments,click to retry",
+ fontSize = 14.sp,
+ modifier = Modifier.noRippleClickable {
+ commentsPagging.retry()
+ }
+ )
+ }
+ }
}
@@ -488,30 +615,12 @@ fun Header(
Spacer(modifier = Modifier.width(8.dp))
Text(text = nickname ?: "", fontWeight = FontWeight.Bold)
if (AppState.UserId != userId) {
- Box(
- modifier = Modifier
- .height(20.dp)
- .wrapContentWidth()
- .padding(start = 6.dp)
- .noRippleClickable {
- onFollowClick()
- },
- contentAlignment = Alignment.Center
- ) {
- Image(
- modifier = Modifier.height(18.dp),
- painter = painterResource(id = R.drawable.follow_bg),
- contentDescription = ""
- )
- Text(
- text = if (isFollowing) stringResource(R.string.following_upper) else stringResource(
- R.string.follow_upper
- ),
- fontSize = 12.sp,
- color = Color.White,
- style = TextStyle(fontWeight = FontWeight.Bold)
- )
- }
+ FollowButton(
+ isFollowing = isFollowing,
+ onFollowClick = onFollowClick,
+ imageModifier = Modifier.height(18.dp).width(80.dp),
+ fontSize = 12.sp
+ )
}
if (AppState.UserId == userId) {
Spacer(modifier = Modifier.weight(1f))
@@ -581,7 +690,7 @@ fun PostImageView(
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
- if(images.size > 1){
+ if (images.size > 1) {
images.forEachIndexed { index, _ ->
Box(
modifier = Modifier
@@ -626,47 +735,6 @@ fun PostDetails(
}
}
-@Composable
-fun CommentsSection(
- lazyPagingItems: LazyPagingItems,
- scrollState: LazyListState = rememberLazyListState(),
- onLike: (CommentEntity) -> Unit,
- onLongClick: (CommentEntity) -> Unit,
- onWillCollapse: (Boolean) -> Unit,
-) {
- LazyColumn(
- state = scrollState, modifier = Modifier
- .fillMaxHeight()
- .padding(start = 16.dp, end = 16.dp)
- ) {
- items(lazyPagingItems.itemCount) { idx ->
- val item = lazyPagingItems[idx] ?: return@items
- CommentItem(
- item,
- onLike = {
- onLike(item)
- },
- onLongClick = {
- onLongClick(item)
- }
- )
- }
- }
-
- // Detect scroll direction and update showCollapseContent
- val coroutineScope = rememberCoroutineScope()
- LaunchedEffect(scrollState) {
- coroutineScope.launch {
- snapshotFlow { scrollState.firstVisibleItemScrollOffset }
- .collect { offset ->
- Log.d("scroll", "offset: $offset")
- onWillCollapse(offset == 0)
- }
- }
- }
-}
-
-
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CommentItem(
@@ -680,7 +748,7 @@ fun CommentItem(
replyUserAvatar: String?
) -> Unit = { _, _, _, _ -> },
onLoadMoreSubComments: ((CommentEntity) -> Unit)? = {},
- onLongClick: () -> Unit = {},
+ onLongClick: (CommentEntity) -> Unit = {},
addedCommentList: List = emptyList()
) {
val context = LocalContext.current
@@ -688,12 +756,6 @@ fun CommentItem(
Column(
modifier = Modifier
.fillMaxWidth()
- .combinedClickable(
- indication = null,
- interactionSource = remember { MutableInteractionSource() },
- onClick = {},
- onLongClick = onLongClick
- )
) {
Row(modifier = Modifier.padding(vertical = 8.dp)) {
Box(
@@ -719,7 +781,15 @@ fun CommentItem(
}
Spacer(modifier = Modifier.width(8.dp))
Column(
- modifier = Modifier.weight(1f)
+ modifier = Modifier
+ .weight(1f)
+ .combinedClickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onLongClick = {
+ onLongClick(commentEntity)
+ }
+ ) {}
) {
Text(text = commentEntity.name, fontWeight = FontWeight.W600, fontSize = 14.sp)
Row {
@@ -741,33 +811,49 @@ fun CommentItem(
pop()
}
append(" ${commentEntity.comment}")
+
}
- ClickableText(
- text = annotatedText,
- onClick = { offset ->
- annotatedText.getStringAnnotations(
- tag = "replyUser",
- start = offset,
- end = offset
- ).firstOrNull()?.let {
- navController.navigate(
- NavigationRoute.AccountProfile.route.replace(
- "{id}",
- it.item
+ Box {
+ CustomClickableText(
+ text = annotatedText,
+ onClick = { offset ->
+ annotatedText.getStringAnnotations(
+ tag = "replyUser",
+ start = offset,
+ end = offset
+ ).firstOrNull()?.let {
+ navController.navigate(
+ NavigationRoute.AccountProfile.route.replace(
+ "{id}",
+ it.item
+ )
)
- )
- }
- },
- style = TextStyle(fontSize = 14.sp),
- maxLines = Int.MAX_VALUE,
- softWrap = true
- )
+ }
+ },
+ style = TextStyle(fontSize = 14.sp),
+ onLongPress = {
+ onLongClick(commentEntity)
+ },
+ )
+ }
+
} else {
Text(
text = commentEntity.comment,
fontSize = 14.sp,
maxLines = Int.MAX_VALUE,
- softWrap = true
+ softWrap = true,
+ modifier = Modifier.combinedClickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onLongClick = {
+ onLongClick(
+ commentEntity
+ )
+ },
+ ) {
+
+ }
)
}
}
@@ -780,7 +866,7 @@ fun CommentItem(
Spacer(modifier = Modifier.width(8.dp))
if (AppState.UserId?.toLong() != commentEntity.author) {
Text(
- text = "Reply",
+ text = stringResource(R.string.reply),
fontSize = 12.sp,
color = Color.Gray,
modifier = Modifier.noRippleClickable {
@@ -832,13 +918,15 @@ fun CommentItem(
isChild = true,
onLike = onLike,
onReply = onReply,
- onLongClick = onLongClick
+ onLongClick = { comment ->
+ onLongClick(comment)
+ }
)
}
if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) {
val remaining = commentEntity.replyCount - commentEntity.reply.size
Text(
- text = "View $remaining more replies",
+ text = stringResource(R.string.view_more_reply, remaining),
fontSize = 12.sp,
color = Color(0xFF6F94AE),
modifier = Modifier.noRippleClickable {
@@ -851,7 +939,6 @@ fun CommentItem(
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PostBottomBar(
onCreateCommentClick: () -> Unit = {},
@@ -956,12 +1043,16 @@ fun PostMenuModal(
onDeleteClick()
}
) {
- Image(painter = painterResource(id = R.drawable.rider_pro_moment_delete), contentDescription = "",modifier = Modifier.size(24.dp))
+ Image(
+ painter = painterResource(id = R.drawable.rider_pro_moment_delete),
+ contentDescription = "",
+ modifier = Modifier.size(24.dp)
+ )
}
Spacer(modifier = Modifier.height(8.dp))
Text(
- text = "Delete",
+ text = stringResource(R.string.delete),
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
@@ -971,43 +1062,165 @@ fun PostMenuModal(
}
@Composable
-fun CommentMenuModal(
- onDeleteClick: () -> Unit = {}
+fun MenuActionItem(
+ icon: Int? = null,
+ text: String,
+ content: @Composable() (() -> Unit)? = null,
+ onClick: () -> Unit
) {
Column(
- modifier = Modifier
- .fillMaxWidth()
- .height(160.dp)
- .padding(vertical = 47.dp, horizontal = 20.dp)
+ modifier = Modifier,
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- Row(
+ Box(
modifier = Modifier
- .fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(
- modifier = Modifier,
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Box(
- modifier = Modifier
- .clip(CircleShape)
- .noRippleClickable {
- onDeleteClick()
- }
- ) {
- Image(painter = painterResource(id = R.drawable.rider_pro_moment_delete), contentDescription = "",modifier = Modifier.size(24.dp))
+ .clip(CircleShape)
+ .noRippleClickable {
+ onClick()
}
-
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = "Delete",
- fontSize = 11.sp,
- fontWeight = FontWeight.Bold
+ ) {
+ content?.invoke()
+ if (icon != null) {
+ Icon(
+ painter = painterResource(id = icon),
+ contentDescription = "",
+ modifier = Modifier.size(24.dp),
+ tint = Color.Black
)
}
}
+
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = text,
+ fontSize = 11.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+}
+
+/**
+ * 评论菜单弹窗
+ */
+@Composable
+fun CommentMenuModal(
+ onDeleteClick: () -> Unit = {},
+ commentEntity: CommentEntity? = null,
+ onCloseClick: () -> Unit = {},
+ onLikeClick: () -> Unit = {},
+ onReplyClick: () -> Unit = {},
+ isSelf: Boolean = false
+) {
+ val clipboard = LocalClipboardManager.current
+
+ fun copyToClipboard() {
+ commentEntity?.let {
+ clipboard.setText(
+ AnnotatedString(
+ text = it.comment,
+ )
+ )
+ }
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 24.dp, horizontal = 20.dp)
+ ) {
+ Text(stringResource(R.string.comment), fontSize = 18.sp, fontWeight = FontWeight.Bold)
+ Spacer(modifier = Modifier.height(24.dp))
+ commentEntity?.let {
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(8.dp))
+ .background(Color(0xffeeeeee))
+
+ .padding(8.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+
+ ) {
+ Box(
+ modifier = Modifier
+ .size(24.dp)
+ .clip(CircleShape)
+ ) {
+ CustomAsyncImage(
+ imageUrl = it.avatar,
+ modifier = Modifier.fillMaxSize(),
+ contentDescription = "Avatar",
+ )
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ androidx.compose.material.Text(
+ it.name,
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ )
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ androidx.compose.material.Text(
+ it.comment,
+ maxLines = 1,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 32.dp),
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ Spacer(modifier = Modifier.height(32.dp))
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (isSelf) {
+ MenuActionItem(
+ icon = R.drawable.rider_pro_moment_delete,
+ text = stringResource(R.string.delete)
+ ) {
+ onDeleteClick()
+ }
+
+ Spacer(modifier = Modifier.width(48.dp))
+ }
+ MenuActionItem(
+ icon = R.drawable.rider_pro_copy,
+ text = stringResource(R.string.copy)
+ ) {
+ copyToClipboard()
+ onCloseClick()
+ }
+ commentEntity?.let {
+ Spacer(modifier = Modifier.width(48.dp))
+ MenuActionItem(
+ text = stringResource(R.string.like),
+ content = {
+ AnimatedLikeIcon(
+ liked = it.liked,
+ onClick = onLikeClick,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ ) {
+ onCloseClick()
+ }
+ }
+ if (!isSelf) {
+ Spacer(modifier = Modifier.width(48.dp))
+ MenuActionItem(
+ icon = R.drawable.rider_pro_comment,
+ text = stringResource(R.string.reply)
+ ) {
+ onReplyClick()
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(48.dp))
}
}
diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt
index 4dd3eb0..1f537de 100644
--- a/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt
+++ b/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt
@@ -28,24 +28,20 @@ class PostViewModel(
var moment by mutableStateOf(null)
var accountService: AccountService = AccountServiceImpl()
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
+ var isError by mutableStateOf(false)
- /**
- * 预加载,在跳转到 PostScreen 之前设置好内容
- */
- fun preTransit(momentEntity: MomentEntity?) {
- this.moment = momentEntity
- this.nickname = momentEntity?.nickname ?: ""
- this.commentsViewModel = CommentsViewModel(postId)
- commentsViewModel.preTransit()
- }
fun reloadComment() {
commentsViewModel.reloadComment()
}
suspend fun initData() {
- moment = service.getMomentById(postId.toInt())
-// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString())
+ try {
+ moment = service.getMomentById(postId.toInt())
+ } catch (e: Exception) {
+ isError = true
+ return
+ }
commentsViewModel.reloadComment()
}
@@ -106,16 +102,16 @@ class PostViewModel(
}
suspend fun followUser() {
- accountProfileEntity?.let {
- userService.followUser(it.id.toString())
- accountProfileEntity = accountProfileEntity?.copy(isFollowing = true)
+ moment?.let {
+ userService.followUser(it.authorId.toString())
+ moment = moment?.copy(followStatus = true)
}
}
suspend fun unfollowUser() {
- accountProfileEntity?.let {
- userService.unFollowUser(it.id.toString())
- accountProfileEntity = accountProfileEntity?.copy(isFollowing = false)
+ moment?.let {
+ userService.unFollowUser(it.authorId.toString())
+ moment = moment?.copy(followStatus = false)
}
}
diff --git a/app/src/main/res/drawable/rider_pro_copy.xml b/app/src/main/res/drawable/rider_pro_copy.xml
new file mode 100644
index 0000000..fb84e00
--- /dev/null
+++ b/app/src/main/res/drawable/rider_pro_copy.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rider_pro_send_disable.xml b/app/src/main/res/drawable/rider_pro_send_disable.xml
new file mode 100644
index 0000000..2e25ffc
--- /dev/null
+++ b/app/src/main/res/drawable/rider_pro_send_disable.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 2c7e47f..8de99af 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -57,4 +57,17 @@
默认
最新
最早
+ 下载
+ 原始图片
+ 收藏
+ 删除
+ 复制
+ 点赞
+ 回复
+ 查看更多%1d条回复
+ 错误的用户名或密码
+ 找回密码
+ 找回
+ 邮件已发送!请查收您的邮箱,按照邮件中的指示重置密码。
+ 邮件发送失败,请检查您的网络连接或稍后重试。
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9e790b6..29f2fe3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -56,4 +56,17 @@
Default
Latest
Earliest
+ Download
+ Original
+ Favourite
+ Delete
+ Copy
+ Like
+ Reply
+ View %1d more replies
+ Invalid email or password
+ RCOVER ACCOUNT
+ Recover
+ An email has been sent to your registered email address. Please check your inbox and follow the instructions to reset your password.
+ Failed to send email. Please check your network connection or try again later.
\ No newline at end of file