diff --git a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt index a72a932..fea49c0 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt @@ -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 /** diff --git a/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt b/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt index 473c54f..476bc02 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt @@ -92,10 +92,12 @@ data class RegisterMessageChannelRequestBody( @SerializedName("identifier") val identifier: String, ) + data class ResetPasswordRequestBody( @SerializedName("username") val username: String, ) + interface RiderProAPI { @POST("register") suspend fun register(@Body body: RegisterRequestBody): Response @@ -120,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> @Multipart diff --git a/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt b/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt index d1641f1..5979544 100644 --- a/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt +++ b/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt @@ -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() { override suspend fun load(params: LoadParams): LoadResult { 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 { 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 { 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 { 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( diff --git a/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt b/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt index b21919c..10c6141 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt @@ -30,6 +30,7 @@ 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 @@ -82,6 +83,7 @@ sealed class NavigationRoute( data object FollowerList : NavigationRoute("FollowerList/{id}") data object FollowingList : NavigationRoute("FollowingList/{id}") data object ResetPassword : NavigationRoute("ResetPassword") + data object FavouriteList : NavigationRoute("FavouriteList") } @@ -271,7 +273,13 @@ fun NavigationController( ResetPasswordScreen() } } - + composable(route = NavigationRoute.FavouriteList.route) { + CompositionLocalProvider( + LocalAnimatedContentScope provides this, + ) { + FavouriteListPage() + } + } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/DropdownMenu.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/DropdownMenu.kt index a3cbfce..948fc82 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/composables/DropdownMenu.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/DropdownMenu.kt @@ -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( diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/Image.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/Image.kt index f077bc9..bc8bfe2 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/composables/Image.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/Image.kt @@ -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(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) diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt new file mode 100644 index 0000000..e7d1b2a --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt @@ -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) + ) + + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListViewModel.kt new file mode 100644 index 0000000..975751a --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListViewModel.kt @@ -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.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 + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt b/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt index 074f8a8..06a7f66 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt @@ -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,27 +87,100 @@ fun ImageViewer() { modifier = Modifier.fillMaxSize() ) { page -> val zoomState = rememberZoomState() - with(sharedTransitionScope) { - CustomAsyncImage( - context, - images[page].url, - contentDescription = null, - modifier = Modifier.sharedElement( - rememberSharedContentState(key = images[page]), - animatedVisibilityScope = animatedVisibilityScope - ) - .fillMaxSize() - .zoomable( - zoomState = zoomState, - onTap = { - navController.popBackStack() - } + CustomAsyncImage( + context, + if (showRawImageStates[page]) images[page].url else images[page].thumbnail, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .zoomable( + zoomState = zoomState, + onTap = { + navController.popBackStack() + } + ), + contentScale = ContentScale.Fit, + ) + } + + 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 + ) + } + } } } } - } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt index 076d1cd..50750de 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt @@ -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,83 +169,65 @@ fun ProfilePage() { .align(Alignment.TopEnd) .padding( top = statusBarPaddingValues.calculateTopPadding(), - start = 16.dp, - end = 16.dp + start = 8.dp, + end = 8.dp ) ) { - Icon( - painter = painterResource(id = R.drawable.rider_pro_more_horizon), - contentDescription = "", - modifier = Modifier.noRippleClickable { - expanded = true - }, - tint = Color.White - ) - MaterialTheme( - shapes = MaterialTheme.shapes.copy( - extraSmall = RoundedCornerShape( - 16.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.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)) diff --git a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt index 30b2ec0..d875630 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt @@ -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(), diff --git a/app/src/main/java/com/aiosman/riderpro/utils/File.kt b/app/src/main/java/com/aiosman/riderpro/utils/File.kt new file mode 100644 index 0000000..73be24a --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/utils/File.kt @@ -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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/rider_pro_delete.xml b/app/src/main/res/drawable/rider_pro_delete.xml new file mode 100644 index 0000000..5eaf9e8 --- /dev/null +++ b/app/src/main/res/drawable/rider_pro_delete.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rider_pro_download.xml b/app/src/main/res/drawable/rider_pro_download.xml new file mode 100644 index 0000000..2c7b7ac --- /dev/null +++ b/app/src/main/res/drawable/rider_pro_download.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rider_pro_raw.xml b/app/src/main/res/drawable/rider_pro_raw.xml new file mode 100644 index 0000000..8016091 --- /dev/null +++ b/app/src/main/res/drawable/rider_pro_raw.xml @@ -0,0 +1,9 @@ + + + + + + + + +