更新代码

This commit is contained in:
2024-09-13 23:20:38 +08:00
parent 5c3c3111ae
commit de5088dc02
15 changed files with 514 additions and 137 deletions

View File

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

View File

@@ -92,10 +92,12 @@ data class RegisterMessageChannelRequestBody(
@SerializedName("identifier") @SerializedName("identifier")
val identifier: String, val identifier: String,
) )
data class ResetPasswordRequestBody( data class ResetPasswordRequestBody(
@SerializedName("username") @SerializedName("username")
val username: String, val username: String,
) )
interface RiderProAPI { interface RiderProAPI {
@POST("register") @POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit> suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@@ -120,6 +122,7 @@ interface RiderProAPI {
@Query("contentSearch") contentSearch: String? = null, @Query("contentSearch") contentSearch: String? = null,
@Query("postUser") postUser: Int? = null, @Query("postUser") postUser: Int? = null,
@Query("trend") trend: String? = null, @Query("trend") trend: String? = null,
@Query("favouriteUserId") favouriteUserId: Int? = null,
): Response<ListContainer<Moment>> ): Response<ListContainer<Moment>>
@Multipart @Multipart

View File

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

View File

@@ -30,6 +30,7 @@ import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.ui.account.AccountEditScreen2 import com.aiosman.riderpro.ui.account.AccountEditScreen2
import com.aiosman.riderpro.ui.account.ResetPasswordScreen import com.aiosman.riderpro.ui.account.ResetPasswordScreen
import com.aiosman.riderpro.ui.comment.CommentsScreen 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.favourite.FavouriteNoticeScreen
import com.aiosman.riderpro.ui.follower.FollowerListScreen import com.aiosman.riderpro.ui.follower.FollowerListScreen
import com.aiosman.riderpro.ui.follower.FollowerNoticeScreen import com.aiosman.riderpro.ui.follower.FollowerNoticeScreen
@@ -82,6 +83,7 @@ sealed class NavigationRoute(
data object FollowerList : NavigationRoute("FollowerList/{id}") data object FollowerList : NavigationRoute("FollowerList/{id}")
data object FollowingList : NavigationRoute("FollowingList/{id}") data object FollowingList : NavigationRoute("FollowingList/{id}")
data object ResetPassword : NavigationRoute("ResetPassword") data object ResetPassword : NavigationRoute("ResetPassword")
data object FavouriteList : NavigationRoute("FavouriteList")
} }
@@ -271,7 +273,13 @@ fun NavigationController(
ResetPasswordScreen() ResetPasswordScreen()
} }
} }
composable(route = NavigationRoute.FavouriteList.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
FavouriteListPage()
}
}
} }

View File

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

View File

@@ -61,31 +61,6 @@ fun CustomAsyncImage(
val localContext = LocalContext.current val localContext = LocalContext.current
val imageLoader = getImageLoader(context ?: localContext) 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( AsyncImage(
model = ImageRequest.Builder(context ?: localContext) model = ImageRequest.Builder(context ?: localContext)

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.animation.ExperimentalSharedTransitionApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
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.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.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.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState 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.Composable
import androidx.compose.runtime.LaunchedEffect 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.Modifier
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.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalAnimatedContentScope import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope import com.aiosman.riderpro.LocalSharedTransitionScope
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.utils.File.saveImageToGallery
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
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(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3Api::class
)
@Composable @Composable
fun ImageViewer() { fun ImageViewer() {
val model = ImageViewerViewModel val model = ImageViewerViewModel
val images = model.imageList val images = model.imageList
val pagerState = rememberPagerState(pageCount = { images.size }, initialPage = model.initialIndex) val pagerState =
val systemUiController = rememberSystemUiController() rememberPagerState(pageCount = { images.size }, initialPage = model.initialIndex)
val navController = LocalNavController.current val navController = LocalNavController.current
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedVisibilityScope = LocalAnimatedContentScope.current
val context = LocalContext.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( StatusBarMaskLayout(
modifier = Modifier.background(Color.Black), modifier = Modifier.background(Color.Black),
) { ) {
@@ -50,27 +87,100 @@ fun ImageViewer() {
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { page -> ) { page ->
val zoomState = rememberZoomState() val zoomState = rememberZoomState()
with(sharedTransitionScope) { CustomAsyncImage(
CustomAsyncImage( context,
context, if (showRawImageStates[page]) images[page].url else images[page].thumbnail,
images[page].url, contentDescription = null,
contentDescription = null, modifier = Modifier
modifier = Modifier.sharedElement( .fillMaxSize()
rememberSharedContentState(key = images[page]), .zoomable(
animatedVisibilityScope = animatedVisibilityScope zoomState = zoomState,
) onTap = {
.fillMaxSize() navController.popBackStack()
.zoomable( }
zoomState = zoomState, ),
onTap = { contentScale = ContentScale.Fit,
navController.popBackStack() )
} }
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black
), ),
contentScale = ContentScale.Fit, )
) )
.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.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
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.icons.Icons 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.exp.formatPostTime2
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.MenuItem
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 kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -118,6 +120,7 @@ fun ProfilePage() {
}) })
.pullRefresh(state) .pullRefresh(state)
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -166,83 +169,65 @@ fun ProfilePage() {
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding( .padding(
top = statusBarPaddingValues.calculateTopPadding(), top = statusBarPaddingValues.calculateTopPadding(),
start = 16.dp, start = 8.dp,
end = 16.dp end = 8.dp
) )
) { ) {
Icon( Box(
painter = painterResource(id = R.drawable.rider_pro_more_horizon), modifier = Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)).shadow(
contentDescription = "", elevation = 20.dp
modifier = Modifier.noRippleClickable { ).background(Color.White.copy(alpha = 0.7f))
expanded = true ){
}, Icon(
tint = Color.White painter = painterResource(id = R.drawable.rider_pro_more_horizon),
) contentDescription = "",
MaterialTheme( modifier = Modifier.noRippleClickable {
shapes = MaterialTheme.shapes.copy( expanded = true
extraSmall = RoundedCornerShape( },
16.dp tint = Color.Black
)
) )
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(250.dp)
.background(Color.White)
) {
Box(
modifier = Modifier
.padding(vertical = 14.dp, horizontal = 24.dp)
.noRippleClickable {
expanded = false
scope.launch {
model.logout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
}
}
}
}) {
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 {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
}) {
Row {
Text(
stringResource(R.string.change_password),
fontWeight = FontWeight.W500
)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.mipmap.rider_pro_change_password),
contentDescription = "",
modifier = Modifier.size(24.dp)
)
}
}
}
} }
com.aiosman.riderpro.ui.composables.DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
width = 300,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
R.mipmap.rider_pro_logout
) {
expanded = false
scope.launch {
model.logout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
}
}
}
},
MenuItem(
stringResource(R.string.change_password),
R.mipmap.rider_pro_change_password
) {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
},
MenuItem(
"Favourite",
R.drawable.rider_pro_favourite
) {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.FavouriteList.route)
}
}
),
)
} }
} }
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))

View File

@@ -125,7 +125,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 = 24.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

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>