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-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")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
package com.aiosman.riderpro.ui.composables
|
||||||
|
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -24,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -74,17 +77,26 @@ fun EditCommentBottomModal(
|
|||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
fontStyle = FontStyle.Italic
|
fontStyle = FontStyle.Italic
|
||||||
)
|
)
|
||||||
|
Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty ->
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_send),
|
painter = rememberUpdatedState(
|
||||||
|
if (isNotEmpty) painterResource(id = R.drawable.rider_pro_send) else painterResource(
|
||||||
|
id = R.drawable.rider_pro_send_disable
|
||||||
|
)
|
||||||
|
).value,
|
||||||
contentDescription = "Send",
|
contentDescription = "Send",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(32.dp)
|
.size(32.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
|
if (text.isNotEmpty()){
|
||||||
onSend(text)
|
onSend(text)
|
||||||
text = ""
|
text = ""
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
if (replyComment != null) {
|
if (replyComment != null) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -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
|
package com.aiosman.riderpro.ui.composables
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -112,16 +116,23 @@ fun TextInputField(
|
|||||||
.height(16.dp),
|
.height(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (error != null) {
|
AnimatedVisibility(
|
||||||
|
visible = error != null,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.rider_pro_input_error),
|
painter = painterResource(id = R.mipmap.rider_pro_input_error),
|
||||||
contentDescription = "Error",
|
contentDescription = "Error",
|
||||||
modifier = Modifier.size(8.dp)
|
modifier = Modifier.size(8.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(4.dp))
|
Spacer(modifier = Modifier.size(4.dp))
|
||||||
Text(error, color = Color(0xFFE53935), fontSize = 12.sp)
|
AnimatedContent(targetState = error) { targetError ->
|
||||||
|
Text(targetError ?: "", color = Color(0xFFE53935), fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
isFollowing = user.isFollowing
|
isFollowing = user.isFollowing
|
||||||
) {
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
if (user.isFollowing) {
|
||||||
|
model.unFollowUser(user.id)
|
||||||
|
} else {
|
||||||
model.followUser(user.id)
|
model.followUser(user.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,4 +57,5 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -25,12 +25,14 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.aiosman.riderpro.AppState
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.riderpro.LocalNavController
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.riderpro.R
|
||||||
import com.aiosman.riderpro.ui.NavigationRoute
|
import com.aiosman.riderpro.ui.NavigationRoute
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
|
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
|
||||||
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
||||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.riderpro.ui.composables.FollowButton
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -47,11 +49,14 @@ fun FollowerNoticeScreen() {
|
|||||||
var dataFlow = model.followerItemsFlow
|
var dataFlow = model.followerItemsFlow
|
||||||
var followers = dataFlow.collectAsLazyPagingItems()
|
var followers = dataFlow.collectAsLazyPagingItems()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
|
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
model.reload()
|
||||||
model.updateNotice()
|
model.updateNotice()
|
||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -114,30 +119,43 @@ fun FollowItem(
|
|||||||
) {
|
) {
|
||||||
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||||
}
|
}
|
||||||
if (!isFollowing) {
|
if (userId != AppState.UserId) {
|
||||||
Box(
|
FollowButton(
|
||||||
modifier = Modifier.noRippleClickable {
|
isFollowing = isFollowing,
|
||||||
onFollow()
|
onFollowClick = onFollow,
|
||||||
}
|
fontSize = 14.sp,
|
||||||
) {
|
imageModifier = Modifier
|
||||||
Image(
|
.width(100.dp)
|
||||||
painter = painterResource(id = R.drawable.follow_bg),
|
|
||||||
contentDescription = "Follow",
|
|
||||||
modifier = Modifier
|
|
||||||
.width(79.dp)
|
|
||||||
.height(24.dp)
|
.height(24.dp)
|
||||||
)
|
)
|
||||||
Text(
|
|
||||||
"FOLLOW",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = Color(0xFFFFFFFF),
|
|
||||||
modifier = Modifier.align(
|
|
||||||
Alignment.Center
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier.noRippleClickable {
|
||||||
|
// onFollow()
|
||||||
|
// }
|
||||||
|
// ) {
|
||||||
|
// Image(
|
||||||
|
// painter = painterResource(id = R.drawable.follow_bg),
|
||||||
|
// contentDescription = "Follow",
|
||||||
|
// modifier = Modifier
|
||||||
|
// .width(79.dp)
|
||||||
|
// .height(24.dp)
|
||||||
|
// )
|
||||||
|
// Text(
|
||||||
|
// text = if (isFollowing) {
|
||||||
|
// stringResource(R.string.following_upper)
|
||||||
|
// } else {
|
||||||
|
// stringResource(R.string.follow_upper)
|
||||||
|
// },
|
||||||
|
// fontSize = 14.sp,
|
||||||
|
// color = Color(0xFFFFFFFF),
|
||||||
|
// modifier = Modifier.align(
|
||||||
|
// Alignment.Center
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowingListScreen(userId: Int) {
|
fun FollowingListScreen(userId: Int) {
|
||||||
val model = FollowerListViewModel
|
val model = FollowingListViewModel
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.loadData(userId)
|
model.loadData(userId)
|
||||||
@@ -47,6 +47,9 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
isFollowing = user.isFollowing
|
isFollowing = user.isFollowing
|
||||||
) {
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
if (user.isFollowing) {
|
||||||
|
model.unfollowUser(user.id)
|
||||||
|
} else {
|
||||||
model.followUser(user.id)
|
model.followUser(user.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,4 +57,5 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.LinearProgressIndicator
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
@@ -43,6 +44,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.riderpro.LocalNavController
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.riderpro.R
|
||||||
@@ -52,6 +54,9 @@ import com.aiosman.riderpro.ui.NavigationRoute
|
|||||||
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
||||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeViewModel
|
||||||
|
import com.aiosman.riderpro.ui.follower.FollowerNoticeViewModel
|
||||||
|
import com.aiosman.riderpro.ui.like.LikeNoticeViewModel
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.riderpro.ui.post.PostViewModel
|
import com.aiosman.riderpro.ui.post.PostViewModel
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
@@ -70,7 +75,7 @@ fun NotificationsScreen() {
|
|||||||
var comments = dataFlow.collectAsLazyPagingItems()
|
var comments = dataFlow.collectAsLazyPagingItems()
|
||||||
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
|
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
|
||||||
MessageListViewModel.viewModelScope.launch {
|
MessageListViewModel.viewModelScope.launch {
|
||||||
MessageListViewModel.initData()
|
MessageListViewModel.initData(force = true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -81,10 +86,12 @@ fun NotificationsScreen() {
|
|||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
Box(modifier = Modifier
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.pullRefresh(state)) {
|
.pullRefresh(state)
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
@@ -99,6 +106,13 @@ fun NotificationsScreen() {
|
|||||||
R.drawable.rider_pro_like,
|
R.drawable.rider_pro_like,
|
||||||
stringResource(R.string.like_upper)
|
stringResource(R.string.like_upper)
|
||||||
) {
|
) {
|
||||||
|
if (MessageListViewModel.likeNoticeCount > 0) {
|
||||||
|
// 刷新点赞消息列表
|
||||||
|
LikeNoticeViewModel.isFirstLoad = true
|
||||||
|
// 清除点赞消息数量
|
||||||
|
MessageListViewModel.clearLikeNoticeCount()
|
||||||
|
}
|
||||||
|
|
||||||
navController.navigate(NavigationRoute.Likes.route)
|
navController.navigate(NavigationRoute.Likes.route)
|
||||||
}
|
}
|
||||||
NotificationIndicator(
|
NotificationIndicator(
|
||||||
@@ -106,6 +120,11 @@ fun NotificationsScreen() {
|
|||||||
R.drawable.rider_pro_followers,
|
R.drawable.rider_pro_followers,
|
||||||
stringResource(R.string.followers_upper)
|
stringResource(R.string.followers_upper)
|
||||||
) {
|
) {
|
||||||
|
if (MessageListViewModel.followNoticeCount > 0) {
|
||||||
|
// 刷新关注消息列表
|
||||||
|
FollowerNoticeViewModel.isFirstLoad = true
|
||||||
|
MessageListViewModel.clearFollowNoticeCount()
|
||||||
|
}
|
||||||
navController.navigate(NavigationRoute.Followers.route)
|
navController.navigate(NavigationRoute.Followers.route)
|
||||||
}
|
}
|
||||||
NotificationIndicator(
|
NotificationIndicator(
|
||||||
@@ -113,11 +132,41 @@ fun NotificationsScreen() {
|
|||||||
R.drawable.rider_pro_favoriate,
|
R.drawable.rider_pro_favoriate,
|
||||||
stringResource(R.string.favourites_upper)
|
stringResource(R.string.favourites_upper)
|
||||||
) {
|
) {
|
||||||
|
if (MessageListViewModel.favouriteNoticeCount > 0) {
|
||||||
|
// 刷新收藏消息列表
|
||||||
|
FavouriteNoticeViewModel.isFirstLoad = true
|
||||||
|
MessageListViewModel.clearFavouriteNoticeCount()
|
||||||
|
}
|
||||||
navController.navigate(NavigationRoute.FavouritesScreen.route)
|
navController.navigate(NavigationRoute.FavouritesScreen.route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp))
|
HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp))
|
||||||
NotificationCounterItem(MessageListViewModel.commentNoticeCount)
|
NotificationCounterItem(MessageListViewModel.commentNoticeCount)
|
||||||
|
if (comments.loadState.refresh is LoadState.Loading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(bottom = 48.dp)
|
||||||
|
,
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Loading",
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.width(160.dp),
|
||||||
|
color = Color(0xFFDA3832)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -128,8 +177,6 @@ fun NotificationsScreen() {
|
|||||||
CommentNoticeItem(comment) {
|
CommentNoticeItem(comment) {
|
||||||
MessageListViewModel.updateReadStatus(comment.id)
|
MessageListViewModel.updateReadStatus(comment.id)
|
||||||
MessageListViewModel.viewModelScope.launch {
|
MessageListViewModel.viewModelScope.launch {
|
||||||
// PostViewModel.postId = comment.postId.toString()
|
|
||||||
// PostViewModel.initData()
|
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.Post.route.replace(
|
NavigationRoute.Post.route.replace(
|
||||||
"{id}",
|
"{id}",
|
||||||
@@ -137,7 +184,43 @@ fun NotificationsScreen() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// handle load error
|
||||||
|
when {
|
||||||
|
comments.loadState.append is LoadState.Loading -> {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(64.dp)
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.width(160.dp),
|
||||||
|
color = Color(0xFFDA3832)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comments.loadState.append is LoadState.Error -> {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(64.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
comments.retry()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Load comment error, click to retry",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,6 +229,7 @@ fun NotificationsScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
PullRefreshIndicator(
|
PullRefreshIndicator(
|
||||||
MessageListViewModel.isLoading,
|
MessageListViewModel.isLoading,
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -31,9 +31,15 @@ object MessageListViewModel : ViewModel() {
|
|||||||
private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
|
private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
|
||||||
val commentItemsFlow = _commentItemsFlow.asStateFlow()
|
val commentItemsFlow = _commentItemsFlow.asStateFlow()
|
||||||
var isLoading by mutableStateOf(false)
|
var isLoading by mutableStateOf(false)
|
||||||
|
var isFirstLoad = true
|
||||||
suspend fun initData() {
|
suspend fun initData(force: Boolean = false) {
|
||||||
|
if (!isFirstLoad && !force) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (force) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
}
|
||||||
|
isFirstLoad = false
|
||||||
val info = accountService.getMyNoticeInfo()
|
val info = accountService.getMyNoticeInfo()
|
||||||
noticeInfo = info
|
noticeInfo = info
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,14 +37,20 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
private var _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
private var _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
||||||
var momentsFlow = _momentsFlow.asStateFlow()
|
var momentsFlow = _momentsFlow.asStateFlow()
|
||||||
var refreshing by mutableStateOf(false)
|
var refreshing by mutableStateOf(false)
|
||||||
|
var firstLoad = true
|
||||||
fun loadProfile(pullRefresh: Boolean = false) {
|
fun loadProfile(pullRefresh: Boolean = false) {
|
||||||
|
if (!firstLoad && !pullRefresh) {
|
||||||
|
return
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (pullRefresh){
|
if (pullRefresh){
|
||||||
refreshing = true
|
refreshing = true
|
||||||
}
|
}
|
||||||
|
firstLoad = false
|
||||||
profile = accountService.getMyAccountProfile()
|
profile = accountService.getMyAccountProfile()
|
||||||
val profile = accountService.getMyAccountProfile()
|
val profile = accountService.getMyAccountProfile()
|
||||||
refreshing = false
|
refreshing = false
|
||||||
|
try {
|
||||||
Pager(
|
Pager(
|
||||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||||
pagingSourceFactory = {
|
pagingSourceFactory = {
|
||||||
@@ -56,6 +62,10 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
).flow.cachedIn(viewModelScope).collectLatest {
|
).flow.cachedIn(viewModelScope).collectLatest {
|
||||||
_momentsFlow.value = it
|
_momentsFlow.value = it
|
||||||
}
|
}
|
||||||
|
}catch (e: Exception){
|
||||||
|
Log.e("MyProfileViewModel", "loadProfile: ", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.shadow(
|
||||||
elevation = 20.dp
|
elevation = 20.dp
|
||||||
).background(Color.White.copy(alpha = 0.7f))
|
)
|
||||||
){
|
.background(Color.White.copy(alpha = 0.7f))
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||||
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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
// 非空
|
// 非空
|
||||||
|
|||||||
@@ -93,9 +93,15 @@ fun UserAuthScreen() {
|
|||||||
}
|
}
|
||||||
} catch (e: ServiceException) {
|
} catch (e: ServiceException) {
|
||||||
// handle error
|
// handle error
|
||||||
|
|
||||||
|
if (e.code == 12005) {
|
||||||
|
emailError = context.getString(R.string.error_invalidate_username_password)
|
||||||
|
passwordError = context.getString(R.string.error_invalidate_username_password)
|
||||||
|
} else {
|
||||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,15 +187,12 @@ fun UserAuthScreen() {
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
com.aiosman.riderpro.ui.composables.Checkbox(
|
||||||
Checkbox(
|
|
||||||
checked = rememberMe,
|
checked = rememberMe,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
rememberMe = it
|
rememberMe = it
|
||||||
},
|
},
|
||||||
colors = CheckboxDefaults.colors(
|
size = 18
|
||||||
checkedColor = Color.Black
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.remember_me),
|
stringResource(R.string.remember_me),
|
||||||
@@ -197,10 +200,34 @@ fun UserAuthScreen() {
|
|||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable {
|
Text(
|
||||||
|
stringResource(R.string.forgot_password),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
navController.navigate(NavigationRoute.ResetPassword.route)
|
navController.navigate(NavigationRoute.ResetPassword.route)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
// CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
||||||
|
// Checkbox(
|
||||||
|
// checked = rememberMe,
|
||||||
|
// onCheckedChange = {
|
||||||
|
// rememberMe = it
|
||||||
|
// },
|
||||||
|
// colors = CheckboxDefaults.colors(
|
||||||
|
// checkedColor = Color.Black
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// Text(
|
||||||
|
// stringResource(R.string.remember_me),
|
||||||
|
// modifier = Modifier.padding(start = 8.dp),
|
||||||
|
// fontSize = 12.sp
|
||||||
|
// )
|
||||||
|
// Spacer(modifier = Modifier.weight(1f))
|
||||||
|
// Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable {
|
||||||
|
// navController.navigate(NavigationRoute.ResetPassword.route)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(64.dp))
|
Spacer(modifier = Modifier.height(64.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class CommentsViewModel(
|
|||||||
val commentsFlow = _commentsFlow.asStateFlow()
|
val commentsFlow = _commentsFlow.asStateFlow()
|
||||||
var order: String by mutableStateOf("like")
|
var order: String by mutableStateOf("like")
|
||||||
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
|
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
|
||||||
|
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预加载,在跳转到 PostScreen 之前设置好内容
|
* 预加载,在跳转到 PostScreen 之前设置好内容
|
||||||
@@ -49,6 +50,7 @@ class CommentsViewModel(
|
|||||||
|
|
||||||
fun reloadComment() {
|
fun reloadComment() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||||
pagingSourceFactory = {
|
pagingSourceFactory = {
|
||||||
CommentPagingSource(
|
CommentPagingSource(
|
||||||
@@ -59,6 +61,9 @@ class CommentsViewModel(
|
|||||||
}).flow.cachedIn(viewModelScope).collectLatest {
|
}).flow.cachedIn(viewModelScope).collectLatest {
|
||||||
_commentsFlow.value = it
|
_commentsFlow.value = it
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,15 +149,22 @@ class CommentsViewModel(
|
|||||||
fun deleteComment(commentId: Int) {
|
fun deleteComment(commentId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
commentService.DeleteComment(commentId)
|
commentService.DeleteComment(commentId)
|
||||||
|
// 如果是刚刚创建的评论,则从addedCommentList中删除
|
||||||
|
if (addedCommentList.any { it.id == commentId }) {
|
||||||
|
addedCommentList = addedCommentList.filter { it.id != commentId }
|
||||||
|
} else {
|
||||||
reloadComment()
|
reloadComment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadMoreSubComments(commentId: Int) {
|
fun loadMoreSubComments(commentId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val currentPagingData = commentsFlow.value
|
val currentPagingData = commentsFlow.value
|
||||||
val updatedPagingData = currentPagingData.map { comment ->
|
val updatedPagingData = currentPagingData.map { comment ->
|
||||||
if (comment.id == commentId) {
|
if (comment.id == commentId) {
|
||||||
|
try {
|
||||||
|
subCommentLoadingMap[commentId] = true
|
||||||
val subCommentList = commentService.getComments(
|
val subCommentList = commentService.getComments(
|
||||||
postId = postId.toInt(),
|
postId = postId.toInt(),
|
||||||
parentCommentId = commentId,
|
parentCommentId = commentId,
|
||||||
@@ -163,6 +175,11 @@ class CommentsViewModel(
|
|||||||
reply = comment.reply.plus(subCommentList),
|
reply = comment.reply.plus(subCommentList),
|
||||||
replyPage = comment.replyPage + 1
|
replyPage = comment.replyPage + 1
|
||||||
)
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@map comment.copy()
|
||||||
|
} finally {
|
||||||
|
subCommentLoadingMap[commentId] = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
comment
|
comment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
DraggableGrid(
|
||||||
|
items = NewPostViewModel.imageUriList,
|
||||||
|
onMove = { from, to ->
|
||||||
|
NewPostViewModel.imageUriList = NewPostViewModel.imageUriList.toMutableList().apply {
|
||||||
|
add(to, removeAt(from))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lockedIndices = listOf(
|
||||||
|
|
||||||
|
),
|
||||||
|
onDragModeEnd = {},
|
||||||
|
onDragModeStart = {},
|
||||||
|
additionalItems = listOf(
|
||||||
|
|
||||||
|
),
|
||||||
|
|
||||||
|
) { item, isDrag ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(18.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
) {
|
||||||
model.imageUriList.forEach {
|
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context,
|
LocalContext.current,
|
||||||
it,
|
item,
|
||||||
contentDescription = "Image",
|
contentDescription = "Image",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(110.dp)
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
.drawBehind {
|
.noRippleClickable {
|
||||||
drawRoundRect(color = Color(0xFF999999), style = stroke)
|
|
||||||
}.noRippleClickable {
|
|
||||||
navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
||||||
},
|
},
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
}
|
if (isDrag) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(110.dp)
|
.fillMaxSize()
|
||||||
|
.background(Color(0x66000000))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(18.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// items(model.imageUriList.size) { index ->
|
||||||
|
// val uri = model.imageUriList[index]
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .drawBehind {
|
||||||
|
// drawRoundRect(color = Color(0xFF999999), style = stroke)
|
||||||
|
// }
|
||||||
|
// ) {
|
||||||
|
// CustomAsyncImage(
|
||||||
|
// context,
|
||||||
|
// uri,
|
||||||
|
// contentDescription = "Image",
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxWidth().aspectRatio(1f)
|
||||||
|
// .noRippleClickable {
|
||||||
|
// navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
||||||
|
// },
|
||||||
|
// contentScale = ContentScale.Crop
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
drawRoundRect(color = Color(0xFF999999), style = stroke)
|
drawRoundRect(color = Color(0xFF999999), style = stroke)
|
||||||
}
|
}
|
||||||
.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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.aiosman.riderpro.ui.post
|
package com.aiosman.riderpro.ui.post
|
||||||
|
|
||||||
import android.util.Log
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -14,7 +16,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -24,15 +25,11 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.material.LinearProgressIndicator
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
@@ -46,22 +43,23 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -69,7 +67,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.aiosman.riderpro.AppState
|
import com.aiosman.riderpro.AppState
|
||||||
import com.aiosman.riderpro.LocalAnimatedContentScope
|
import com.aiosman.riderpro.LocalAnimatedContentScope
|
||||||
@@ -82,12 +80,14 @@ import com.aiosman.riderpro.entity.MomentImageEntity
|
|||||||
import com.aiosman.riderpro.exp.formatPostTime
|
import com.aiosman.riderpro.exp.formatPostTime
|
||||||
import com.aiosman.riderpro.exp.timeAgo
|
import com.aiosman.riderpro.exp.timeAgo
|
||||||
import com.aiosman.riderpro.ui.NavigationRoute
|
import com.aiosman.riderpro.ui.NavigationRoute
|
||||||
import com.aiosman.riderpro.ui.comment.CommentModalViewModel
|
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
||||||
import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon
|
import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon
|
||||||
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
|
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
|
||||||
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
||||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.riderpro.ui.composables.CustomClickableText
|
||||||
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal
|
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal
|
||||||
|
import com.aiosman.riderpro.ui.composables.FollowButton
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
|
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
@@ -113,28 +113,67 @@ fun PostScreen(
|
|||||||
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
var showCommentModal by remember { mutableStateOf(false) }
|
var showCommentModal by remember { mutableStateOf(false) }
|
||||||
|
var commentModalState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
var editCommentModalState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true
|
||||||
|
)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.initData()
|
viewModel.initData()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCommentMenu) {
|
if (showCommentMenu) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showCommentMenu = false
|
showCommentMenu = false
|
||||||
},
|
},
|
||||||
containerColor = Color.White,
|
containerColor = Color.White,
|
||||||
sheetState = rememberModalBottomSheetState(
|
sheetState = commentModalState,
|
||||||
skipPartiallyExpanded = true
|
|
||||||
),
|
|
||||||
dragHandle = {},
|
dragHandle = {},
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
windowInsets = WindowInsets(0)
|
windowInsets = WindowInsets(0)
|
||||||
) {
|
) {
|
||||||
CommentMenuModal(
|
CommentMenuModal(
|
||||||
onDeleteClick = {
|
onDeleteClick = {
|
||||||
|
scope.launch {
|
||||||
|
commentModalState.hide()
|
||||||
showCommentMenu = false
|
showCommentMenu = false
|
||||||
|
}
|
||||||
contextComment?.let {
|
contextComment?.let {
|
||||||
viewModel.deleteComment(it.id)
|
viewModel.deleteComment(it.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
commentEntity = contextComment,
|
||||||
|
onCloseClick = {
|
||||||
|
scope.launch {
|
||||||
|
commentModalState.hide()
|
||||||
|
showCommentMenu = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isSelf = AppState.UserId?.toLong() == contextComment?.author,
|
||||||
|
onLikeClick = {
|
||||||
|
scope.launch {
|
||||||
|
commentModalState.hide()
|
||||||
|
showCommentMenu = false
|
||||||
|
}
|
||||||
|
contextComment?.let {
|
||||||
|
viewModel.viewModelScope.launch {
|
||||||
|
if (it.liked) {
|
||||||
|
viewModel.unlikeComment(it.id)
|
||||||
|
} else {
|
||||||
|
viewModel.likeComment(it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
onReplyClick = {
|
||||||
|
scope.launch {
|
||||||
|
commentModalState.hide()
|
||||||
|
showCommentMenu = false
|
||||||
|
replyComment = contextComment
|
||||||
|
showCommentModal = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -177,6 +216,8 @@ fun PostScreen(
|
|||||||
content = it
|
content = it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editCommentModalState.hide()
|
||||||
showCommentModal = false
|
showCommentModal = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +227,7 @@ fun PostScreen(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
if (!viewModel.isError) {
|
||||||
PostBottomBar(
|
PostBottomBar(
|
||||||
onLikeClick = {
|
onLikeClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -212,6 +254,8 @@ fun PostScreen(
|
|||||||
momentEntity = viewModel.moment
|
momentEntity = viewModel.moment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
it
|
it
|
||||||
Column(
|
Column(
|
||||||
@@ -220,14 +264,35 @@ fun PostScreen(
|
|||||||
.background(Color.White)
|
.background(Color.White)
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
|
if (viewModel.isError) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(16.dp)
|
||||||
|
) {
|
||||||
|
NoticeScreenHeader("Post", moreIcon = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Umm, post are not found.",
|
||||||
|
style = TextStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
Header(
|
Header(
|
||||||
avatar = viewModel.avatar,
|
avatar = viewModel.avatar,
|
||||||
nickname = viewModel.nickname,
|
nickname = viewModel.nickname,
|
||||||
userId = viewModel.moment?.authorId,
|
userId = viewModel.moment?.authorId,
|
||||||
isFollowing = viewModel.accountProfileEntity?.isFollowing ?: false,
|
isFollowing = viewModel.moment?.followStatus == true,
|
||||||
onFollowClick = {
|
onFollowClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (viewModel.accountProfileEntity?.isFollowing == true) {
|
if (viewModel.moment?.followStatus == true) {
|
||||||
viewModel.unfollowUser()
|
viewModel.unfollowUser()
|
||||||
} else {
|
} else {
|
||||||
viewModel.followUser()
|
viewModel.followUser()
|
||||||
@@ -295,9 +360,9 @@ fun PostScreen(
|
|||||||
item {
|
item {
|
||||||
CommentContent(
|
CommentContent(
|
||||||
viewModel = commentsViewModel,
|
viewModel = commentsViewModel,
|
||||||
onLongClick = {
|
onLongClick = { comment ->
|
||||||
showCommentMenu = true
|
showCommentMenu = true
|
||||||
contextComment = it
|
contextComment = comment
|
||||||
},
|
},
|
||||||
onReply = { parentComment, _, _, _ ->
|
onReply = { parentComment, _, _, _ ->
|
||||||
replyComment = parentComment
|
replyComment = parentComment
|
||||||
@@ -311,6 +376,8 @@ fun PostScreen(
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -327,6 +394,10 @@ fun CommentContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (item in addedTopLevelComment) {
|
for (item in addedTopLevelComment) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = true,
|
||||||
|
enter = fadeIn() + slideInVertically()
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
@@ -341,11 +412,8 @@ fun CommentContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = { comment ->
|
||||||
if (AppState.UserId != item.id) {
|
onLongClick(comment)
|
||||||
return@CommentItem
|
|
||||||
}
|
|
||||||
onLongClick(item)
|
|
||||||
},
|
},
|
||||||
onReply = { parentComment, _, _, _ ->
|
onReply = { parentComment, _, _, _ ->
|
||||||
onReply(
|
onReply(
|
||||||
@@ -364,6 +432,7 @@ fun CommentContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (idx in 0 until commentsPagging.itemCount) {
|
for (idx in 0 until commentsPagging.itemCount) {
|
||||||
val item = commentsPagging[idx] ?: return
|
val item = commentsPagging[idx] ?: return
|
||||||
@@ -381,11 +450,8 @@ fun CommentContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = { comment ->
|
||||||
if (AppState.UserId != item.id) {
|
onLongClick(comment)
|
||||||
return@CommentItem
|
|
||||||
}
|
|
||||||
onLongClick(item)
|
|
||||||
},
|
},
|
||||||
onReply = { parentComment, _, _, _ ->
|
onReply = { parentComment, _, _, _ ->
|
||||||
onReply(
|
onReply(
|
||||||
@@ -404,6 +470,67 @@ fun CommentContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (commentsPagging.loadState.refresh is LoadState.Loading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().height(120.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.width(160.dp),
|
||||||
|
color = Color(0xFFDA3832)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Loading...",
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (commentsPagging.loadState.append is LoadState.Loading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().height(64.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.width(160.dp),
|
||||||
|
color = Color(0xFFDA3832)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (commentsPagging.loadState.refresh is LoadState.Error) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().height(120.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Failed to load comments,click to retry",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
viewModel.reloadComment()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (commentsPagging.loadState.append is LoadState.Error) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().height(64.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Failed to load more comments,click to retry",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
commentsPagging.retry()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -488,30 +615,12 @@ fun Header(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(text = nickname ?: "", fontWeight = FontWeight.Bold)
|
Text(text = nickname ?: "", fontWeight = FontWeight.Bold)
|
||||||
if (AppState.UserId != userId) {
|
if (AppState.UserId != userId) {
|
||||||
Box(
|
FollowButton(
|
||||||
modifier = Modifier
|
isFollowing = isFollowing,
|
||||||
.height(20.dp)
|
onFollowClick = onFollowClick,
|
||||||
.wrapContentWidth()
|
imageModifier = Modifier.height(18.dp).width(80.dp),
|
||||||
.padding(start = 6.dp)
|
fontSize = 12.sp
|
||||||
.noRippleClickable {
|
|
||||||
onFollowClick()
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier.height(18.dp),
|
|
||||||
painter = painterResource(id = R.drawable.follow_bg),
|
|
||||||
contentDescription = ""
|
|
||||||
)
|
)
|
||||||
Text(
|
|
||||||
text = if (isFollowing) stringResource(R.string.following_upper) else stringResource(
|
|
||||||
R.string.follow_upper
|
|
||||||
),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = Color.White,
|
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (AppState.UserId == userId) {
|
if (AppState.UserId == userId) {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
@@ -581,7 +690,7 @@ fun PostImageView(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
if(images.size > 1){
|
if (images.size > 1) {
|
||||||
images.forEachIndexed { index, _ ->
|
images.forEachIndexed { index, _ ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -626,47 +735,6 @@ fun PostDetails(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CommentsSection(
|
|
||||||
lazyPagingItems: LazyPagingItems<CommentEntity>,
|
|
||||||
scrollState: LazyListState = rememberLazyListState(),
|
|
||||||
onLike: (CommentEntity) -> Unit,
|
|
||||||
onLongClick: (CommentEntity) -> Unit,
|
|
||||||
onWillCollapse: (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
state = scrollState, modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.padding(start = 16.dp, end = 16.dp)
|
|
||||||
) {
|
|
||||||
items(lazyPagingItems.itemCount) { idx ->
|
|
||||||
val item = lazyPagingItems[idx] ?: return@items
|
|
||||||
CommentItem(
|
|
||||||
item,
|
|
||||||
onLike = {
|
|
||||||
onLike(item)
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
onLongClick(item)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect scroll direction and update showCollapseContent
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
LaunchedEffect(scrollState) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
snapshotFlow { scrollState.firstVisibleItemScrollOffset }
|
|
||||||
.collect { offset ->
|
|
||||||
Log.d("scroll", "offset: $offset")
|
|
||||||
onWillCollapse(offset == 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CommentItem(
|
fun CommentItem(
|
||||||
@@ -680,7 +748,7 @@ fun CommentItem(
|
|||||||
replyUserAvatar: String?
|
replyUserAvatar: String?
|
||||||
) -> Unit = { _, _, _, _ -> },
|
) -> Unit = { _, _, _, _ -> },
|
||||||
onLoadMoreSubComments: ((CommentEntity) -> Unit)? = {},
|
onLoadMoreSubComments: ((CommentEntity) -> Unit)? = {},
|
||||||
onLongClick: () -> Unit = {},
|
onLongClick: (CommentEntity) -> Unit = {},
|
||||||
addedCommentList: List<CommentEntity> = emptyList()
|
addedCommentList: List<CommentEntity> = emptyList()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -688,12 +756,6 @@ fun CommentItem(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.combinedClickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = {},
|
|
||||||
onLongClick = onLongClick
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.padding(vertical = 8.dp)) {
|
Row(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
Box(
|
Box(
|
||||||
@@ -719,7 +781,15 @@ fun CommentItem(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.combinedClickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick(commentEntity)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
) {
|
) {
|
||||||
Text(text = commentEntity.name, fontWeight = FontWeight.W600, fontSize = 14.sp)
|
Text(text = commentEntity.name, fontWeight = FontWeight.W600, fontSize = 14.sp)
|
||||||
Row {
|
Row {
|
||||||
@@ -741,8 +811,10 @@ fun CommentItem(
|
|||||||
pop()
|
pop()
|
||||||
}
|
}
|
||||||
append(" ${commentEntity.comment}")
|
append(" ${commentEntity.comment}")
|
||||||
|
|
||||||
}
|
}
|
||||||
ClickableText(
|
Box {
|
||||||
|
CustomClickableText(
|
||||||
text = annotatedText,
|
text = annotatedText,
|
||||||
onClick = { offset ->
|
onClick = { offset ->
|
||||||
annotatedText.getStringAnnotations(
|
annotatedText.getStringAnnotations(
|
||||||
@@ -759,15 +831,29 @@ fun CommentItem(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style = TextStyle(fontSize = 14.sp),
|
style = TextStyle(fontSize = 14.sp),
|
||||||
maxLines = Int.MAX_VALUE,
|
onLongPress = {
|
||||||
softWrap = true
|
onLongClick(commentEntity)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = commentEntity.comment,
|
text = commentEntity.comment,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = Int.MAX_VALUE,
|
maxLines = Int.MAX_VALUE,
|
||||||
softWrap = true
|
softWrap = true,
|
||||||
|
modifier = Modifier.combinedClickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick(
|
||||||
|
commentEntity
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -780,7 +866,7 @@ fun CommentItem(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
if (AppState.UserId?.toLong() != commentEntity.author) {
|
if (AppState.UserId?.toLong() != commentEntity.author) {
|
||||||
Text(
|
Text(
|
||||||
text = "Reply",
|
text = stringResource(R.string.reply),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = Color.Gray,
|
color = Color.Gray,
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
@@ -832,13 +918,15 @@ fun CommentItem(
|
|||||||
isChild = true,
|
isChild = true,
|
||||||
onLike = onLike,
|
onLike = onLike,
|
||||||
onReply = onReply,
|
onReply = onReply,
|
||||||
onLongClick = onLongClick
|
onLongClick = { comment ->
|
||||||
|
onLongClick(comment)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) {
|
if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) {
|
||||||
val remaining = commentEntity.replyCount - commentEntity.reply.size
|
val remaining = commentEntity.replyCount - commentEntity.reply.size
|
||||||
Text(
|
Text(
|
||||||
text = "View $remaining more replies",
|
text = stringResource(R.string.view_more_reply, remaining),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = Color(0xFF6F94AE),
|
color = Color(0xFF6F94AE),
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
@@ -851,7 +939,6 @@ fun CommentItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PostBottomBar(
|
fun PostBottomBar(
|
||||||
onCreateCommentClick: () -> Unit = {},
|
onCreateCommentClick: () -> Unit = {},
|
||||||
@@ -956,12 +1043,16 @@ fun PostMenuModal(
|
|||||||
onDeleteClick()
|
onDeleteClick()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Image(painter = painterResource(id = R.drawable.rider_pro_moment_delete), contentDescription = "",modifier = Modifier.size(24.dp))
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_moment_delete),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Delete",
|
text = stringResource(R.string.delete),
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@@ -971,20 +1062,12 @@ fun PostMenuModal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommentMenuModal(
|
fun MenuActionItem(
|
||||||
onDeleteClick: () -> Unit = {}
|
icon: Int? = null,
|
||||||
|
text: String,
|
||||||
|
content: @Composable() (() -> Unit)? = null,
|
||||||
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(160.dp)
|
|
||||||
.padding(vertical = 47.dp, horizontal = 20.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
@@ -994,21 +1077,151 @@ fun CommentMenuModal(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
onDeleteClick()
|
onClick()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Image(painter = painterResource(id = R.drawable.rider_pro_moment_delete), contentDescription = "",modifier = Modifier.size(24.dp))
|
content?.invoke()
|
||||||
|
if (icon != null) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = icon),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Delete",
|
text = text,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论菜单弹窗
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CommentMenuModal(
|
||||||
|
onDeleteClick: () -> Unit = {},
|
||||||
|
commentEntity: CommentEntity? = null,
|
||||||
|
onCloseClick: () -> Unit = {},
|
||||||
|
onLikeClick: () -> Unit = {},
|
||||||
|
onReplyClick: () -> Unit = {},
|
||||||
|
isSelf: Boolean = false
|
||||||
|
) {
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
|
||||||
|
fun copyToClipboard() {
|
||||||
|
commentEntity?.let {
|
||||||
|
clipboard.setText(
|
||||||
|
AnnotatedString(
|
||||||
|
text = it.comment,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 24.dp, horizontal = 20.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.comment), fontSize = 18.sp, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
commentEntity?.let {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color(0xffeeeeee))
|
||||||
|
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = it.avatar,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "Avatar",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
it.name,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
it.comment,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 32.dp),
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (isSelf) {
|
||||||
|
MenuActionItem(
|
||||||
|
icon = R.drawable.rider_pro_moment_delete,
|
||||||
|
text = stringResource(R.string.delete)
|
||||||
|
) {
|
||||||
|
onDeleteClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(48.dp))
|
||||||
|
}
|
||||||
|
MenuActionItem(
|
||||||
|
icon = R.drawable.rider_pro_copy,
|
||||||
|
text = stringResource(R.string.copy)
|
||||||
|
) {
|
||||||
|
copyToClipboard()
|
||||||
|
onCloseClick()
|
||||||
|
}
|
||||||
|
commentEntity?.let {
|
||||||
|
Spacer(modifier = Modifier.width(48.dp))
|
||||||
|
MenuActionItem(
|
||||||
|
text = stringResource(R.string.like),
|
||||||
|
content = {
|
||||||
|
AnimatedLikeIcon(
|
||||||
|
liked = it.liked,
|
||||||
|
onClick = onLikeClick,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
onCloseClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isSelf) {
|
||||||
|
Spacer(modifier = Modifier.width(48.dp))
|
||||||
|
MenuActionItem(
|
||||||
|
icon = R.drawable.rider_pro_comment,
|
||||||
|
text = stringResource(R.string.reply)
|
||||||
|
) {
|
||||||
|
onReplyClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -28,24 +28,20 @@ class PostViewModel(
|
|||||||
var moment by mutableStateOf<MomentEntity?>(null)
|
var moment by mutableStateOf<MomentEntity?>(null)
|
||||||
var accountService: AccountService = AccountServiceImpl()
|
var accountService: AccountService = AccountServiceImpl()
|
||||||
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
|
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
|
||||||
|
var isError by mutableStateOf(false)
|
||||||
|
|
||||||
/**
|
|
||||||
* 预加载,在跳转到 PostScreen 之前设置好内容
|
|
||||||
*/
|
|
||||||
fun preTransit(momentEntity: MomentEntity?) {
|
|
||||||
this.moment = momentEntity
|
|
||||||
this.nickname = momentEntity?.nickname ?: ""
|
|
||||||
this.commentsViewModel = CommentsViewModel(postId)
|
|
||||||
commentsViewModel.preTransit()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reloadComment() {
|
fun reloadComment() {
|
||||||
commentsViewModel.reloadComment()
|
commentsViewModel.reloadComment()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun initData() {
|
suspend fun initData() {
|
||||||
|
try {
|
||||||
moment = service.getMomentById(postId.toInt())
|
moment = service.getMomentById(postId.toInt())
|
||||||
// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString())
|
} catch (e: Exception) {
|
||||||
|
isError = true
|
||||||
|
return
|
||||||
|
}
|
||||||
commentsViewModel.reloadComment()
|
commentsViewModel.reloadComment()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,16 +102,16 @@ class PostViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun followUser() {
|
suspend fun followUser() {
|
||||||
accountProfileEntity?.let {
|
moment?.let {
|
||||||
userService.followUser(it.id.toString())
|
userService.followUser(it.authorId.toString())
|
||||||
accountProfileEntity = accountProfileEntity?.copy(isFollowing = true)
|
moment = moment?.copy(followStatus = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun unfollowUser() {
|
suspend fun unfollowUser() {
|
||||||
accountProfileEntity?.let {
|
moment?.let {
|
||||||
userService.unFollowUser(it.id.toString())
|
userService.unFollowUser(it.authorId.toString())
|
||||||
accountProfileEntity = accountProfileEntity?.copy(isFollowing = false)
|
moment = moment?.copy(followStatus = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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_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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user