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,8 +18,9 @@
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:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4"/> android:name="com.google.android.geo.API_KEY"
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"
android:resource="@drawable/googleg_standard_color_18" /> android:resource="@drawable/googleg_standard_color_18" />
@@ -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,16 +77,25 @@ fun EditCommentBottomModal(
fontSize = 20.sp, fontSize = 20.sp,
fontStyle = FontStyle.Italic fontStyle = FontStyle.Italic
) )
Image( Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty ->
painter = painterResource(id = R.drawable.rider_pro_send), Image(
contentDescription = "Send", painter = rememberUpdatedState(
modifier = Modifier if (isNotEmpty) painterResource(id = R.drawable.rider_pro_send) else painterResource(
.size(32.dp) id = R.drawable.rider_pro_send_disable
.noRippleClickable { )
onSend(text) ).value,
text = "" contentDescription = "Send",
}, modifier = Modifier
) .size(32.dp)
.noRippleClickable {
if (text.isNotEmpty()){
onSend(text)
text = ""
}
},
)
}
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (replyComment != null) { if (replyComment != null) {

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(
Image( visible = error != null,
painter = painterResource(id = R.mipmap.rider_pro_input_error), enter = fadeIn(),
contentDescription = "Error", exit = fadeOut()
modifier = Modifier.size(8.dp) ) {
) Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.size(4.dp)) Image(
Text(error, color = Color(0xFFE53935), fontSize = 12.sp) painter = painterResource(id = R.mipmap.rider_pro_input_error),
contentDescription = "Error",
modifier = Modifier.size(8.dp)
)
Spacer(modifier = Modifier.size(4.dp))
AnimatedContent(targetState = error) { targetError ->
Text(targetError ?: "", color = Color(0xFFE53935), fontSize = 12.sp)
}
}
} }
} }
} }
} }

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,7 +47,11 @@ fun FollowerListScreen(userId: Int) {
isFollowing = user.isFollowing isFollowing = user.isFollowing
) { ) {
scope.launch { scope.launch {
model.followUser(user.id) if (user.isFollowing) {
model.unFollowUser(user.id)
} else {
model.followUser(user.id)
}
} }
} }
} }

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), .height(24.dp)
contentDescription = "Follow", )
modifier = Modifier
.width(79.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,7 +47,11 @@ fun FollowingListScreen(userId: Int) {
isFollowing = user.isFollowing isFollowing = user.isFollowing
) { ) {
scope.launch { scope.launch {
model.followUser(user.id) if (user.isFollowing) {
model.unfollowUser(user.id)
} else {
model.followUser(user.id)
}
} }
} }
} }

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
@@ -142,7 +157,7 @@ fun ImageViewer() {
modifier = Modifier.size(32.dp), modifier = Modifier.size(32.dp),
color = Color.White color = Color.White
) )
}else{ } else {
Icon( Icon(
painter = painterResource(id = R.drawable.rider_pro_download), painter = painterResource(id = R.drawable.rider_pro_download),
contentDescription = "", contentDescription = "",
@@ -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(
.fillMaxWidth() modifier = Modifier
.weight(1f) .fillMaxWidth()
.pullRefresh(state)) { .weight(1f)
.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,36 +132,101 @@ 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)
LazyColumn( if (comments.loadState.refresh is LoadState.Loading) {
modifier = Modifier Box(
.weight(1f) modifier = Modifier
.fillMaxSize() .fillMaxWidth()
) { .weight(1f)
items(comments.itemCount) { index -> .padding(bottom = 48.dp)
comments[index]?.let { comment -> ,
CommentNoticeItem(comment) { contentAlignment = Alignment.Center
MessageListViewModel.updateReadStatus(comment.id) ) {
MessageListViewModel.viewModelScope.launch { Column(
// PostViewModel.postId = comment.postId.toString() horizontalAlignment = Alignment.CenterHorizontally
// PostViewModel.initData() ) {
navController.navigate( Text(
NavigationRoute.Post.route.replace( text = "Loading",
"{id}", fontSize = 18.sp
comment.postId.toString() )
) Spacer(modifier = Modifier.height(16.dp))
) LinearProgressIndicator(
} modifier = Modifier.width(160.dp),
color = Color(0xFFDA3832)
)
}
}
} else {
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxSize()
) {
items(comments.itemCount) { index ->
comments[index]?.let { comment ->
CommentNoticeItem(comment) {
MessageListViewModel.updateReadStatus(comment.id)
MessageListViewModel.viewModelScope.launch {
navController.navigate(
NavigationRoute.Post.route.replace(
"{id}",
comment.postId.toString()
)
)
}
}
} }
} }
} // handle load error
item { when {
Spacer(modifier = Modifier.height(72.dp)) comments.loadState.append is LoadState.Loading -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
LinearProgressIndicator(
modifier = Modifier.width(160.dp),
color = Color(0xFFDA3832)
)
}
}
}
comments.loadState.append is LoadState.Error -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.noRippleClickable {
comments.retry()
},
contentAlignment = Alignment.Center
) {
Text(
text = "Load comment error, click to retry",
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(72.dp))
}
} }
} }
} }

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) {
isLoading = true if (!isFirstLoad && !force) {
return
}
if (force) {
isLoading = true
}
isFirstLoad = false
val info = accountService.getMyNoticeInfo() val info = accountService.getMyNoticeInfo()
noticeInfo = info noticeInfo = info
viewModelScope.launch { viewModelScope.launch {
@@ -43,7 +49,7 @@ object MessageListViewModel : ViewModel() {
CommentPagingSource( CommentPagingSource(
CommentRemoteDataSource(commentService), CommentRemoteDataSource(commentService),
selfNotice = true, selfNotice = true,
order="latest" order = "latest"
) )
} }
).flow.cachedIn(viewModelScope).collectLatest { ).flow.cachedIn(viewModelScope).collectLatest {
@@ -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,26 +37,36 @@ 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
Pager( try {
config = PagingConfig(pageSize = 5, enablePlaceholders = false), Pager(
pagingSourceFactory = { config = PagingConfig(pageSize = 5, enablePlaceholders = false),
MomentPagingSource( pagingSourceFactory = {
MomentRemoteDataSource(momentService), MomentPagingSource(
author = profile.id MomentRemoteDataSource(momentService),
) author = profile.id
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_momentsFlow.value = it
} }
).flow.cachedIn(viewModelScope).collectLatest { }catch (e: Exception){
_momentsFlow.value = it 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,10 +172,14 @@ fun ProfilePage() {
) )
) { ) {
Box( Box(
modifier = Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)).shadow( modifier = Modifier
elevation = 20.dp .padding(16.dp)
).background(Color.White.copy(alpha = 0.7f)) .clip(RoundedCornerShape(8.dp))
){ .shadow(
elevation = 20.dp
)
.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),
contentDescription = "", contentDescription = "",
@@ -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
@@ -227,7 +229,7 @@ fun ProfilePage() {
} }
), ),
) )
} }
} }
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))

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,7 +93,13 @@ fun UserAuthScreen() {
} }
} catch (e: ServiceException) { } catch (e: ServiceException) {
// handle error // handle error
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
if (e.code == 12005) {
emailError = context.getString(R.string.error_invalidate_username_password)
passwordError = context.getString(R.string.error_invalidate_username_password)
} else {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}
} }
} }
@@ -181,26 +187,47 @@ fun UserAuthScreen() {
Row( 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 },
}, size = 18
colors = CheckboxDefaults.colors( )
checkedColor = Color.Black Text(
), stringResource(R.string.remember_me),
) modifier = Modifier.padding(start = 8.dp),
Text( fontSize = 12.sp
stringResource(R.string.remember_me), )
modifier = Modifier.padding(start = 8.dp), Spacer(modifier = Modifier.weight(1f))
fontSize = 12.sp Text(
) stringResource(R.string.forgot_password),
Spacer(modifier = Modifier.weight(1f)) fontSize = 12.sp,
Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable { 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,15 +50,19 @@ class CommentsViewModel(
fun reloadComment() { fun reloadComment() {
viewModelScope.launch { viewModelScope.launch {
Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false), try {
pagingSourceFactory = { Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
CommentPagingSource( pagingSourceFactory = {
CommentRemoteDataSource(commentService), CommentPagingSource(
postId = postId.toInt(), CommentRemoteDataSource(commentService),
order = order postId = postId.toInt(),
) order = order
}).flow.cachedIn(viewModelScope).collectLatest { )
_commentsFlow.value = it }).flow.cachedIn(viewModelScope).collectLatest {
_commentsFlow.value = it
}
} catch (e: Exception) {
e.printStackTrace()
} }
} }
} }
@@ -144,7 +149,12 @@ class CommentsViewModel(
fun deleteComment(commentId: Int) { fun deleteComment(commentId: Int) {
viewModelScope.launch { viewModelScope.launch {
commentService.DeleteComment(commentId) commentService.DeleteComment(commentId)
reloadComment() // 如果是刚刚创建的评论则从addedCommentList中删除
if (addedCommentList.any { it.id == commentId }) {
addedCommentList = addedCommentList.filter { it.id != commentId }
} else {
reloadComment()
}
} }
} }
@@ -153,16 +163,23 @@ class CommentsViewModel(
val currentPagingData = commentsFlow.value val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment -> val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) { if (comment.id == commentId) {
val subCommentList = commentService.getComments( try {
postId = postId.toInt(), subCommentLoadingMap[commentId] = true
parentCommentId = commentId, val subCommentList = commentService.getComments(
pageNumber = comment.replyPage + 1, postId = postId.toInt(),
pageSize = 3, parentCommentId = commentId,
).list pageNumber = comment.replyPage + 1,
return@map comment.copy( pageSize = 3,
reply = comment.reply.plus(subCommentList), ).list
replyPage = comment.replyPage + 1 return@map comment.copy(
) reply = comment.reply.plus(subCommentList),
replyPage = comment.replyPage + 1
)
} catch (e: Exception) {
return@map comment.copy()
} finally {
subCommentLoadingMap[commentId] = false
}
} }
comment 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
@@ -94,14 +102,19 @@ fun NewPostScreen() {
NewPostTextField("Share your adventure…", NewPostViewModel.textContent) { NewPostTextField("Share your adventure…", NewPostViewModel.textContent) {
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,44 +241,97 @@ 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)
) )
Box( DraggableGrid(
modifier = Modifier.fillMaxWidth() items = NewPostViewModel.imageUriList,
) { onMove = { from, to ->
FlowRow( NewPostViewModel.imageUriList = NewPostViewModel.imageUriList.toMutableList().apply {
modifier = Modifier add(to, removeAt(from))
.fillMaxWidth() }
.padding(18.dp), },
verticalArrangement = Arrangement.spacedBy(8.dp), lockedIndices = listOf(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
model.imageUriList.forEach {
CustomAsyncImage(
context,
it,
contentDescription = "Image",
modifier = Modifier
.size(110.dp)
.drawBehind { ),
drawRoundRect(color = Color(0xFF999999), style = stroke) onDragModeEnd = {},
}.noRippleClickable { onDragModeStart = {},
navController.navigate(NavigationRoute.NewPostImageGrid.route) additionalItems = listOf(
},
contentScale = ContentScale.Crop ),
) { item, isDrag ->
Box(
modifier = Modifier
) {
CustomAsyncImage(
LocalContext.current,
item,
contentDescription = "Image",
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.noRippleClickable {
navController.navigate(NavigationRoute.NewPostImageGrid.route)
},
contentScale = ContentScale.Crop
)
if (isDrag) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0x66000000))
) )
} }
}
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
.fillMaxWidth()
.padding(18.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// items(model.imageUriList.size) { index ->
// val uri = model.imageUriList[index]
// Box(
// modifier = Modifier
// .drawBehind {
// drawRoundRect(color = Color(0xFF999999), style = stroke)
// }
// ) {
// CustomAsyncImage(
// context,
// uri,
// contentDescription = "Image",
// modifier = Modifier
// .fillMaxWidth().aspectRatio(1f)
// .noRippleClickable {
// navController.navigate(NavigationRoute.NewPostImageGrid.route)
// },
// contentScale = ContentScale.Crop
// )
// }
// }
item {
Box( Box(
modifier = Modifier modifier = Modifier
.size(110.dp) .fillMaxWidth()
.aspectRatio(1f)
.drawBehind { .drawBehind {
drawRoundRect(color = Color(0xFF999999), style = stroke) drawRoundRect(color = Color(0xFF999999), style = stroke)
} }
.noRippleClickable{ .noRippleClickable {
pickImagesLauncher.launch("image/*") pickImagesLauncher.launch("image/*")
}, },
) { ) {
@@ -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

File diff suppressed because it is too large Load Diff

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() {
moment = service.getMomentById(postId.toInt()) try {
// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString()) moment = service.getMomentById(postId.toInt())
} 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>