Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
2024-09-16 03:43:27 +08:00
42 changed files with 1562 additions and 546 deletions

View File

@@ -101,6 +101,5 @@ dependencies {
implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-perf") implementation("com.google.firebase:firebase-perf")
implementation("com.google.firebase:firebase-messaging-ktx") implementation("com.google.firebase:firebase-messaging-ktx")
} }

View File

@@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -16,7 +18,8 @@
android:theme="@style/Theme.RiderPro" android:theme="@style/Theme.RiderPro"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<meta-data android:name="com.google.android.geo.API_KEY" <meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4" /> android:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4" />
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_icon" android:name="com.google.firebase.messaging.default_notification_icon"
@@ -49,6 +52,17 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.aiosman.riderpro.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@@ -101,9 +101,15 @@ class MainActivity : ComponentActivity() {
val postId = intent.getStringExtra("POST_ID") val postId = intent.getStringExtra("POST_ID")
if (postId != null) { if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId") Log.d("MainActivity", "Navigation to Post$postId")
navController.navigate(NavigationRoute.Post.route.replace("{id}", postId)) navController.navigate(
NavigationRoute.Post.route.replace(
"{id}",
postId
)
)
} }
} }
} }
} }

View File

@@ -460,11 +460,17 @@ class AccountServiceImpl : AccountService {
} }
override suspend fun resetPassword(email: String) { override suspend fun resetPassword(email: String) {
ApiClient.api.resetPassword( val resp = ApiClient.api.resetPassword(
ResetPasswordRequestBody( ResetPasswordRequestBody(
username = email username = email
) )
) )
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to reset password")
}
} }
} }

View File

@@ -155,7 +155,8 @@ class CommentRemoteDataSource(
postUser: Int?, postUser: Int?,
selfNotice: Boolean?, selfNotice: Boolean?,
order: String?, order: String?,
parentCommentId: Int? parentCommentId: Int?,
pageSize: Int? = 20
): ListContainer<CommentEntity> { ): ListContainer<CommentEntity> {
return commentService.getComments( return commentService.getComments(
pageNumber, pageNumber,
@@ -163,7 +164,8 @@ class CommentRemoteDataSource(
postUser = postUser, postUser = postUser,
selfNotice = selfNotice, selfNotice = selfNotice,
order = order, order = order,
parentCommentId = parentCommentId parentCommentId = parentCommentId,
pageSize = pageSize
) )
} }
} }

View File

@@ -29,7 +29,9 @@ data class Moment(
@SerializedName("commentCount") @SerializedName("commentCount")
val commentCount: Long, val commentCount: Long,
@SerializedName("time") @SerializedName("time")
val time: String val time: String,
@SerializedName("isFollowed")
val isFollowed: Boolean,
) { ) {
fun toMomentItem(): MomentEntity { fun toMomentItem(): MomentEntity {
return MomentEntity( return MomentEntity(
@@ -38,7 +40,7 @@ data class Moment(
nickname = user.nickName, nickname = user.nickName,
location = "Worldwide", location = "Worldwide",
time = ApiClient.dateFromApiString(time), time = ApiClient.dateFromApiString(time),
followStatus = false, followStatus = isFollowed,
momentTextContent = textContent, momentTextContent = textContent,
momentPicture = R.drawable.default_moment_img, momentPicture = R.drawable.default_moment_img,
likeCount = likeCount.toInt(), likeCount = likeCount.toInt(),

View File

@@ -46,14 +46,15 @@ class CommentPagingSource(
postUser = postUser, postUser = postUser,
selfNotice = selfNotice, selfNotice = selfNotice,
order = order, order = order,
parentCommentId = parentCommentId parentCommentId = parentCommentId,
pageSize = params.loadSize
) )
LoadResult.Page( LoadResult.Page(
data = comments.list, data = comments.list,
prevKey = if (currentPage == 1) null else currentPage - 1, prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (comments.list.isEmpty()) null else comments.page + 1 nextKey = if (comments.list.isEmpty()) null else comments.page + 1
) )
} catch (exception: IOException) { } catch (exception: Exception) {
return LoadResult.Error(exception) return LoadResult.Error(exception)
} }
} }

View File

@@ -8,6 +8,7 @@ import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.ServiceException import com.aiosman.riderpro.data.ServiceException
import com.aiosman.riderpro.data.UploadImage import com.aiosman.riderpro.data.UploadImage
import com.aiosman.riderpro.data.api.ApiClient import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.data.parseErrorResponse
import com.aiosman.riderpro.entity.MomentEntity import com.aiosman.riderpro.entity.MomentEntity
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
@@ -165,8 +166,13 @@ class MomentBackend {
suspend fun getMomentById(id: Int): MomentEntity { suspend fun getMomentById(id: Int): MomentEntity {
var resp = ApiClient.api.getPost(id) var resp = ApiClient.api.getPost(id)
var body = resp.body()?.data ?: throw ServiceException("Failed to get moment") if (!resp.isSuccessful) {
return body.toMomentItem() 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) { suspend fun likeMoment(id: Int) {

View File

@@ -49,8 +49,17 @@ fun ResetPasswordScreen() {
var isSendSuccess by remember { mutableStateOf<Boolean?>(null) } var isSendSuccess by remember { mutableStateOf<Boolean?>(null) }
var isLoading by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(false) }
val navController = LocalNavController.current val navController = LocalNavController.current
var usernameError by remember { mutableStateOf<String?>(null) }
fun validate(): Boolean {
if (username.isEmpty()) {
usernameError = context.getString(R.string.text_error_email_required)
return false
}
usernameError = null
return true
}
fun resetPassword() { fun resetPassword() {
if (!validate()) return
scope.launch { scope.launch {
isLoading = true isLoading = true
try { try {
@@ -78,7 +87,7 @@ fun ResetPasswordScreen() {
) )
) { ) {
NoticeScreenHeader( NoticeScreenHeader(
"RECOVER ACCOUNT", stringResource(R.string.recover_account_upper),
moreIcon = false moreIcon = false
) )
} }
@@ -93,7 +102,7 @@ fun ResetPasswordScreen() {
if (isSendSuccess!!) { if (isSendSuccess!!) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "Reset password email has been sent to your email address", text = stringResource(R.string.reset_mail_send_success),
style = TextStyle( style = TextStyle(
color = Color(0xFF333333), color = Color(0xFF333333),
fontSize = 14.sp, fontSize = 14.sp,
@@ -103,7 +112,7 @@ fun ResetPasswordScreen() {
} else { } else {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "Failed to send reset password email", text = stringResource(R.string.reset_mail_send_failed),
style = TextStyle( style = TextStyle(
color = Color(0xFF333333), color = Color(0xFF333333),
fontSize = 14.sp, fontSize = 14.sp,
@@ -138,7 +147,8 @@ fun ResetPasswordScreen() {
onValueChange = { username = it }, onValueChange = { username = it },
label = stringResource(R.string.login_email_label), label = stringResource(R.string.login_email_label),
hint = stringResource(R.string.text_hint_email), hint = stringResource(R.string.text_hint_email),
enabled = !isLoading enabled = !isLoading,
error = usernameError
) )
Spacer(modifier = Modifier.height(72.dp)) Spacer(modifier = Modifier.height(72.dp))
if (isLoading) { if (isLoading) {
@@ -148,7 +158,7 @@ fun ResetPasswordScreen() {
modifier = Modifier modifier = Modifier
.width(345.dp) .width(345.dp)
.height(48.dp), .height(48.dp),
text = "Recover Account", text = stringResource(R.string.recover),
backgroundImage = R.mipmap.rider_pro_signup_red_bg backgroundImage = R.mipmap.rider_pro_signup_red_bg
) { ) {
resetPassword() resetPassword()

View File

@@ -1,6 +1,5 @@
package com.aiosman.riderpro.ui.comment package com.aiosman.riderpro.ui.comment
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.ime
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding 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.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
@@ -30,17 +26,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -48,15 +40,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel 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.R
import com.aiosman.riderpro.entity.CommentEntity import com.aiosman.riderpro.entity.CommentEntity
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal 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.CommentContent
import com.aiosman.riderpro.ui.post.CommentMenuModal 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.CommentsViewModel
import com.aiosman.riderpro.ui.post.OrderSelectionComponent import com.aiosman.riderpro.ui.post.OrderSelectionComponent
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -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<TextLayoutResult?>(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)
}
)
}

View File

@@ -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 <T : Any> DraggableGrid(
items: List<T>,
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<Int> = 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<Int> // 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<Int> // New parameter for locked indices
) {
var draggingItemIndex by mutableStateOf<Int?>(null)
private set
internal val scrollChannel = Channel<Float>()
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<Int?>(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)
}

View File

@@ -1,5 +1,7 @@
package com.aiosman.riderpro.ui.composables 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.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -24,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -74,17 +77,26 @@ fun EditCommentBottomModal(
fontSize = 20.sp, fontSize = 20.sp,
fontStyle = FontStyle.Italic fontStyle = FontStyle.Italic
) )
Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty ->
Image( Image(
painter = painterResource(id = R.drawable.rider_pro_send), painter = rememberUpdatedState(
if (isNotEmpty) painterResource(id = R.drawable.rider_pro_send) else painterResource(
id = R.drawable.rider_pro_send_disable
)
).value,
contentDescription = "Send", contentDescription = "Send",
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
.noRippleClickable { .noRippleClickable {
if (text.isNotEmpty()){
onSend(text) onSend(text)
text = "" text = ""
}
}, },
) )
} }
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (replyComment != null) { if (replyComment != null) {
Row( Row(

View File

@@ -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)
)
}
}

View File

@@ -1,5 +1,9 @@
package com.aiosman.riderpro.ui.composables 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.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -112,16 +116,23 @@ fun TextInputField(
.height(16.dp), .height(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (error != null) { AnimatedVisibility(
visible = error != null,
enter = fadeIn(),
exit = fadeOut()
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image( Image(
painter = painterResource(id = R.mipmap.rider_pro_input_error), painter = painterResource(id = R.mipmap.rider_pro_input_error),
contentDescription = "Error", contentDescription = "Error",
modifier = Modifier.size(8.dp) modifier = Modifier.size(8.dp)
) )
Spacer(modifier = Modifier.size(4.dp)) Spacer(modifier = Modifier.size(4.dp))
Text(error, color = Color(0xFFE53935), fontSize = 12.sp) AnimatedContent(targetState = error) { targetError ->
} Text(targetError ?: "", color = Color(0xFFE53935), fontSize = 12.sp)
}
}
}
} }
} }
} }

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
@@ -47,7 +48,8 @@ fun FavouriteListPage() {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f).pullRefresh(state) .weight(1f)
.pullRefresh(state)
) { ) {
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -57,7 +59,7 @@ fun FavouriteListPage() {
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp), .padding(horizontal = 16.dp, vertical = 16.dp),
) { ) {
NoticeScreenHeader("Favourite") NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false)
} }
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(3), columns = GridCells.Fixed(3),

View File

@@ -30,6 +30,7 @@ fun FavouriteNoticeScreen() {
var dataFlow = model.favouriteItemsFlow var dataFlow = model.favouriteItemsFlow
var favourites = dataFlow.collectAsLazyPagingItems() var favourites = dataFlow.collectAsLazyPagingItems()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.reload()
model.updateNotice() model.updateNotice()
} }
StatusBarMaskLayout( StatusBarMaskLayout(

View File

@@ -26,8 +26,13 @@ object FavouriteNoticeViewModel : ViewModel() {
private val _favouriteItemsFlow = private val _favouriteItemsFlow =
MutableStateFlow<PagingData<AccountFavouriteEntity>>(PagingData.empty()) MutableStateFlow<PagingData<AccountFavouriteEntity>>(PagingData.empty())
val favouriteItemsFlow = _favouriteItemsFlow.asStateFlow() val favouriteItemsFlow = _favouriteItemsFlow.asStateFlow()
var isFirstLoad = true
init { fun reload(force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
isFirstLoad = false
viewModelScope.launch { viewModelScope.launch {
Pager( Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false), config = PagingConfig(pageSize = 5, enablePlaceholders = false),

View File

@@ -47,6 +47,9 @@ fun FollowerListScreen(userId: Int) {
isFollowing = user.isFollowing isFollowing = user.isFollowing
) { ) {
scope.launch { scope.launch {
if (user.isFollowing) {
model.unFollowUser(user.id)
} else {
model.followUser(user.id) model.followUser(user.id)
} }
} }
@@ -55,3 +58,4 @@ fun FollowerListScreen(userId: Int) {
} }
} }
} }
}

View File

@@ -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 currentPagingData = usersFlow.value
val updatedPagingData = currentPagingData.map { user -> val updatedPagingData = currentPagingData.map { user ->
if (user.id == id) { if (user.id == id) {
user.copy(isFollowing = true) user.copy(isFollowing = isFollow)
} else { } else {
user user
} }
@@ -60,4 +60,9 @@ object FollowerListViewModel : ViewModel() {
updateIsFollow(userId) updateIsFollow(userId)
} }
suspend fun unFollowUser(userId: Int) {
userService.unFollowUser(userId.toString())
updateIsFollow(userId, false)
}
} }

View File

@@ -25,12 +25,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.FollowButton
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -47,11 +49,14 @@ fun FollowerNoticeScreen() {
var dataFlow = model.followerItemsFlow var dataFlow = model.followerItemsFlow
var followers = dataFlow.collectAsLazyPagingItems() var followers = dataFlow.collectAsLazyPagingItems()
Box( Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp) modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) { ) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false) NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.reload()
model.updateNotice() model.updateNotice()
} }
LazyColumn( LazyColumn(
@@ -114,30 +119,43 @@ fun FollowItem(
) { ) {
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp) Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp)
} }
if (!isFollowing) { if (userId != AppState.UserId) {
Box( FollowButton(
modifier = Modifier.noRippleClickable { isFollowing = isFollowing,
onFollow() onFollowClick = onFollow,
} fontSize = 14.sp,
) { imageModifier = Modifier
Image( .width(100.dp)
painter = painterResource(id = R.drawable.follow_bg),
contentDescription = "Follow",
modifier = Modifier
.width(79.dp)
.height(24.dp) .height(24.dp)
) )
Text(
"FOLLOW",
fontSize = 14.sp,
color = Color(0xFFFFFFFF),
modifier = Modifier.align(
Alignment.Center
)
)
}
} }
// 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
// )
// )
// }
} }
} }

View File

@@ -30,8 +30,13 @@ object FollowerNoticeViewModel : ViewModel() {
private val _followerItemsFlow = private val _followerItemsFlow =
MutableStateFlow<PagingData<AccountFollow>>(PagingData.empty()) MutableStateFlow<PagingData<AccountFollow>>(PagingData.empty())
val followerItemsFlow = _followerItemsFlow.asStateFlow() val followerItemsFlow = _followerItemsFlow.asStateFlow()
var isFirstLoad = true
init { fun reload(force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
isFirstLoad = false
viewModelScope.launch { viewModelScope.launch {
Pager( Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false), config = PagingConfig(pageSize = 5, enablePlaceholders = false),

View File

@@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun FollowingListScreen(userId: Int) { fun FollowingListScreen(userId: Int) {
val model = FollowerListViewModel val model = FollowingListViewModel
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.loadData(userId) model.loadData(userId)
@@ -47,6 +47,9 @@ fun FollowingListScreen(userId: Int) {
isFollowing = user.isFollowing isFollowing = user.isFollowing
) { ) {
scope.launch { scope.launch {
if (user.isFollowing) {
model.unfollowUser(user.id)
} else {
model.followUser(user.id) model.followUser(user.id)
} }
} }
@@ -55,3 +58,4 @@ fun FollowingListScreen(userId: Int) {
} }
} }
} }
}

View File

@@ -24,9 +24,6 @@ object FollowingListViewModel : ViewModel() {
val usersFlow = _usersFlow.asStateFlow() val usersFlow = _usersFlow.asStateFlow()
private var userId by mutableStateOf<Int?>(null) private var userId by mutableStateOf<Int?>(null)
fun loadData(id: Int) { fun loadData(id: Int) {
if (userId == id) {
return
}
userId = id userId = id
viewModelScope.launch { viewModelScope.launch {
Pager( Pager(
@@ -34,7 +31,7 @@ object FollowingListViewModel : ViewModel() {
pagingSourceFactory = { pagingSourceFactory = {
AccountPagingSource( AccountPagingSource(
userService, userService,
followerId = id followingId = id
) )
} }
).flow.cachedIn(viewModelScope).collectLatest { ).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 currentPagingData = usersFlow.value
val updatedPagingData = currentPagingData.map { user -> val updatedPagingData = currentPagingData.map { user ->
if (user.id == id) { if (user.id == id) {
user.copy(isFollowing = true) user.copy(isFollowing = isFollow)
} else { } else {
user user
} }
@@ -60,4 +57,9 @@ object FollowingListViewModel : ViewModel() {
updateIsFollow(userId) updateIsFollow(userId)
} }
suspend fun unfollowUser(userId: Int) {
userService.unFollowUser(userId.toString())
updateIsFollow(userId, false)
}
} }

View File

@@ -20,11 +20,8 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -35,30 +32,28 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.utils.File.saveImageToGallery import com.aiosman.riderpro.utils.File.saveImageToGallery
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable import net.engawapg.lib.zoomable.zoomable
@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class, @OptIn(
ExperimentalMaterial3Api::class ExperimentalFoundationApi::class,
) )
@Composable @Composable
fun ImageViewer() { fun ImageViewer() {
@@ -72,8 +67,11 @@ fun ImageViewer() {
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.dp WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.dp
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val showRawImageStates = remember { mutableStateListOf(*Array(images.size) { false }) } val showRawImageStates = remember { mutableStateListOf(*Array(images.size) { false }) }
var showBottomSheet by remember { mutableStateOf(false) }
var isDownloading by remember { mutableStateOf(false) } var isDownloading by remember { mutableStateOf(false) }
var currentPage by remember { mutableStateOf(model.initialIndex) }
LaunchedEffect(pagerState) {
currentPage = pagerState.currentPage
}
StatusBarMaskLayout( StatusBarMaskLayout(
modifier = Modifier.background(Color.Black), modifier = Modifier.background(Color.Black),
) { ) {
@@ -84,7 +82,7 @@ fun ImageViewer() {
) { ) {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(),
) { page -> ) { page ->
val zoomState = rememberZoomState() val zoomState = rememberZoomState()
CustomAsyncImage( CustomAsyncImage(
@@ -102,6 +100,23 @@ fun ImageViewer() {
contentScale = ContentScale.Fit, 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( Box(
modifier = Modifier modifier = Modifier
@@ -153,7 +168,7 @@ fun ImageViewer() {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
"Download", stringResource(R.string.download),
color = Color.White color = Color.White
) )
} }
@@ -174,7 +189,7 @@ fun ImageViewer() {
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
"Original", stringResource(R.string.original),
color = Color.White color = Color.White
) )
} }

View File

@@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R 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.BottomNavigationPlaceholder
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer 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.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.PostViewModel import com.aiosman.riderpro.ui.post.PostViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -70,7 +75,7 @@ fun NotificationsScreen() {
var comments = dataFlow.collectAsLazyPagingItems() var comments = dataFlow.collectAsLazyPagingItems()
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = { val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch { MessageListViewModel.viewModelScope.launch {
MessageListViewModel.initData() MessageListViewModel.initData(force = true)
} }
}) })
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -81,10 +86,12 @@ fun NotificationsScreen() {
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
StatusBarSpacer() StatusBarSpacer()
Box(modifier = Modifier Box(
modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.pullRefresh(state)) { .pullRefresh(state)
) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
@@ -99,6 +106,13 @@ fun NotificationsScreen() {
R.drawable.rider_pro_like, R.drawable.rider_pro_like,
stringResource(R.string.like_upper) stringResource(R.string.like_upper)
) { ) {
if (MessageListViewModel.likeNoticeCount > 0) {
// 刷新点赞消息列表
LikeNoticeViewModel.isFirstLoad = true
// 清除点赞消息数量
MessageListViewModel.clearLikeNoticeCount()
}
navController.navigate(NavigationRoute.Likes.route) navController.navigate(NavigationRoute.Likes.route)
} }
NotificationIndicator( NotificationIndicator(
@@ -106,6 +120,11 @@ fun NotificationsScreen() {
R.drawable.rider_pro_followers, R.drawable.rider_pro_followers,
stringResource(R.string.followers_upper) stringResource(R.string.followers_upper)
) { ) {
if (MessageListViewModel.followNoticeCount > 0) {
// 刷新关注消息列表
FollowerNoticeViewModel.isFirstLoad = true
MessageListViewModel.clearFollowNoticeCount()
}
navController.navigate(NavigationRoute.Followers.route) navController.navigate(NavigationRoute.Followers.route)
} }
NotificationIndicator( NotificationIndicator(
@@ -113,11 +132,41 @@ fun NotificationsScreen() {
R.drawable.rider_pro_favoriate, R.drawable.rider_pro_favoriate,
stringResource(R.string.favourites_upper) stringResource(R.string.favourites_upper)
) { ) {
if (MessageListViewModel.favouriteNoticeCount > 0) {
// 刷新收藏消息列表
FavouriteNoticeViewModel.isFirstLoad = true
MessageListViewModel.clearFavouriteNoticeCount()
}
navController.navigate(NavigationRoute.FavouritesScreen.route) navController.navigate(NavigationRoute.FavouritesScreen.route)
} }
} }
HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp)) HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp))
NotificationCounterItem(MessageListViewModel.commentNoticeCount) NotificationCounterItem(MessageListViewModel.commentNoticeCount)
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( LazyColumn(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
@@ -128,8 +177,6 @@ fun NotificationsScreen() {
CommentNoticeItem(comment) { CommentNoticeItem(comment) {
MessageListViewModel.updateReadStatus(comment.id) MessageListViewModel.updateReadStatus(comment.id)
MessageListViewModel.viewModelScope.launch { MessageListViewModel.viewModelScope.launch {
// PostViewModel.postId = comment.postId.toString()
// PostViewModel.initData()
navController.navigate( navController.navigate(
NavigationRoute.Post.route.replace( NavigationRoute.Post.route.replace(
"{id}", "{id}",
@@ -137,7 +184,43 @@ fun NotificationsScreen() {
) )
) )
} }
}
}
}
// 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",
)
}
} }
} }
} }
@@ -146,6 +229,7 @@ fun NotificationsScreen() {
} }
} }
} }
}
PullRefreshIndicator( PullRefreshIndicator(
MessageListViewModel.isLoading, MessageListViewModel.isLoading,
state, state,

View File

@@ -31,9 +31,15 @@ object MessageListViewModel : ViewModel() {
private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty()) private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
val commentItemsFlow = _commentItemsFlow.asStateFlow() val commentItemsFlow = _commentItemsFlow.asStateFlow()
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var isFirstLoad = true
suspend fun initData() { suspend fun initData(force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
if (force) {
isLoading = true isLoading = true
}
isFirstLoad = false
val info = accountService.getMyNoticeInfo() val info = accountService.getMyNoticeInfo()
noticeInfo = info noticeInfo = info
viewModelScope.launch { viewModelScope.launch {
@@ -51,6 +57,7 @@ object MessageListViewModel : ViewModel() {
} }
} }
isLoading = false isLoading = false
} }
val likeNoticeCount val likeNoticeCount
@@ -80,4 +87,15 @@ object MessageListViewModel : ViewModel() {
updateIsRead(id) updateIsRead(id)
} }
} }
fun clearLikeNoticeCount() {
noticeInfo = noticeInfo?.copy(likeCount = 0)
}
fun clearFollowNoticeCount() {
noticeInfo = noticeInfo?.copy(followCount = 0)
}
fun clearFavouriteNoticeCount() {
noticeInfo = noticeInfo?.copy(favoriteCount = 0)
}
} }

View File

@@ -37,14 +37,20 @@ object MyProfileViewModel : ViewModel() {
private var _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty()) private var _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
var momentsFlow = _momentsFlow.asStateFlow() var momentsFlow = _momentsFlow.asStateFlow()
var refreshing by mutableStateOf(false) var refreshing by mutableStateOf(false)
var firstLoad = true
fun loadProfile(pullRefresh: Boolean = false) { fun loadProfile(pullRefresh: Boolean = false) {
if (!firstLoad && !pullRefresh) {
return
}
viewModelScope.launch { viewModelScope.launch {
if (pullRefresh){ if (pullRefresh){
refreshing = true refreshing = true
} }
firstLoad = false
profile = accountService.getMyAccountProfile() profile = accountService.getMyAccountProfile()
val profile = accountService.getMyAccountProfile() val profile = accountService.getMyAccountProfile()
refreshing = false refreshing = false
try {
Pager( Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false), config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = { pagingSourceFactory = {
@@ -56,6 +62,10 @@ object MyProfileViewModel : ViewModel() {
).flow.cachedIn(viewModelScope).collectLatest { ).flow.cachedIn(viewModelScope).collectLatest {
_momentsFlow.value = it _momentsFlow.value = it
} }
}catch (e: Exception){
Log.e("MyProfileViewModel", "loadProfile: ", e)
}
} }
} }

View File

@@ -162,8 +162,6 @@ fun ProfilePage() {
) )
} }
} }
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
@@ -174,9 +172,13 @@ fun ProfilePage() {
) )
) { ) {
Box( Box(
modifier = Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)).shadow( modifier = Modifier
.padding(16.dp)
.clip(RoundedCornerShape(8.dp))
.shadow(
elevation = 20.dp elevation = 20.dp
).background(Color.White.copy(alpha = 0.7f)) )
.background(Color.White.copy(alpha = 0.7f))
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon), painter = painterResource(id = R.drawable.rider_pro_more_horizon),
@@ -191,7 +193,7 @@ fun ProfilePage() {
com.aiosman.riderpro.ui.composables.DropdownMenu( com.aiosman.riderpro.ui.composables.DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = { expanded = false }, onDismissRequest = { expanded = false },
width = 300, width = 250,
menuItems = listOf( menuItems = listOf(
MenuItem( MenuItem(
stringResource(R.string.logout), stringResource(R.string.logout),
@@ -217,7 +219,7 @@ fun ProfilePage() {
} }
}, },
MenuItem( MenuItem(
"Favourite", stringResource(R.string.favourites),
R.drawable.rider_pro_favourite R.drawable.rider_pro_favourite
) { ) {
expanded = false expanded = false

View File

@@ -23,8 +23,12 @@ object LikeNoticeViewModel : ViewModel() {
private val accountService: AccountService = AccountServiceImpl() private val accountService: AccountService = AccountServiceImpl()
private val _likeItemsFlow = MutableStateFlow<PagingData<AccountLikeEntity>>(PagingData.empty()) private val _likeItemsFlow = MutableStateFlow<PagingData<AccountLikeEntity>>(PagingData.empty())
val likeItemsFlow = _likeItemsFlow.asStateFlow() val likeItemsFlow = _likeItemsFlow.asStateFlow()
var isFirstLoad = true
init { fun reload(force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
isFirstLoad = false
viewModelScope.launch { viewModelScope.launch {
Pager( Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false), config = PagingConfig(pageSize = 5, enablePlaceholders = false),

View File

@@ -56,8 +56,10 @@ fun LikeNoticeScreen() {
var dataFlow = model.likeItemsFlow var dataFlow = model.likeItemsFlow
var likes = dataFlow.collectAsLazyPagingItems() var likes = dataFlow.collectAsLazyPagingItems()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.reload()
model.updateNotice() model.updateNotice()
} }
StatusBarMaskLayout( StatusBarMaskLayout(
darkIcons = true, darkIcons = true,
maskBoxBackgroundColor = Color(0xFFFFFFFF) maskBoxBackgroundColor = Color(0xFFFFFFFF)
@@ -125,7 +127,7 @@ fun ActionPostNoticeItem(
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
Box( Box(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp) modifier = Modifier.padding(vertical = 16.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -188,7 +190,7 @@ fun LikeCommentNoticeItem(
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
Box( Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp).noRippleClickable { modifier = Modifier.padding(vertical = 16.dp).noRippleClickable {
item.comment?.postId.let { item.comment?.postId.let {
navController.navigate( navController.navigate(
NavigationRoute.Post.route.replace( NavigationRoute.Post.route.replace(
@@ -261,7 +263,8 @@ fun LikeCommentNoticeItem(
Text( Text(
text = item.comment?.content ?: "", text = item.comment?.content ?: "",
fontSize = 12.sp, fontSize = 12.sp,
color = Color(0x99000000) color = Color(0x99000000),
maxLines = 2
) )
} }
} }

View File

@@ -53,9 +53,9 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun EmailSignupScreen() { fun EmailSignupScreen() {
var email by remember { mutableStateOf("takayamaaren@gmail.com") } var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("Dzh17217.") } var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("Dzh17217.") } var confirmPassword by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) } var rememberMe by remember { mutableStateOf(false) }
var acceptTerms by remember { mutableStateOf(false) } var acceptTerms by remember { mutableStateOf(false) }
var acceptPromotions by remember { mutableStateOf(false) } var acceptPromotions by remember { mutableStateOf(false) }
@@ -68,7 +68,6 @@ fun EmailSignupScreen() {
var confirmPasswordError by remember { mutableStateOf<String?>(null) } var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var termsError by remember { mutableStateOf<Boolean>(false) } var termsError by remember { mutableStateOf<Boolean>(false) }
var promotionsError by remember { mutableStateOf<Boolean>(false) } var promotionsError by remember { mutableStateOf<Boolean>(false) }
fun validateForm(): Boolean { fun validateForm(): Boolean {
emailError = when { emailError = when {
// 非空 // 非空

View File

@@ -93,9 +93,15 @@ fun UserAuthScreen() {
} }
} catch (e: ServiceException) { } catch (e: ServiceException) {
// handle error // handle error
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() Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
} }
} }
}
} }
@@ -181,15 +187,12 @@ fun UserAuthScreen() {
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { com.aiosman.riderpro.ui.composables.Checkbox(
Checkbox(
checked = rememberMe, checked = rememberMe,
onCheckedChange = { onCheckedChange = {
rememberMe = it rememberMe = it
}, },
colors = CheckboxDefaults.colors( size = 18
checkedColor = Color.Black
),
) )
Text( Text(
stringResource(R.string.remember_me), stringResource(R.string.remember_me),
@@ -197,10 +200,34 @@ fun UserAuthScreen() {
fontSize = 12.sp fontSize = 12.sp
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable { Text(
stringResource(R.string.forgot_password),
fontSize = 12.sp,
modifier = Modifier.noRippleClickable {
navController.navigate(NavigationRoute.ResetPassword.route) 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)) Spacer(modifier = Modifier.height(64.dp))
ActionButton( ActionButton(

View File

@@ -29,6 +29,7 @@ class CommentsViewModel(
val commentsFlow = _commentsFlow.asStateFlow() val commentsFlow = _commentsFlow.asStateFlow()
var order: String by mutableStateOf("like") var order: String by mutableStateOf("like")
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList()) var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
/** /**
* 预加载,在跳转到 PostScreen 之前设置好内容 * 预加载,在跳转到 PostScreen 之前设置好内容
@@ -49,6 +50,7 @@ class CommentsViewModel(
fun reloadComment() { fun reloadComment() {
viewModelScope.launch { viewModelScope.launch {
try {
Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false), Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = { pagingSourceFactory = {
CommentPagingSource( CommentPagingSource(
@@ -59,6 +61,9 @@ class CommentsViewModel(
}).flow.cachedIn(viewModelScope).collectLatest { }).flow.cachedIn(viewModelScope).collectLatest {
_commentsFlow.value = it _commentsFlow.value = it
} }
} catch (e: Exception) {
e.printStackTrace()
}
} }
} }
@@ -144,15 +149,22 @@ class CommentsViewModel(
fun deleteComment(commentId: Int) { fun deleteComment(commentId: Int) {
viewModelScope.launch { viewModelScope.launch {
commentService.DeleteComment(commentId) commentService.DeleteComment(commentId)
// 如果是刚刚创建的评论则从addedCommentList中删除
if (addedCommentList.any { it.id == commentId }) {
addedCommentList = addedCommentList.filter { it.id != commentId }
} else {
reloadComment() reloadComment()
} }
} }
}
fun loadMoreSubComments(commentId: Int) { fun loadMoreSubComments(commentId: Int) {
viewModelScope.launch { viewModelScope.launch {
val currentPagingData = commentsFlow.value val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment -> val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) { if (comment.id == commentId) {
try {
subCommentLoadingMap[commentId] = true
val subCommentList = commentService.getComments( val subCommentList = commentService.getComments(
postId = postId.toInt(), postId = postId.toInt(),
parentCommentId = commentId, parentCommentId = commentId,
@@ -163,6 +175,11 @@ class CommentsViewModel(
reply = comment.reply.plus(subCommentList), reply = comment.reply.plus(subCommentList),
replyPage = comment.replyPage + 1 replyPage = comment.replyPage + 1
) )
} catch (e: Exception) {
return@map comment.copy()
} finally {
subCommentLoadingMap[commentId] = false
}
} }
comment comment
} }

View File

@@ -1,12 +1,11 @@
package com.aiosman.riderpro.ui.post package com.aiosman.riderpro.ui.post
import android.app.Activity import android.net.Uri
import android.content.Intent
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -41,7 +43,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale 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.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage 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.RelPostCard
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
@Preview @Preview
@@ -76,7 +82,9 @@ fun NewPostScreen() {
} }
StatusBarMaskLayout( StatusBarMaskLayout(
darkIcons = true, darkIcons = true,
modifier = Modifier.fillMaxSize().background(Color.White) modifier = Modifier
.fillMaxSize()
.background(Color.White)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -95,13 +103,18 @@ fun NewPostScreen() {
NewPostViewModel.textContent = it NewPostViewModel.textContent = it
} }
Column( Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) { ) {
model.relMoment?.let { model.relMoment?.let {
Text("Share with") Text("Share with")
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Box( 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( RelPostCard(
momentEntity = it, momentEntity = it,
@@ -188,6 +201,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
} }
} }
} }
@Composable @Composable
fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Unit) { 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 @Composable
fun AddImageGrid() { fun AddImageGrid() {
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -226,40 +241,93 @@ fun AddImageGrid() {
} }
} }
val takePictureLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture()
) { success ->
if (success) {
model.imageUriList += model.currentPhotoUri.toString()
}
}
val stroke = Stroke( val stroke = Stroke(
width = 2f, width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
) )
DraggableGrid(
items = NewPostViewModel.imageUriList,
onMove = { from, to ->
NewPostViewModel.imageUriList = NewPostViewModel.imageUriList.toMutableList().apply {
add(to, removeAt(from))
}
},
lockedIndices = listOf(
),
onDragModeEnd = {},
onDragModeStart = {},
additionalItems = listOf(
),
) { item, isDrag ->
Box( Box(
modifier = Modifier.fillMaxWidth()
) {
FlowRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
model.imageUriList.forEach {
CustomAsyncImage( CustomAsyncImage(
context, LocalContext.current,
it, item,
contentDescription = "Image", contentDescription = "Image",
modifier = Modifier modifier = Modifier
.size(110.dp) .fillMaxWidth()
.aspectRatio(1f)
.drawBehind { .noRippleClickable {
drawRoundRect(color = Color(0xFF999999), style = stroke)
}.noRippleClickable {
navController.navigate(NavigationRoute.NewPostImageGrid.route) navController.navigate(NavigationRoute.NewPostImageGrid.route)
}, },
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} if (isDrag) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(110.dp) .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
.fillMaxWidth()
.aspectRatio(1f)
.drawBehind { .drawBehind {
drawRoundRect(color = Color(0xFF999999), style = stroke) drawRoundRect(color = Color(0xFF999999), style = stroke)
} }
@@ -275,10 +343,37 @@ fun AddImageGrid() {
.align(Alignment.Center) .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) @OptIn(ExperimentalMaterial3Api::class)

View File

@@ -29,6 +29,7 @@ object NewPostViewModel : ViewModel() {
var imageUriList by mutableStateOf(listOf<String>()) var imageUriList by mutableStateOf(listOf<String>())
var relPostId by mutableStateOf<Int?>(null) var relPostId by mutableStateOf<Int?>(null)
var relMoment by mutableStateOf<MomentEntity?>(null) var relMoment by mutableStateOf<MomentEntity?>(null)
var currentPhotoUri: Uri? = null
fun asNewPost() { fun asNewPost() {
textContent = "" textContent = ""
searchPlaceAddressResult = null searchPlaceAddressResult = null

View File

@@ -1,7 +1,9 @@
package com.aiosman.riderpro.ui.post package com.aiosman.riderpro.ui.post
import android.util.Log import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background 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.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn 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.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
@@ -46,22 +43,23 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -69,7 +67,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.compose.LazyPagingItems import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.AppState import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalAnimatedContentScope 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.formatPostTime
import com.aiosman.riderpro.exp.timeAgo import com.aiosman.riderpro.exp.timeAgo
import com.aiosman.riderpro.ui.NavigationRoute 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.AnimatedFavouriteIcon
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
import com.aiosman.riderpro.ui.composables.CustomAsyncImage 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.EditCommentBottomModal
import com.aiosman.riderpro.ui.composables.FollowButton
import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
@@ -113,28 +113,67 @@ fun PostScreen(
var contextComment by remember { mutableStateOf<CommentEntity?>(null) } var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
var replyComment by remember { mutableStateOf<CommentEntity?>(null) } var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
var showCommentModal by remember { mutableStateOf(false) } var showCommentModal by remember { mutableStateOf(false) }
var commentModalState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var editCommentModalState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.initData() viewModel.initData()
} }
if (showCommentMenu) { if (showCommentMenu) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
showCommentMenu = false showCommentMenu = false
}, },
containerColor = Color.White, containerColor = Color.White,
sheetState = rememberModalBottomSheetState( sheetState = commentModalState,
skipPartiallyExpanded = true
),
dragHandle = {}, dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0) windowInsets = WindowInsets(0)
) { ) {
CommentMenuModal( CommentMenuModal(
onDeleteClick = { onDeleteClick = {
scope.launch {
commentModalState.hide()
showCommentMenu = false showCommentMenu = false
}
contextComment?.let { contextComment?.let {
viewModel.deleteComment(it.id) 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 content = it
) )
} }
editCommentModalState.hide()
showCommentModal = false showCommentModal = false
} }
@@ -186,6 +227,7 @@ fun PostScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { bottomBar = {
if (!viewModel.isError) {
PostBottomBar( PostBottomBar(
onLikeClick = { onLikeClick = {
scope.launch { scope.launch {
@@ -212,6 +254,8 @@ fun PostScreen(
momentEntity = viewModel.moment momentEntity = viewModel.moment
) )
} }
}
) { ) {
it it
Column( Column(
@@ -220,14 +264,35 @@ fun PostScreen(
.background(Color.White) .background(Color.White)
) { ) {
StatusBarSpacer() StatusBarSpacer()
if (viewModel.isError) {
Box(
modifier = Modifier.fillMaxWidth().padding(16.dp)
) {
NoticeScreenHeader("Post", moreIcon = false)
}
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Umm, post are not found.",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
)
}
}else{
Header( Header(
avatar = viewModel.avatar, avatar = viewModel.avatar,
nickname = viewModel.nickname, nickname = viewModel.nickname,
userId = viewModel.moment?.authorId, userId = viewModel.moment?.authorId,
isFollowing = viewModel.accountProfileEntity?.isFollowing ?: false, isFollowing = viewModel.moment?.followStatus == true,
onFollowClick = { onFollowClick = {
scope.launch { scope.launch {
if (viewModel.accountProfileEntity?.isFollowing == true) { if (viewModel.moment?.followStatus == true) {
viewModel.unfollowUser() viewModel.unfollowUser()
} else { } else {
viewModel.followUser() viewModel.followUser()
@@ -295,9 +360,9 @@ fun PostScreen(
item { item {
CommentContent( CommentContent(
viewModel = commentsViewModel, viewModel = commentsViewModel,
onLongClick = { onLongClick = { comment ->
showCommentMenu = true showCommentMenu = true
contextComment = it contextComment = comment
}, },
onReply = { parentComment, _, _, _ -> onReply = { parentComment, _, _, _ ->
replyComment = parentComment replyComment = parentComment
@@ -311,6 +376,8 @@ fun PostScreen(
} }
} }
}
} }
} }
@@ -327,6 +394,10 @@ fun CommentContent(
} }
for (item in addedTopLevelComment) { for (item in addedTopLevelComment) {
AnimatedVisibility(
visible = true,
enter = fadeIn() + slideInVertically()
) {
Box( Box(
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
) { ) {
@@ -341,11 +412,8 @@ fun CommentContent(
} }
} }
}, },
onLongClick = { onLongClick = { comment ->
if (AppState.UserId != item.id) { onLongClick(comment)
return@CommentItem
}
onLongClick(item)
}, },
onReply = { parentComment, _, _, _ -> onReply = { parentComment, _, _, _ ->
onReply( onReply(
@@ -364,6 +432,7 @@ fun CommentContent(
) )
} }
} }
}
for (idx in 0 until commentsPagging.itemCount) { for (idx in 0 until commentsPagging.itemCount) {
val item = commentsPagging[idx] ?: return val item = commentsPagging[idx] ?: return
@@ -381,11 +450,8 @@ fun CommentContent(
} }
} }
}, },
onLongClick = { onLongClick = { comment ->
if (AppState.UserId != item.id) { onLongClick(comment)
return@CommentItem
}
onLongClick(item)
}, },
onReply = { parentComment, _, _, _ -> onReply = { parentComment, _, _, _ ->
onReply( 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)) Spacer(modifier = Modifier.width(8.dp))
Text(text = nickname ?: "", fontWeight = FontWeight.Bold) Text(text = nickname ?: "", fontWeight = FontWeight.Bold)
if (AppState.UserId != userId) { if (AppState.UserId != userId) {
Box( FollowButton(
modifier = Modifier isFollowing = isFollowing,
.height(20.dp) onFollowClick = onFollowClick,
.wrapContentWidth() imageModifier = Modifier.height(18.dp).width(80.dp),
.padding(start = 6.dp) fontSize = 12.sp
.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)
)
}
} }
if (AppState.UserId == userId) { if (AppState.UserId == userId) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@@ -626,47 +735,6 @@ fun PostDetails(
} }
} }
@Composable
fun CommentsSection(
lazyPagingItems: LazyPagingItems<CommentEntity>,
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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun CommentItem( fun CommentItem(
@@ -680,7 +748,7 @@ fun CommentItem(
replyUserAvatar: String? replyUserAvatar: String?
) -> Unit = { _, _, _, _ -> }, ) -> Unit = { _, _, _, _ -> },
onLoadMoreSubComments: ((CommentEntity) -> Unit)? = {}, onLoadMoreSubComments: ((CommentEntity) -> Unit)? = {},
onLongClick: () -> Unit = {}, onLongClick: (CommentEntity) -> Unit = {},
addedCommentList: List<CommentEntity> = emptyList() addedCommentList: List<CommentEntity> = emptyList()
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -688,12 +756,6 @@ fun CommentItem(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.combinedClickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = {},
onLongClick = onLongClick
)
) { ) {
Row(modifier = Modifier.padding(vertical = 8.dp)) { Row(modifier = Modifier.padding(vertical = 8.dp)) {
Box( Box(
@@ -719,7 +781,15 @@ fun CommentItem(
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Column( 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) Text(text = commentEntity.name, fontWeight = FontWeight.W600, fontSize = 14.sp)
Row { Row {
@@ -741,8 +811,10 @@ fun CommentItem(
pop() pop()
} }
append(" ${commentEntity.comment}") append(" ${commentEntity.comment}")
} }
ClickableText( Box {
CustomClickableText(
text = annotatedText, text = annotatedText,
onClick = { offset -> onClick = { offset ->
annotatedText.getStringAnnotations( annotatedText.getStringAnnotations(
@@ -759,15 +831,29 @@ fun CommentItem(
} }
}, },
style = TextStyle(fontSize = 14.sp), style = TextStyle(fontSize = 14.sp),
maxLines = Int.MAX_VALUE, onLongPress = {
softWrap = true onLongClick(commentEntity)
},
) )
}
} else { } else {
Text( Text(
text = commentEntity.comment, text = commentEntity.comment,
fontSize = 14.sp, fontSize = 14.sp,
maxLines = Int.MAX_VALUE, 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)) Spacer(modifier = Modifier.width(8.dp))
if (AppState.UserId?.toLong() != commentEntity.author) { if (AppState.UserId?.toLong() != commentEntity.author) {
Text( Text(
text = "Reply", text = stringResource(R.string.reply),
fontSize = 12.sp, fontSize = 12.sp,
color = Color.Gray, color = Color.Gray,
modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
@@ -832,13 +918,15 @@ fun CommentItem(
isChild = true, isChild = true,
onLike = onLike, onLike = onLike,
onReply = onReply, onReply = onReply,
onLongClick = onLongClick onLongClick = { comment ->
onLongClick(comment)
}
) )
} }
if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) { if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) {
val remaining = commentEntity.replyCount - commentEntity.reply.size val remaining = commentEntity.replyCount - commentEntity.reply.size
Text( Text(
text = "View $remaining more replies", text = stringResource(R.string.view_more_reply, remaining),
fontSize = 12.sp, fontSize = 12.sp,
color = Color(0xFF6F94AE), color = Color(0xFF6F94AE),
modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
@@ -851,7 +939,6 @@ fun CommentItem(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PostBottomBar( fun PostBottomBar(
onCreateCommentClick: () -> Unit = {}, onCreateCommentClick: () -> Unit = {},
@@ -956,12 +1043,16 @@ fun PostMenuModal(
onDeleteClick() 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)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "Delete", text = stringResource(R.string.delete),
fontSize = 11.sp, fontSize = 11.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -971,19 +1062,11 @@ fun PostMenuModal(
} }
@Composable @Composable
fun CommentMenuModal( fun MenuActionItem(
onDeleteClick: () -> Unit = {} icon: Int? = null,
) { text: String,
Column( content: @Composable() (() -> Unit)? = null,
modifier = Modifier onClick: () -> Unit
.fillMaxWidth()
.height(160.dp)
.padding(vertical = 47.dp, horizontal = 20.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
modifier = Modifier, modifier = Modifier,
@@ -994,20 +1077,150 @@ fun CommentMenuModal(
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.noRippleClickable { .noRippleClickable {
onDeleteClick() onClick()
} }
) { ) {
Image(painter = painterResource(id = R.drawable.rider_pro_moment_delete), contentDescription = "",modifier = Modifier.size(24.dp)) 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)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "Delete", text = text,
fontSize = 11.sp, fontSize = 11.sp,
fontWeight = FontWeight.Bold 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))
} }
} }

View File

@@ -28,24 +28,20 @@ class PostViewModel(
var moment by mutableStateOf<MomentEntity?>(null) var moment by mutableStateOf<MomentEntity?>(null)
var accountService: AccountService = AccountServiceImpl() var accountService: AccountService = AccountServiceImpl()
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId) 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() { fun reloadComment() {
commentsViewModel.reloadComment() commentsViewModel.reloadComment()
} }
suspend fun initData() { suspend fun initData() {
try {
moment = service.getMomentById(postId.toInt()) moment = service.getMomentById(postId.toInt())
// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString()) } catch (e: Exception) {
isError = true
return
}
commentsViewModel.reloadComment() commentsViewModel.reloadComment()
} }
@@ -106,16 +102,16 @@ class PostViewModel(
} }
suspend fun followUser() { suspend fun followUser() {
accountProfileEntity?.let { moment?.let {
userService.followUser(it.id.toString()) userService.followUser(it.authorId.toString())
accountProfileEntity = accountProfileEntity?.copy(isFollowing = true) moment = moment?.copy(followStatus = true)
} }
} }
suspend fun unfollowUser() { suspend fun unfollowUser() {
accountProfileEntity?.let { moment?.let {
userService.unFollowUser(it.id.toString()) userService.unFollowUser(it.authorId.toString())
accountProfileEntity = accountProfileEntity?.copy(isFollowing = false) moment = moment?.copy(followStatus = false)
} }
} }

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:alpha="0.77" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M15,20H5V7c0,-0.55 -0.45,-1 -1,-1h0C3.45,6 3,6.45 3,7v13c0,1.1 0.9,2 2,2h10c0.55,0 1,-0.45 1,-1v0C16,20.45 15.55,20 15,20zM20,16V4c0,-1.1 -0.9,-2 -2,-2H9C7.9,2 7,2.9 7,4v12c0,1.1 0.9,2 2,2h9C19.1,18 20,17.1 20,16zM18,16H9V4h9V16z"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:alpha="0.77" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#e0e0e0" android:pathData="M12,12m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"/>
<group>
<clip-path android:pathData="M12,12m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"/>
<path android:fillColor="#fff" android:fillType="evenOdd" android:pathData="M12,11l6,4.5L18,19l-6,-4.5L6,19L6,15.5ZM12,5 L18,9.5L18,13L12,8.5 6,13L6,9.5Z"/>
</group>
</vector>

View File

@@ -57,4 +57,17 @@
<string name="order_comment_default">默认</string> <string name="order_comment_default">默认</string>
<string name="order_comment_latest">最新</string> <string name="order_comment_latest">最新</string>
<string name="order_comment_earliest">最早</string> <string name="order_comment_earliest">最早</string>
<string name="download">下载</string>
<string name="original">原始图片</string>
<string name="favourites">收藏</string>
<string name="delete">删除</string>
<string name="copy">复制</string>
<string name="like">点赞</string>
<string name="reply">回复</string>
<string name="view_more_reply">查看更多%1d条回复</string>
<string name="error_invalidate_username_password">错误的用户名或密码</string>
<string name="recover_account_upper">找回密码</string>
<string name="recover">找回</string>
<string name="reset_mail_send_success">邮件已发送!请查收您的邮箱,按照邮件中的指示重置密码。</string>
<string name="reset_mail_send_failed">邮件发送失败,请检查您的网络连接或稍后重试。</string>
</resources> </resources>

View File

@@ -56,4 +56,17 @@
<string name="order_comment_default">Default</string> <string name="order_comment_default">Default</string>
<string name="order_comment_latest">Latest</string> <string name="order_comment_latest">Latest</string>
<string name="order_comment_earliest">Earliest</string> <string name="order_comment_earliest">Earliest</string>
<string name="download">Download</string>
<string name="original">Original</string>
<string name="favourites">Favourite</string>
<string name="delete">Delete</string>
<string name="copy">Copy</string>
<string name="like">Like</string>
<string name="reply">Reply</string>
<string name="view_more_reply">View %1d more replies</string>
<string name="error_invalidate_username_password">Invalid email or password</string>
<string name="recover_account_upper">RCOVER ACCOUNT</string>
<string name="recover">Recover</string>
<string name="reset_mail_send_success">An email has been sent to your registered email address. Please check your inbox and follow the instructions to reset your password.</string>
<string name="reset_mail_send_failed">Failed to send email. Please check your network connection or try again later.</string>
</resources> </resources>