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

This commit is contained in:
2024-09-14 16:39:29 +08:00
20 changed files with 708 additions and 141 deletions

View File

@@ -6,6 +6,7 @@ import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody
import com.aiosman.riderpro.data.api.LoginUserRequestBody
import com.aiosman.riderpro.data.api.RegisterMessageChannelRequestBody
import com.aiosman.riderpro.data.api.RegisterRequestBody
import com.aiosman.riderpro.data.api.ResetPasswordRequestBody
import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody
import com.aiosman.riderpro.entity.AccountFavouriteEntity
import com.aiosman.riderpro.entity.AccountLikeEntity
@@ -339,6 +340,9 @@ interface AccountService {
suspend fun updateNotice(payload: UpdateNoticeRequestBody)
suspend fun registerMessageChannel(client: String, identifier: String)
suspend fun resetPassword(email: String)
}
class AccountServiceImpl : AccountService {
@@ -455,4 +459,12 @@ class AccountServiceImpl : AccountService {
ApiClient.api.registerMessageChannel(RegisterMessageChannelRequestBody(client, identifier))
}
override suspend fun resetPassword(email: String) {
ApiClient.api.resetPassword(
ResetPasswordRequestBody(
username = email
)
)
}
}

View File

@@ -120,7 +120,8 @@ interface MomentService {
author: Int? = null,
timelineId: Int? = null,
contentSearch: String? = null,
trend: Boolean? = false
trend: Boolean? = false,
favoriteUserId: Int? = null
): ListContainer<MomentEntity>
/**

View File

@@ -4,7 +4,6 @@ import android.icu.text.SimpleDateFormat
import android.icu.util.TimeZone
import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.ConstVars
import com.aiosman.riderpro.data.ServiceException
import com.auth0.android.jwt.JWT
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor

View File

@@ -93,6 +93,11 @@ data class RegisterMessageChannelRequestBody(
val identifier: String,
)
data class ResetPasswordRequestBody(
@SerializedName("username")
val username: String,
)
interface RiderProAPI {
@POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@@ -117,6 +122,7 @@ interface RiderProAPI {
@Query("contentSearch") contentSearch: String? = null,
@Query("postUser") postUser: Int? = null,
@Query("trend") trend: String? = null,
@Query("favouriteUserId") favouriteUserId: Int? = null,
): Response<ListContainer<Moment>>
@Multipart
@@ -270,4 +276,8 @@ interface RiderProAPI {
@Path("id") id: Int
): Response<Unit>
@POST("account/my/password/reset")
suspend fun resetPassword(
@Body body: ResetPasswordRequestBody
): Response<Unit>
}

View File

@@ -25,7 +25,8 @@ class MomentPagingSource(
private val author: Int? = null,
private val timelineId: Int? = null,
private val contentSearch: String? = null,
private val trend: Boolean? = false
private val trend: Boolean? = false,
private val favoriteUserId: Int? = null
) : PagingSource<Int, MomentEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
return try {
@@ -35,7 +36,8 @@ class MomentPagingSource(
author = author,
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend
trend = trend,
favoriteUserId = favoriteUserId
)
LoadResult.Page(
@@ -62,14 +64,16 @@ class MomentRemoteDataSource(
author: Int?,
timelineId: Int?,
contentSearch: String?,
trend: Boolean?
trend: Boolean?,
favoriteUserId: Int?
): ListContainer<MomentEntity> {
return momentService.getMoments(
pageNumber = pageNumber,
author = author,
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend
trend = trend,
favoriteUserId = favoriteUserId
)
}
}
@@ -82,14 +86,16 @@ class MomentServiceImpl() : MomentService {
author: Int?,
timelineId: Int?,
contentSearch: String?,
trend: Boolean?
trend: Boolean?,
favoriteUserId: Int?
): ListContainer<MomentEntity> {
return momentBackend.fetchMomentItems(
pageNumber = pageNumber,
author = author,
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend
trend = trend,
favoriteUserId = favoriteUserId
)
}
@@ -136,7 +142,8 @@ class MomentBackend {
author: Int? = null,
timelineId: Int?,
contentSearch: String?,
trend: Boolean?
trend: Boolean?,
favoriteUserId: Int? = null
): ListContainer<MomentEntity> {
val resp = ApiClient.api.getPosts(
pageSize = DataBatchSize,
@@ -144,7 +151,8 @@ class MomentBackend {
timelineId = timelineId,
authorId = author,
contentSearch = contentSearch,
trend = if (trend == true) "true" else ""
trend = if (trend == true) "true" else "",
favouriteUserId = favoriteUserId
)
val body = resp.body() ?: throw ServiceException("Failed to get moments")
return ListContainer(

View File

@@ -28,7 +28,9 @@ import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.ui.account.AccountEditScreen2
import com.aiosman.riderpro.ui.account.ResetPasswordScreen
import com.aiosman.riderpro.ui.comment.CommentsScreen
import com.aiosman.riderpro.ui.favourite.FavouriteListPage
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeScreen
import com.aiosman.riderpro.ui.follower.FollowerListScreen
import com.aiosman.riderpro.ui.follower.FollowerNoticeScreen
@@ -80,6 +82,8 @@ sealed class NavigationRoute(
data object Search : NavigationRoute("Search")
data object FollowerList : NavigationRoute("FollowerList/{id}")
data object FollowingList : NavigationRoute("FollowingList/{id}")
data object ResetPassword : NavigationRoute("ResetPassword")
data object FavouriteList : NavigationRoute("FavouriteList")
}
@@ -311,7 +315,20 @@ fun NavigationController(
FollowingListScreen(it.arguments?.getInt("id")!!)
}
}
composable(route = NavigationRoute.ResetPassword.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
ResetPasswordScreen()
}
}
composable(route = NavigationRoute.FavouriteList.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
FavouriteListPage()
}
}
}

View File

@@ -0,0 +1,160 @@
package com.aiosman.riderpro.ui.account
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.ActionButton
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.composables.TextInputField
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@Composable
fun ResetPasswordScreen() {
var username by remember { mutableStateOf("") }
val accountService: AccountService = AccountServiceImpl()
val scope = rememberCoroutineScope()
val context = LocalContext.current
var isSendSuccess by remember { mutableStateOf<Boolean?>(null) }
var isLoading by remember { mutableStateOf(false) }
val navController = LocalNavController.current
fun resetPassword() {
scope.launch {
isLoading = true
try {
accountService.resetPassword(username)
isSendSuccess = true
} catch (e: Exception) {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
isSendSuccess = false
} finally {
isLoading = false
}
}
}
Column(
modifier = Modifier.fillMaxSize()
) {
StatusBarSpacer()
Box(
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
top = 16.dp,
bottom = 0.dp
)
) {
NoticeScreenHeader(
"RECOVER ACCOUNT",
moreIcon = false
)
}
Column(
modifier = Modifier.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(36.dp))
if (isSendSuccess != null) {
if (isSendSuccess!!) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Reset password email has been sent to your email address",
style = TextStyle(
color = Color(0xFF333333),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
)
} else {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Failed to send reset password email",
style = TextStyle(
color = Color(0xFF333333),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
)
}
Spacer(modifier = Modifier.height(40.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.noRippleClickable {
navController.popBackStack()
}
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_back),
contentDescription = "Back",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
androidx.compose.material3.Text(
stringResource(R.string.back_upper),
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
} else {
Spacer(modifier = Modifier.height(120.dp))
TextInputField(
text = username,
onValueChange = { username = it },
label = stringResource(R.string.login_email_label),
hint = stringResource(R.string.text_hint_email),
enabled = !isLoading
)
Spacer(modifier = Modifier.height(72.dp))
if (isLoading) {
CircularProgressIndicator()
} else {
ActionButton(
modifier = Modifier
.width(345.dp)
.height(48.dp),
text = "Recover Account",
backgroundImage = R.mipmap.rider_pro_signup_red_bg
) {
resetPassword()
}
}
}
}
}
}

View File

@@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontVariation.width
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.aiosman.riderpro.R
@@ -29,6 +30,7 @@ data class MenuItem(
val action: () -> Unit
)
@Composable
fun DropdownMenu(
expanded: Boolean = false,
@@ -47,8 +49,8 @@ fun DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = Modifier
.apply {
width?.let { width(width.dp) }
.let {
if (width != null) it.width(width.dp) else it
}
.background(Color.White)
) {
@@ -67,7 +69,7 @@ fun DropdownMenu(
if (width != null) {
Spacer(modifier = Modifier.weight(1f))
}else{
} else {
Spacer(modifier = Modifier.width(16.dp))
}
Icon(

View File

@@ -61,31 +61,6 @@ fun CustomAsyncImage(
val localContext = LocalContext.current
val imageLoader = getImageLoader(context ?: localContext)
// val blurBitmap = remember(blurHash) {
// blurHash?.let {
// BlurHashDecoder.decode(
// blurHash = it,
// width = DEFAULT_HASHED_BITMAP_WIDTH,
// height = DEFAULT_HASHED_BITMAP_HEIGHT
// )
// }
// }
// var bitmap by remember(imageUrl) { mutableStateOf<Bitmap?>(null) }
//
// LaunchedEffect(imageUrl) {
// if (bitmap == null) {
// val request = ImageRequest.Builder(context)
// .data(imageUrl)
// .crossfade(3000)
// .build()
//
// val result = withContext(Dispatchers.IO) {
// (imageLoader.execute(request) as? SuccessResult)?.drawable?.toBitmap()
// }
// bitmap = result
// }
// }
AsyncImage(
model = ImageRequest.Builder(context ?: localContext)

View File

@@ -43,7 +43,8 @@ fun TextInputField(
password: Boolean = false,
label: String? = null,
hint: String? = null,
error: String? = null
error: String? = null,
enabled: Boolean = true
) {
var showPassword by remember { mutableStateOf(!password) }
var isFocused by remember { mutableStateOf(false) }
@@ -74,6 +75,7 @@ fun TextInputField(
),
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
singleLine = true,
enabled = enabled
)
if (password) {
Image(

View File

@@ -0,0 +1,121 @@
package com.aiosman.riderpro.ui.favourite
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FavouriteListPage() {
val model = FavouriteListViewModel
var dataFlow = model.favouriteMomentsFlow
var moments = dataFlow.collectAsLazyPagingItems()
val context = LocalContext.current
val navController = LocalNavController.current
val state = rememberPullRefreshState(FavouriteListViewModel.isLoading, onRefresh = {
model.refreshPager(force = true)
})
Column(
modifier = Modifier.fillMaxSize()
) {
StatusBarSpacer()
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f).pullRefresh(state)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
) {
NoticeScreenHeader("Favourite")
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize()
) {
items(moments.itemCount) { idx ->
val momentItem = moments[idx] ?: return@items
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.padding(2.dp)
.noRippleClickable {
navController.navigate(
NavigationRoute.Post.route.replace(
"{id}",
momentItem.id.toString()
)
)
}
) {
CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail,
contentDescription = "",
modifier = Modifier
.fillMaxSize(),
context = context
)
if (momentItem.images.size > 1) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Image(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.rider_pro_picture_more),
contentDescription = "",
)
}
}
}
}
}
}
PullRefreshIndicator(
FavouriteListViewModel.isLoading,
state,
Modifier.align(Alignment.TopCenter)
)
}
}
}

View File

@@ -0,0 +1,53 @@
package com.aiosman.riderpro.ui.favourite
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.entity.MomentPagingSource
import com.aiosman.riderpro.entity.MomentRemoteDataSource
import com.aiosman.riderpro.entity.MomentServiceImpl
import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel.accountService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
object FavouriteListViewModel:ViewModel() {
private val momentService: MomentService = MomentServiceImpl()
private val _favouriteMomentsFlow =
MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
val favouriteMomentsFlow = _favouriteMomentsFlow.asStateFlow()
var isLoading by mutableStateOf(false)
init {
refreshPager()
}
fun refreshPager(force:Boolean = false) {
viewModelScope.launch {
if (force) {
isLoading = true
}
val profile = accountService.getMyAccountProfile()
isLoading = false
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
MomentPagingSource(
MomentRemoteDataSource(momentService),
favoriteUserId = profile.id
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_favouriteMomentsFlow.value = it
}
}
}
}

View File

@@ -1,42 +1,79 @@
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.utils.File.saveImageToGallery
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3Api::class
)
@Composable
fun ImageViewer() {
val model = ImageViewerViewModel
val images = model.imageList
val pagerState = rememberPagerState(pageCount = { images.size }, initialPage = model.initialIndex)
val systemUiController = rememberSystemUiController()
val pagerState =
rememberPagerState(pageCount = { images.size }, initialPage = model.initialIndex)
val navController = LocalNavController.current
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedVisibilityScope = LocalAnimatedContentScope.current
val context = LocalContext.current
LaunchedEffect(Unit) {
}
val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.dp
val scope = rememberCoroutineScope()
val showRawImageStates = remember { mutableStateListOf(*Array(images.size) { false }) }
var showBottomSheet by remember { mutableStateOf(false) }
var isDownloading by remember { mutableStateOf(false) }
StatusBarMaskLayout(
modifier = Modifier.background(Color.Black),
) {
@@ -50,15 +87,11 @@ fun ImageViewer() {
modifier = Modifier.fillMaxSize()
) { page ->
val zoomState = rememberZoomState()
with(sharedTransitionScope) {
CustomAsyncImage(
context,
images[page].url,
if (showRawImageStates[page]) images[page].url else images[page].thumbnail,
contentDescription = null,
modifier = Modifier.sharedElement(
rememberSharedContentState(key = images[page]),
animatedVisibilityScope = animatedVisibilityScope
)
modifier = Modifier
.fillMaxSize()
.zoomable(
zoomState = zoomState,
@@ -69,8 +102,85 @@ fun ImageViewer() {
contentScale = ContentScale.Fit,
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black
),
)
)
.padding(start = 16.dp, end = 16.dp, bottom = navigationBarPaddings),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
horizontalArrangement = Arrangement.Center
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable {
if (isDownloading) {
return@noRippleClickable
}
isDownloading = true
scope.launch {
saveImageToGallery(context, images[pagerState.currentPage].url)
isDownloading = false
}
}
) {
if (isDownloading) {
CircularProgressIndicator(
modifier = Modifier.size(32.dp),
color = Color.White
)
}else{
Icon(
painter = painterResource(id = R.drawable.rider_pro_download),
contentDescription = "",
modifier = Modifier.size(32.dp),
tint = Color.White
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
"Download",
color = Color.White
)
}
if (!showRawImageStates[pagerState.currentPage]) {
Spacer(modifier = Modifier.width(32.dp))
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable {
showRawImageStates[pagerState.currentPage] = true
}
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_raw),
contentDescription = "",
modifier = Modifier.size(32.dp),
tint = Color.White
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"Original",
color = Color.White
)
}
}
}
}
}
}
}

View File

@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
@@ -75,6 +76,7 @@ import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.exp.formatPostTime2
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.PostViewModel
import kotlinx.coroutines.launch
@@ -118,6 +120,7 @@ fun ProfilePage() {
})
.pullRefresh(state)
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
@@ -166,36 +169,34 @@ fun ProfilePage() {
.align(Alignment.TopEnd)
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
start = 16.dp,
end = 16.dp
start = 8.dp,
end = 8.dp
)
) {
Box(
modifier = Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)).shadow(
elevation = 20.dp
).background(Color.White.copy(alpha = 0.7f))
){
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
modifier = Modifier.noRippleClickable {
expanded = true
},
tint = Color.White
tint = Color.Black
)
MaterialTheme(
shapes = MaterialTheme.shapes.copy(
extraSmall = RoundedCornerShape(
16.dp
)
)
) {
DropdownMenu(
}
com.aiosman.riderpro.ui.composables.DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(250.dp)
.background(Color.White)
width = 300,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
R.mipmap.rider_pro_logout
) {
Box(
modifier = Modifier
.padding(vertical = 14.dp, horizontal = 24.dp)
.noRippleClickable {
expanded = false
scope.launch {
model.logout()
@@ -205,44 +206,28 @@ fun ProfilePage() {
}
}
}
}) {
Row {
Text(
stringResource(R.string.logout),
fontWeight = FontWeight.W500
)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.mipmap.rider_pro_logout),
contentDescription = "",
modifier = Modifier.size(24.dp)
)
}
}
Box(
modifier = Modifier
.padding(vertical = 14.dp, horizontal = 24.dp)
.noRippleClickable {
},
MenuItem(
stringResource(R.string.change_password),
R.mipmap.rider_pro_change_password
) {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
}) {
Row {
Text(
stringResource(R.string.change_password),
fontWeight = FontWeight.W500
},
MenuItem(
"Favourite",
R.drawable.rider_pro_favourite
) {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.FavouriteList.route)
}
}
),
)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.mipmap.rider_pro_change_password),
contentDescription = "",
modifier = Modifier.size(24.dp)
)
}
}
}
}
}
}
Spacer(modifier = Modifier.height(32.dp))

View File

@@ -125,7 +125,7 @@ fun ActionPostNoticeItem(
val context = LocalContext.current
val navController = LocalNavController.current
Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),

View File

@@ -197,7 +197,9 @@ fun UserAuthScreen() {
fontSize = 12.sp
)
Spacer(modifier = Modifier.weight(1f))
Text(stringResource(R.string.forgot_password), fontSize = 12.sp)
Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable {
navController.navigate(NavigationRoute.ResetPassword.route)
})
}
}
Spacer(modifier = Modifier.height(64.dp))
@@ -210,7 +212,6 @@ fun UserAuthScreen() {
) {
onLogin()
}
Spacer(modifier = Modifier.height(48.dp))
Text(stringResource(R.string.or_login_with), color = Color(0xFF999999))
Spacer(modifier = Modifier.height(16.dp))

View File

@@ -0,0 +1,92 @@
package com.aiosman.riderpro.utils
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import coil.ImageLoader
import coil.request.ImageRequest
import coil.request.SuccessResult
import com.aiosman.riderpro.utils.Utils.getImageLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
import java.io.OutputStream
object File {
suspend fun saveImageToGallery(context: Context, url: String) {
val loader = getImageLoader(context)
val request = ImageRequest.Builder(context)
.data(url)
.allowHardware(false) // Disable hardware bitmaps.
.build()
val result = (loader.execute(request) as SuccessResult).drawable
val bitmap = (result as BitmapDrawable).bitmap
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "image_${System.currentTimeMillis()}.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
uri?.let {
val outputStream: OutputStream? = context.contentResolver.openOutputStream(it)
outputStream.use { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream!!)
}
withContext(Dispatchers.Main) {
Toast.makeText(context, "Image saved to gallery", Toast.LENGTH_SHORT).show()
}
}
}
fun saveImageToMediaStore(context: Context, displayName: String, bitmap: Bitmap): Uri? {
val imageCollections = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val imageDetails = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}
val resolver = context.applicationContext.contentResolver
val imageContentUri = resolver.insert(imageCollections, imageDetails) ?: return null
return try {
resolver.openOutputStream(imageContentUri, "w").use { os ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os!!)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
imageDetails.clear()
imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(imageContentUri, imageDetails, null, null)
}
imageContentUri
} catch (e: FileNotFoundException) {
// Some legacy devices won't create directory for the Uri if dir not exist, resulting in
// a FileNotFoundException. To resolve this issue, we should use the File API to save the
// image, which allows us to create the directory ourselves.
null
}
}
}

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:alpha="0.77" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:alpha="0.77" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M16.59,9H15V4c0,-0.55 -0.45,-1 -1,-1h-4c-0.55,0 -1,0.45 -1,1v5H7.41c-0.89,0 -1.34,1.08 -0.71,1.71l4.59,4.59c0.39,0.39 1.02,0.39 1.41,0l4.59,-4.59c0.63,-0.63 0.19,-1.71 -0.7,-1.71zM5,19c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1H6c-0.55,0 -1,0.45 -1,1z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<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="M6.5,9H3v6h1.5v-2h1.1l0.9,2H8l-0.9,-2.1C7.6,12.6 8,12.1 8,11.5v-1C8,9.7 7.3,9 6.5,9zM6.5,11.5h-2v-1h2V11.5z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.25,9l-1.5,6h1.5l0.38,-1.5h1.75l0.37,1.5h1.5l-1.5,-6H10.25zM11,12l0.25,-1h0.5L12,12H11z"/>
<path android:fillColor="@android:color/white" android:pathData="M19.98,9l-0.74,3l-0.74,-3l-1.52,0l-0.74,3l-0.74,-3l-1.5,0l1.5,6l1.48,0l0.76,-3.04l0.76,3.04l1.48,0l1.5,-6z"/>
</vector>