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-perf")
implementation("com.google.firebase:firebase-messaging-ktx")
}

View File

@@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_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
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -16,8 +18,9 @@
android:theme="@style/Theme.RiderPro"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4"/>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/googleg_standard_color_18" />
@@ -49,6 +52,17 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</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>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -24,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -74,17 +77,26 @@ fun EditCommentBottomModal(
fontSize = 20.sp,
fontStyle = FontStyle.Italic
)
Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty ->
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",
modifier = Modifier
.size(32.dp)
.noRippleClickable {
if (text.isNotEmpty()){
onSend(text)
text = ""
}
},
)
}
}
Spacer(modifier = Modifier.height(16.dp))
if (replyComment != null) {
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
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -112,16 +116,23 @@ fun TextInputField(
.height(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (error != null) {
AnimatedVisibility(
visible = error != null,
enter = fadeIn(),
exit = fadeOut()
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(id = R.mipmap.rider_pro_input_error),
contentDescription = "Error",
modifier = Modifier.size(8.dp)
)
Spacer(modifier = Modifier.size(4.dp))
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController
@@ -47,7 +48,8 @@ fun FavouriteListPage() {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f).pullRefresh(state)
.weight(1f)
.pullRefresh(state)
) {
Column(
modifier = Modifier.fillMaxSize()
@@ -57,7 +59,7 @@ fun FavouriteListPage() {
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
) {
NoticeScreenHeader("Favourite")
NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false)
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),

View File

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

View File

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

View File

@@ -47,6 +47,9 @@ fun FollowerListScreen(userId: Int) {
isFollowing = user.isFollowing
) {
scope.launch {
if (user.isFollowing) {
model.unFollowUser(user.id)
} else {
model.followUser(user.id)
}
}
@@ -54,4 +57,5 @@ 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 updatedPagingData = currentPagingData.map { user ->
if (user.id == id) {
user.copy(isFollowing = true)
user.copy(isFollowing = isFollow)
} else {
user
}
@@ -60,4 +60,9 @@ object FollowerListViewModel : ViewModel() {
updateIsFollow(userId)
}
suspend fun unFollowUser(userId: Int) {
userService.unFollowUser(userId.toString())
updateIsFollow(userId, false)
}
}

View File

@@ -25,12 +25,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.FollowButton
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@@ -47,11 +49,14 @@ fun FollowerNoticeScreen() {
var dataFlow = model.followerItemsFlow
var followers = dataFlow.collectAsLazyPagingItems()
Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
}
LaunchedEffect(Unit) {
model.reload()
model.updateNotice()
}
LazyColumn(
@@ -114,30 +119,43 @@ fun FollowItem(
) {
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
if (!isFollowing) {
Box(
modifier = Modifier.noRippleClickable {
onFollow()
}
) {
Image(
painter = painterResource(id = R.drawable.follow_bg),
contentDescription = "Follow",
modifier = Modifier
.width(79.dp)
if (userId != AppState.UserId) {
FollowButton(
isFollowing = isFollowing,
onFollowClick = onFollow,
fontSize = 14.sp,
imageModifier = Modifier
.width(100.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 =
MutableStateFlow<PagingData<AccountFollow>>(PagingData.empty())
val followerItemsFlow = _followerItemsFlow.asStateFlow()
var isFirstLoad = true
init {
fun reload(force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
isFirstLoad = false
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,11 @@
package com.aiosman.riderpro.ui.post
import android.app.Activity
import android.content.Intent
import android.util.Log
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -14,10 +13,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -25,9 +24,12 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
@@ -41,7 +43,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
@@ -50,17 +54,19 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.lifecycle.viewModelScope
import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.DraggableGrid
import com.aiosman.riderpro.ui.composables.RelPostCard
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import java.io.File
@Preview
@@ -76,7 +82,9 @@ fun NewPostScreen() {
}
StatusBarMaskLayout(
darkIcons = true,
modifier = Modifier.fillMaxSize().background(Color.White)
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
Column(
modifier = Modifier
@@ -94,14 +102,19 @@ fun NewPostScreen() {
NewPostTextField("Share your adventure…", NewPostViewModel.textContent) {
NewPostViewModel.textContent = it
}
Column (
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
model.relMoment?.let {
Text("Share with")
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(color = Color(0xFFEEEEEE)).padding(24.dp)
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(color = Color(0xFFEEEEEE))
.padding(24.dp)
) {
RelPostCard(
momentEntity = it,
@@ -188,6 +201,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
}
}
}
@Composable
fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Unit) {
@@ -211,7 +225,8 @@ fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Uni
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AddImageGrid() {
val navController = LocalNavController.current
@@ -226,44 +241,97 @@ fun AddImageGrid() {
}
}
val takePictureLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture()
) { success ->
if (success) {
model.imageUriList += model.currentPhotoUri.toString()
}
}
val stroke = Stroke(
width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
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(
modifier = Modifier.fillMaxWidth()
) {
FlowRow(
modifier = Modifier
.fillMaxWidth()
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
model.imageUriList.forEach {
CustomAsyncImage(
context,
it,
LocalContext.current,
item,
contentDescription = "Image",
modifier = Modifier
.size(110.dp)
.drawBehind {
drawRoundRect(color = Color(0xFF999999), style = stroke)
}.noRippleClickable {
.fillMaxWidth()
.aspectRatio(1f)
.noRippleClickable {
navController.navigate(NavigationRoute.NewPostImageGrid.route)
},
contentScale = ContentScale.Crop
)
}
if (isDrag) {
Box(
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 {
drawRoundRect(color = Color(0xFF999999), style = stroke)
}
.noRippleClickable{
.noRippleClickable {
pickImagesLauncher.launch("image/*")
},
) {
@@ -275,10 +343,37 @@ fun AddImageGrid() {
.align(Alignment.Center)
)
}
}
item {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.drawBehind {
drawRoundRect(color = Color(0xFF999999), style = stroke)
}
.noRippleClickable {
val photoFile = File(context.cacheDir, "photo.jpg")
val photoUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
photoFile
)
model.currentPhotoUri = photoUri
takePictureLauncher.launch(photoUri)
},
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_camera),
contentDescription = "Take Photo",
modifier = Modifier
.size(48.dp)
.align(Alignment.Center),
colorFilter = ColorFilter.tint(Color.Gray)
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)

View File

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

View File

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

View File

@@ -28,24 +28,20 @@ class PostViewModel(
var moment by mutableStateOf<MomentEntity?>(null)
var accountService: AccountService = AccountServiceImpl()
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
var isError by mutableStateOf(false)
/**
* 预加载,在跳转到 PostScreen 之前设置好内容
*/
fun preTransit(momentEntity: MomentEntity?) {
this.moment = momentEntity
this.nickname = momentEntity?.nickname ?: ""
this.commentsViewModel = CommentsViewModel(postId)
commentsViewModel.preTransit()
}
fun reloadComment() {
commentsViewModel.reloadComment()
}
suspend fun initData() {
try {
moment = service.getMomentById(postId.toInt())
// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString())
} catch (e: Exception) {
isError = true
return
}
commentsViewModel.reloadComment()
}
@@ -106,16 +102,16 @@ class PostViewModel(
}
suspend fun followUser() {
accountProfileEntity?.let {
userService.followUser(it.id.toString())
accountProfileEntity = accountProfileEntity?.copy(isFollowing = true)
moment?.let {
userService.followUser(it.authorId.toString())
moment = moment?.copy(followStatus = true)
}
}
suspend fun unfollowUser() {
accountProfileEntity?.let {
userService.unFollowUser(it.id.toString())
accountProfileEntity = accountProfileEntity?.copy(isFollowing = false)
moment?.let {
userService.unFollowUser(it.authorId.toString())
moment = moment?.copy(followStatus = false)
}
}

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_latest">最新</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>

View File

@@ -56,4 +56,17 @@
<string name="order_comment_default">Default</string>
<string name="order_comment_latest">Latest</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>