Merge remote-tracking branch 'refs/remotes/origin/main'
This commit is contained in:
@@ -101,6 +101,5 @@ dependencies {
|
||||
implementation("com.google.firebase:firebase-analytics")
|
||||
implementation("com.google.firebase:firebase-perf")
|
||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.aiosman.riderpro.ui.composables
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -24,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -74,16 +77,25 @@ fun EditCommentBottomModal(
|
||||
fontSize = 20.sp,
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_send),
|
||||
contentDescription = "Send",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.noRippleClickable {
|
||||
onSend(text)
|
||||
text = ""
|
||||
},
|
||||
)
|
||||
Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty ->
|
||||
Image(
|
||||
painter = rememberUpdatedState(
|
||||
if (isNotEmpty) painterResource(id = R.drawable.rider_pro_send) else painterResource(
|
||||
id = R.drawable.rider_pro_send_disable
|
||||
)
|
||||
).value,
|
||||
contentDescription = "Send",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.noRippleClickable {
|
||||
if (text.isNotEmpty()){
|
||||
onSend(text)
|
||||
text = ""
|
||||
}
|
||||
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
if (replyComment != null) {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.aiosman.riderpro.ui.composables
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -112,16 +116,23 @@ fun TextInputField(
|
||||
.height(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (error != null) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.rider_pro_input_error),
|
||||
contentDescription = "Error",
|
||||
modifier = Modifier.size(8.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Text(error, color = Color(0xFFE53935), fontSize = 12.sp)
|
||||
AnimatedVisibility(
|
||||
visible = error != null,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.rider_pro_input_error),
|
||||
contentDescription = "Error",
|
||||
modifier = Modifier.size(8.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
AnimatedContent(targetState = error) { targetError ->
|
||||
Text(targetError ?: "", color = Color(0xFFE53935), fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -30,6 +30,7 @@ fun FavouriteNoticeScreen() {
|
||||
var dataFlow = model.favouriteItemsFlow
|
||||
var favourites = dataFlow.collectAsLazyPagingItems()
|
||||
LaunchedEffect(Unit) {
|
||||
model.reload()
|
||||
model.updateNotice()
|
||||
}
|
||||
StatusBarMaskLayout(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -47,7 +47,11 @@ fun FollowerListScreen(userId: Int) {
|
||||
isFollowing = user.isFollowing
|
||||
) {
|
||||
scope.launch {
|
||||
model.followUser(user.id)
|
||||
if (user.isFollowing) {
|
||||
model.unFollowUser(user.id)
|
||||
} else {
|
||||
model.followUser(user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,12 +25,14 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.riderpro.AppState
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.R
|
||||
import com.aiosman.riderpro.ui.NavigationRoute
|
||||
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
|
||||
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.riderpro.ui.composables.FollowButton
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -47,11 +49,14 @@ fun FollowerNoticeScreen() {
|
||||
var dataFlow = model.followerItemsFlow
|
||||
var followers = dataFlow.collectAsLazyPagingItems()
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
model.reload()
|
||||
model.updateNotice()
|
||||
}
|
||||
LazyColumn(
|
||||
@@ -114,30 +119,43 @@ fun FollowItem(
|
||||
) {
|
||||
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
}
|
||||
if (!isFollowing) {
|
||||
Box(
|
||||
modifier = Modifier.noRippleClickable {
|
||||
onFollow()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.follow_bg),
|
||||
contentDescription = "Follow",
|
||||
modifier = Modifier
|
||||
.width(79.dp)
|
||||
.height(24.dp)
|
||||
)
|
||||
Text(
|
||||
"FOLLOW",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFFFFFFFF),
|
||||
modifier = Modifier.align(
|
||||
Alignment.Center
|
||||
)
|
||||
)
|
||||
}
|
||||
if (userId != AppState.UserId) {
|
||||
FollowButton(
|
||||
isFollowing = isFollowing,
|
||||
onFollowClick = onFollow,
|
||||
fontSize = 14.sp,
|
||||
imageModifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Box(
|
||||
// modifier = Modifier.noRippleClickable {
|
||||
// onFollow()
|
||||
// }
|
||||
// ) {
|
||||
// Image(
|
||||
// painter = painterResource(id = R.drawable.follow_bg),
|
||||
// contentDescription = "Follow",
|
||||
// modifier = Modifier
|
||||
// .width(79.dp)
|
||||
// .height(24.dp)
|
||||
// )
|
||||
// Text(
|
||||
// text = if (isFollowing) {
|
||||
// stringResource(R.string.following_upper)
|
||||
// } else {
|
||||
// stringResource(R.string.follow_upper)
|
||||
// },
|
||||
// fontSize = 14.sp,
|
||||
// color = Color(0xFFFFFFFF),
|
||||
// modifier = Modifier.align(
|
||||
// Alignment.Center
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun FollowingListScreen(userId: Int) {
|
||||
val model = FollowerListViewModel
|
||||
val model = FollowingListViewModel
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
model.loadData(userId)
|
||||
@@ -47,7 +47,11 @@ fun FollowingListScreen(userId: Int) {
|
||||
isFollowing = user.isFollowing
|
||||
) {
|
||||
scope.launch {
|
||||
model.followUser(user.id)
|
||||
if (user.isFollowing) {
|
||||
model.unfollowUser(user.id)
|
||||
} else {
|
||||
model.followUser(user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
@@ -43,6 +44,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.R
|
||||
@@ -52,6 +54,9 @@ import com.aiosman.riderpro.ui.NavigationRoute
|
||||
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeViewModel
|
||||
import com.aiosman.riderpro.ui.follower.FollowerNoticeViewModel
|
||||
import com.aiosman.riderpro.ui.like.LikeNoticeViewModel
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.riderpro.ui.post.PostViewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
@@ -70,7 +75,7 @@ fun NotificationsScreen() {
|
||||
var comments = dataFlow.collectAsLazyPagingItems()
|
||||
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
|
||||
MessageListViewModel.viewModelScope.launch {
|
||||
MessageListViewModel.initData()
|
||||
MessageListViewModel.initData(force = true)
|
||||
}
|
||||
})
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -81,10 +86,12 @@ fun NotificationsScreen() {
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Box(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.pullRefresh(state)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.pullRefresh(state)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
@@ -99,6 +106,13 @@ fun NotificationsScreen() {
|
||||
R.drawable.rider_pro_like,
|
||||
stringResource(R.string.like_upper)
|
||||
) {
|
||||
if (MessageListViewModel.likeNoticeCount > 0) {
|
||||
// 刷新点赞消息列表
|
||||
LikeNoticeViewModel.isFirstLoad = true
|
||||
// 清除点赞消息数量
|
||||
MessageListViewModel.clearLikeNoticeCount()
|
||||
}
|
||||
|
||||
navController.navigate(NavigationRoute.Likes.route)
|
||||
}
|
||||
NotificationIndicator(
|
||||
@@ -106,6 +120,11 @@ fun NotificationsScreen() {
|
||||
R.drawable.rider_pro_followers,
|
||||
stringResource(R.string.followers_upper)
|
||||
) {
|
||||
if (MessageListViewModel.followNoticeCount > 0) {
|
||||
// 刷新关注消息列表
|
||||
FollowerNoticeViewModel.isFirstLoad = true
|
||||
MessageListViewModel.clearFollowNoticeCount()
|
||||
}
|
||||
navController.navigate(NavigationRoute.Followers.route)
|
||||
}
|
||||
NotificationIndicator(
|
||||
@@ -113,36 +132,101 @@ fun NotificationsScreen() {
|
||||
R.drawable.rider_pro_favoriate,
|
||||
stringResource(R.string.favourites_upper)
|
||||
) {
|
||||
if (MessageListViewModel.favouriteNoticeCount > 0) {
|
||||
// 刷新收藏消息列表
|
||||
FavouriteNoticeViewModel.isFirstLoad = true
|
||||
MessageListViewModel.clearFavouriteNoticeCount()
|
||||
}
|
||||
navController.navigate(NavigationRoute.FavouritesScreen.route)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp))
|
||||
NotificationCounterItem(MessageListViewModel.commentNoticeCount)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
items(comments.itemCount) { index ->
|
||||
comments[index]?.let { comment ->
|
||||
CommentNoticeItem(comment) {
|
||||
MessageListViewModel.updateReadStatus(comment.id)
|
||||
MessageListViewModel.viewModelScope.launch {
|
||||
// PostViewModel.postId = comment.postId.toString()
|
||||
// PostViewModel.initData()
|
||||
navController.navigate(
|
||||
NavigationRoute.Post.route.replace(
|
||||
"{id}",
|
||||
comment.postId.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
if (comments.loadState.refresh is LoadState.Loading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(bottom = 48.dp)
|
||||
,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Loading",
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.width(160.dp),
|
||||
color = Color(0xFFDA3832)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
items(comments.itemCount) { index ->
|
||||
comments[index]?.let { comment ->
|
||||
CommentNoticeItem(comment) {
|
||||
MessageListViewModel.updateReadStatus(comment.id)
|
||||
MessageListViewModel.viewModelScope.launch {
|
||||
navController.navigate(
|
||||
NavigationRoute.Post.route.replace(
|
||||
"{id}",
|
||||
comment.postId.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(72.dp))
|
||||
// handle load error
|
||||
when {
|
||||
comments.loadState.append is LoadState.Loading -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.width(160.dp),
|
||||
color = Color(0xFFDA3832)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comments.loadState.append is LoadState.Error -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.noRippleClickable {
|
||||
comments.retry()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Load comment error, click to retry",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(72.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
isLoading = true
|
||||
var isFirstLoad = true
|
||||
suspend fun initData(force: Boolean = false) {
|
||||
if (!isFirstLoad && !force) {
|
||||
return
|
||||
}
|
||||
if (force) {
|
||||
isLoading = true
|
||||
}
|
||||
isFirstLoad = false
|
||||
val info = accountService.getMyNoticeInfo()
|
||||
noticeInfo = info
|
||||
viewModelScope.launch {
|
||||
@@ -43,7 +49,7 @@ object MessageListViewModel : ViewModel() {
|
||||
CommentPagingSource(
|
||||
CommentRemoteDataSource(commentService),
|
||||
selfNotice = true,
|
||||
order="latest"
|
||||
order = "latest"
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
@@ -51,6 +57,7 @@ object MessageListViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
|
||||
}
|
||||
|
||||
val likeNoticeCount
|
||||
@@ -80,4 +87,15 @@ object MessageListViewModel : ViewModel() {
|
||||
updateIsRead(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearLikeNoticeCount() {
|
||||
noticeInfo = noticeInfo?.copy(likeCount = 0)
|
||||
}
|
||||
|
||||
fun clearFollowNoticeCount() {
|
||||
noticeInfo = noticeInfo?.copy(followCount = 0)
|
||||
}
|
||||
fun clearFavouriteNoticeCount() {
|
||||
noticeInfo = noticeInfo?.copy(favoriteCount = 0)
|
||||
}
|
||||
}
|
||||
@@ -37,26 +37,36 @@ 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
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
author = profile.id
|
||||
)
|
||||
try {
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
author = profile.id
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_momentsFlow.value = it
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_momentsFlow.value = it
|
||||
}catch (e: Exception){
|
||||
Log.e("MyProfileViewModel", "loadProfile: ", e)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,8 +162,6 @@ fun ProfilePage() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
@@ -174,10 +172,14 @@ fun ProfilePage() {
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)).shadow(
|
||||
elevation = 20.dp
|
||||
).background(Color.White.copy(alpha = 0.7f))
|
||||
){
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.shadow(
|
||||
elevation = 20.dp
|
||||
)
|
||||
.background(Color.White.copy(alpha = 0.7f))
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||
contentDescription = "",
|
||||
@@ -191,7 +193,7 @@ fun ProfilePage() {
|
||||
com.aiosman.riderpro.ui.composables.DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
width = 300,
|
||||
width = 250,
|
||||
menuItems = listOf(
|
||||
MenuItem(
|
||||
stringResource(R.string.logout),
|
||||
@@ -217,7 +219,7 @@ fun ProfilePage() {
|
||||
}
|
||||
},
|
||||
MenuItem(
|
||||
"Favourite",
|
||||
stringResource(R.string.favourites),
|
||||
R.drawable.rider_pro_favourite
|
||||
) {
|
||||
expanded = false
|
||||
@@ -227,7 +229,7 @@ fun ProfilePage() {
|
||||
}
|
||||
),
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
// 非空
|
||||
|
||||
@@ -93,7 +93,13 @@ fun UserAuthScreen() {
|
||||
}
|
||||
} catch (e: ServiceException) {
|
||||
// handle error
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
|
||||
if (e.code == 12005) {
|
||||
emailError = context.getString(R.string.error_invalidate_username_password)
|
||||
passwordError = context.getString(R.string.error_invalidate_username_password)
|
||||
} else {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,26 +187,47 @@ fun UserAuthScreen() {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
||||
Checkbox(
|
||||
checked = rememberMe,
|
||||
onCheckedChange = {
|
||||
rememberMe = it
|
||||
},
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = Color.Black
|
||||
),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.remember_me),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable {
|
||||
com.aiosman.riderpro.ui.composables.Checkbox(
|
||||
checked = rememberMe,
|
||||
onCheckedChange = {
|
||||
rememberMe = it
|
||||
},
|
||||
size = 18
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.remember_me),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
stringResource(R.string.forgot_password),
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
navController.navigate(NavigationRoute.ResetPassword.route)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
// CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
||||
// Checkbox(
|
||||
// checked = rememberMe,
|
||||
// onCheckedChange = {
|
||||
// rememberMe = it
|
||||
// },
|
||||
// colors = CheckboxDefaults.colors(
|
||||
// checkedColor = Color.Black
|
||||
// ),
|
||||
// )
|
||||
// Text(
|
||||
// stringResource(R.string.remember_me),
|
||||
// modifier = Modifier.padding(start = 8.dp),
|
||||
// fontSize = 12.sp
|
||||
// )
|
||||
// Spacer(modifier = Modifier.weight(1f))
|
||||
// Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable {
|
||||
// navController.navigate(NavigationRoute.ResetPassword.route)
|
||||
// })
|
||||
// }
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
ActionButton(
|
||||
|
||||
@@ -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,15 +50,19 @@ class CommentsViewModel(
|
||||
|
||||
fun reloadComment() {
|
||||
viewModelScope.launch {
|
||||
Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
CommentPagingSource(
|
||||
CommentRemoteDataSource(commentService),
|
||||
postId = postId.toInt(),
|
||||
order = order
|
||||
)
|
||||
}).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_commentsFlow.value = it
|
||||
try {
|
||||
Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
CommentPagingSource(
|
||||
CommentRemoteDataSource(commentService),
|
||||
postId = postId.toInt(),
|
||||
order = order
|
||||
)
|
||||
}).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_commentsFlow.value = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +149,12 @@ class CommentsViewModel(
|
||||
fun deleteComment(commentId: Int) {
|
||||
viewModelScope.launch {
|
||||
commentService.DeleteComment(commentId)
|
||||
reloadComment()
|
||||
// 如果是刚刚创建的评论,则从addedCommentList中删除
|
||||
if (addedCommentList.any { it.id == commentId }) {
|
||||
addedCommentList = addedCommentList.filter { it.id != commentId }
|
||||
} else {
|
||||
reloadComment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,16 +163,23 @@ class CommentsViewModel(
|
||||
val currentPagingData = commentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { comment ->
|
||||
if (comment.id == commentId) {
|
||||
val subCommentList = commentService.getComments(
|
||||
postId = postId.toInt(),
|
||||
parentCommentId = commentId,
|
||||
pageNumber = comment.replyPage + 1,
|
||||
pageSize = 3,
|
||||
).list
|
||||
return@map comment.copy(
|
||||
reply = comment.reply.plus(subCommentList),
|
||||
replyPage = comment.replyPage + 1
|
||||
)
|
||||
try {
|
||||
subCommentLoadingMap[commentId] = true
|
||||
val subCommentList = commentService.getComments(
|
||||
postId = postId.toInt(),
|
||||
parentCommentId = commentId,
|
||||
pageNumber = comment.replyPage + 1,
|
||||
pageSize = 3,
|
||||
).list
|
||||
return@map comment.copy(
|
||||
reply = comment.reply.plus(subCommentList),
|
||||
replyPage = comment.replyPage + 1
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return@map comment.copy()
|
||||
} finally {
|
||||
subCommentLoadingMap[commentId] = false
|
||||
}
|
||||
}
|
||||
comment
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.aiosman.riderpro.ui.post
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -14,10 +13,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -25,9 +24,12 @@ import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
@@ -41,7 +43,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@@ -50,17 +54,19 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.compose.AsyncImage
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.R
|
||||
import com.aiosman.riderpro.ui.NavigationRoute
|
||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.riderpro.ui.composables.DraggableGrid
|
||||
import com.aiosman.riderpro.ui.composables.RelPostCard
|
||||
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
|
||||
@Preview
|
||||
@@ -76,7 +82,9 @@ fun NewPostScreen() {
|
||||
}
|
||||
StatusBarMaskLayout(
|
||||
darkIcons = true,
|
||||
modifier = Modifier.fillMaxSize().background(Color.White)
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -94,14 +102,19 @@ fun NewPostScreen() {
|
||||
NewPostTextField("Share your adventure…", NewPostViewModel.textContent) {
|
||||
NewPostViewModel.textContent = it
|
||||
}
|
||||
Column (
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
model.relMoment?.let {
|
||||
Text("Share with")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(color = Color(0xFFEEEEEE)).padding(24.dp)
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(color = Color(0xFFEEEEEE))
|
||||
.padding(24.dp)
|
||||
) {
|
||||
RelPostCard(
|
||||
momentEntity = it,
|
||||
@@ -188,6 +201,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Unit) {
|
||||
|
||||
@@ -211,7 +225,8 @@ fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Uni
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
|
||||
|
||||
@Composable
|
||||
fun AddImageGrid() {
|
||||
val navController = LocalNavController.current
|
||||
@@ -226,44 +241,97 @@ fun AddImageGrid() {
|
||||
}
|
||||
}
|
||||
|
||||
val takePictureLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.TakePicture()
|
||||
) { success ->
|
||||
if (success) {
|
||||
model.imageUriList += model.currentPhotoUri.toString()
|
||||
}
|
||||
}
|
||||
|
||||
val stroke = Stroke(
|
||||
width = 2f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
model.imageUriList.forEach {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
it,
|
||||
contentDescription = "Image",
|
||||
modifier = Modifier
|
||||
.size(110.dp)
|
||||
DraggableGrid(
|
||||
items = NewPostViewModel.imageUriList,
|
||||
onMove = { from, to ->
|
||||
NewPostViewModel.imageUriList = NewPostViewModel.imageUriList.toMutableList().apply {
|
||||
add(to, removeAt(from))
|
||||
}
|
||||
},
|
||||
lockedIndices = listOf(
|
||||
|
||||
.drawBehind {
|
||||
drawRoundRect(color = Color(0xFF999999), style = stroke)
|
||||
}.noRippleClickable {
|
||||
navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
),
|
||||
onDragModeEnd = {},
|
||||
onDragModeStart = {},
|
||||
additionalItems = listOf(
|
||||
|
||||
),
|
||||
|
||||
) { item, isDrag ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
LocalContext.current,
|
||||
item,
|
||||
contentDescription = "Image",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.noRippleClickable {
|
||||
navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
if (isDrag) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0x66000000))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(18.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// items(model.imageUriList.size) { index ->
|
||||
// val uri = model.imageUriList[index]
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .drawBehind {
|
||||
// drawRoundRect(color = Color(0xFF999999), style = stroke)
|
||||
// }
|
||||
// ) {
|
||||
// CustomAsyncImage(
|
||||
// context,
|
||||
// uri,
|
||||
// contentDescription = "Image",
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth().aspectRatio(1f)
|
||||
// .noRippleClickable {
|
||||
// navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
||||
// },
|
||||
// contentScale = ContentScale.Crop
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(110.dp)
|
||||
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.drawBehind {
|
||||
drawRoundRect(color = Color(0xFF999999), style = stroke)
|
||||
}
|
||||
.noRippleClickable{
|
||||
.noRippleClickable {
|
||||
pickImagesLauncher.launch("image/*")
|
||||
},
|
||||
) {
|
||||
@@ -275,10 +343,37 @@ fun AddImageGrid() {
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.drawBehind {
|
||||
drawRoundRect(color = Color(0xFF999999), style = stroke)
|
||||
}
|
||||
.noRippleClickable {
|
||||
val photoFile = File(context.cacheDir, "photo.jpg")
|
||||
val photoUri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
photoFile
|
||||
)
|
||||
model.currentPhotoUri = photoUri
|
||||
takePictureLauncher.launch(photoUri)
|
||||
},
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_camera),
|
||||
contentDescription = "Take Photo",
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.Center),
|
||||
colorFilter = ColorFilter.tint(Color.Gray)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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() {
|
||||
moment = service.getMomentById(postId.toInt())
|
||||
// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString())
|
||||
try {
|
||||
moment = service.getMomentById(postId.toInt())
|
||||
} catch (e: Exception) {
|
||||
isError = true
|
||||
return
|
||||
}
|
||||
commentsViewModel.reloadComment()
|
||||
}
|
||||
|
||||
@@ -106,16 +102,16 @@ class PostViewModel(
|
||||
}
|
||||
|
||||
suspend fun followUser() {
|
||||
accountProfileEntity?.let {
|
||||
userService.followUser(it.id.toString())
|
||||
accountProfileEntity = accountProfileEntity?.copy(isFollowing = true)
|
||||
moment?.let {
|
||||
userService.followUser(it.authorId.toString())
|
||||
moment = moment?.copy(followStatus = true)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unfollowUser() {
|
||||
accountProfileEntity?.let {
|
||||
userService.unFollowUser(it.id.toString())
|
||||
accountProfileEntity = accountProfileEntity?.copy(isFollowing = false)
|
||||
moment?.let {
|
||||
userService.unFollowUser(it.authorId.toString())
|
||||
moment = moment?.copy(followStatus = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
app/src/main/res/drawable/rider_pro_copy.xml
Normal file
5
app/src/main/res/drawable/rider_pro_copy.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/rider_pro_send_disable.xml
Normal file
13
app/src/main/res/drawable/rider_pro_send_disable.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user