From 8920dbda0912b4dae2c96181460ff4134d49138f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0brahim=20Can=20Erdo=C4=9Fan?= <52867508+icanerdogan@users.noreply.github.com> Date: Sun, 15 Dec 2024 00:57:10 +0300 Subject: [PATCH] Like feature added. --- JetNews/app/build.gradle.kts | 12 +++++++++ .../jetnews/data/posts/PostsRepository.kt | 10 ++++++++ .../posts/impl/BlockingFakePostsRepository.kt | 9 +++++++ .../data/posts/impl/FakePostsRepository.kt | 9 +++++++ .../java/com/example/jetnews/ui/AppDrawer.kt | 3 ++- .../jetnews/ui/article/ArticleScreen.kt | 11 +++++--- .../jetnews/ui/components/AppNavRail.kt | 3 ++- .../com/example/jetnews/ui/home/HomeRoute.kt | 8 ++++++ .../example/jetnews/ui/home/HomeScreens.kt | 14 +++++++++-- .../example/jetnews/ui/home/HomeViewModel.kt | 18 +++++++++++++ .../example/jetnews/ui/utils/JetnewsIcons.kt | 25 ++++++++++++++++--- JetNews/app/src/main/res/values/strings.xml | 2 ++ JetNews/build.gradle.kts | 2 ++ 13 files changed, 114 insertions(+), 12 deletions(-) diff --git a/JetNews/app/build.gradle.kts b/JetNews/app/build.gradle.kts index 725b9f48c7..77e1647f5c 100644 --- a/JetNews/app/build.gradle.kts +++ b/JetNews/app/build.gradle.kts @@ -18,6 +18,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.compose) + id("androidx.room") + id("com.google.devtools.ksp") } android { @@ -69,6 +71,10 @@ android { compose = true } + room { + schemaDirectory("$projectDir/schemas") + } + packaging.resources { // Multiple dependency bring these files in. Exclude them to enable // our test APK to build (has no effect on our AARs) @@ -110,6 +116,12 @@ dependencies { implementation(libs.androidx.glance.appwidget) implementation(libs.androidx.glance.material3) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + + implementation("com.google.code.gson:gson:2.10.1") + implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.savedstate) implementation(libs.androidx.lifecycle.livedata.ktx) diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt b/JetNews/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt index d880a36e77..0c3ad2dccd 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt @@ -41,6 +41,11 @@ interface PostsRepository { */ fun observeFavorites(): Flow> + /** + * Observe the current marks + */ + fun observeMarks(): Flow> + /** * Observe the posts feed. */ @@ -50,4 +55,9 @@ interface PostsRepository { * Toggle a postId to be a favorite or not. */ suspend fun toggleFavorite(postId: String) + + /** + * Toggle a postId to be a mark or not. + */ + suspend fun toggleMark(postId: String) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/BlockingFakePostsRepository.kt b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/BlockingFakePostsRepository.kt index aa95a36d69..cab902242d 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/BlockingFakePostsRepository.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/BlockingFakePostsRepository.kt @@ -38,6 +38,8 @@ class BlockingFakePostsRepository : PostsRepository { // for now, keep the favorites in memory private val favorites = MutableStateFlow>(setOf()) + private val marks = MutableStateFlow>(setOf()) + private val postsFeed = MutableStateFlow(null) override suspend fun getPost(postId: String?): Result { @@ -57,9 +59,16 @@ class BlockingFakePostsRepository : PostsRepository { } override fun observeFavorites(): Flow> = favorites + + override fun observeMarks(): Flow> = marks + override fun observePostsFeed(): Flow = postsFeed override suspend fun toggleFavorite(postId: String) { favorites.update { it.addOrRemove(postId) } } + + override suspend fun toggleMark(postId: String) { + marks.update { it.addOrRemove(postId) } + } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/FakePostsRepository.kt b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/FakePostsRepository.kt index 939e095a1b..3e7894515e 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/FakePostsRepository.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/data/posts/impl/FakePostsRepository.kt @@ -37,6 +37,8 @@ class FakePostsRepository : PostsRepository { // for now, store these in memory private val favorites = MutableStateFlow>(setOf()) + private val marks = MutableStateFlow>(setOf()) + private val postsFeed = MutableStateFlow(null) // Used to make suspend functions that read and update state safe to call from any thread @@ -65,6 +67,7 @@ class FakePostsRepository : PostsRepository { } override fun observeFavorites(): Flow> = favorites + override fun observeMarks(): Flow> = marks override fun observePostsFeed(): Flow = postsFeed override suspend fun toggleFavorite(postId: String) { @@ -73,6 +76,12 @@ class FakePostsRepository : PostsRepository { } } + override suspend fun toggleMark(postId: String) { + marks.update { + it.addOrRemove(postId) + } + } + // used to drive "random" failure in a predictable pattern, making the first request always // succeed private var requestCount = 0 diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt index 29a9401ced..71e613d791 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ListAlt import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.ListAlt import androidx.compose.material3.DrawerState @@ -67,7 +68,7 @@ fun AppDrawer( ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.interests_title)) }, - icon = { Icon(Icons.Filled.ListAlt, null) }, + icon = { Icon(Icons.AutoMirrored.Filled.ListAlt, null) }, selected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, onClick = { navigateToInterests(); closeDrawer() }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt index 5891166cbb..3cf3707e6b 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar @@ -86,6 +87,8 @@ fun ArticleScreen( onBack: () -> Unit, isFavorite: Boolean, onToggleFavorite: () -> Unit, + isMarked: Boolean, + onToggleMark: () -> Unit, modifier: Modifier = Modifier, lazyListState: LazyListState = rememberLazyListState() ) { @@ -103,7 +106,7 @@ fun ArticleScreen( if (!isExpandedScreen) { IconButton(onClick = onBack) { Icon( - imageVector = Icons.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.cd_navigate_up), tint = MaterialTheme.colorScheme.primary ) @@ -115,7 +118,7 @@ fun ArticleScreen( if (!isExpandedScreen) { BottomAppBar( actions = { - FavoriteButton(onClick = { showUnimplementedActionDialog = true }) + FavoriteButton(isFavorite = isMarked, onClick = onToggleMark) BookmarkButton(isBookmarked = isFavorite, onClick = onToggleFavorite) ShareButton(onClick = { sharePost(post, context) }) TextSettingsButton(onClick = { showUnimplementedActionDialog = true }) @@ -248,7 +251,7 @@ fun PreviewArticleDrawer() { val post = runBlocking { (BlockingFakePostsRepository().getPost(post3.id) as Result.Success).data } - ArticleScreen(post, false, {}, false, {}) + ArticleScreen(post, false, {}, false, {}, false, {}) } } @@ -265,6 +268,6 @@ fun PreviewArticleNavRail() { val post = runBlocking { (BlockingFakePostsRepository().getPost(post3.id) as Result.Success).data } - ArticleScreen(post, true, {}, false, {}) + ArticleScreen(post, true, {}, false, {}, false, {}) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt index 43bf5640d4..588ec38993 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt @@ -20,6 +20,7 @@ import android.content.res.Configuration import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ListAlt import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.ListAlt import androidx.compose.material3.Icon @@ -66,7 +67,7 @@ fun AppNavRail( NavigationRailItem( selected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, onClick = navigateToInterests, - icon = { Icon(Icons.Filled.ListAlt, stringResource(R.string.interests_title)) }, + icon = { Icon(Icons.AutoMirrored.Filled.ListAlt, stringResource(R.string.interests_title)) }, label = { Text(stringResource(R.string.interests_title)) }, alwaysShowLabel = false ) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeRoute.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeRoute.kt index 0835169687..e8763360e1 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeRoute.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeRoute.kt @@ -54,6 +54,7 @@ fun HomeRoute( uiState = uiState, isExpandedScreen = isExpandedScreen, onToggleFavorite = { homeViewModel.toggleFavourite(it) }, + onToggleMark = { homeViewModel.toggleMark(it) }, onSelectPost = { homeViewModel.selectArticle(it) }, onRefreshPosts = { homeViewModel.refreshPosts() }, onErrorDismiss = { homeViewModel.errorShown(it) }, @@ -87,6 +88,7 @@ fun HomeRoute( uiState: HomeUiState, isExpandedScreen: Boolean, onToggleFavorite: (String) -> Unit, + onToggleMark: (String) -> Unit, onSelectPost: (String) -> Unit, onRefreshPosts: () -> Unit, onErrorDismiss: (Long) -> Unit, @@ -116,6 +118,7 @@ fun HomeRoute( uiState = uiState, showTopAppBar = !isExpandedScreen, onToggleFavorite = onToggleFavorite, + onToggleMark = onToggleMark, onSelectPost = onSelectPost, onRefreshPosts = onRefreshPosts, onErrorDismiss = onErrorDismiss, @@ -146,6 +149,7 @@ fun HomeRoute( // Guaranteed by above condition for home screen type check(uiState is HomeUiState.HasPosts) + // TODO: This ArticleScreen( post = uiState.selectedPost, isExpandedScreen = isExpandedScreen, @@ -154,6 +158,10 @@ fun HomeRoute( onToggleFavorite = { onToggleFavorite(uiState.selectedPost.id) }, + isMarked = uiState.marks.contains(uiState.selectedPost.id), + onToggleMark = { + onToggleMark(uiState.selectedPost.id) + }, lazyListState = articleDetailLazyListStates.getValue( uiState.selectedPost.id ) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreens.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreens.kt index d898962be8..03e3bf378d 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreens.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreens.kt @@ -54,6 +54,7 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -124,6 +125,7 @@ fun HomeFeedWithArticleDetailsScreen( uiState: HomeUiState, showTopAppBar: Boolean, onToggleFavorite: (String) -> Unit, + onToggleMark: (String) -> Unit, onSelectPost: (String) -> Unit, onRefreshPosts: () -> Unit, onErrorDismiss: (Long) -> Unit, @@ -193,6 +195,8 @@ fun HomeFeedWithArticleDetailsScreen( PostTopBar( isFavorite = hasPostsUiState.favorites.contains(detailPost.id), onToggleFavorite = { onToggleFavorite(detailPost.id) }, + isMark = hasPostsUiState.marks.contains(detailPost.id), + onToggleMark = { onToggleMark(detailPost.id) }, onSharePost = { sharePost(detailPost, context) }, modifier = Modifier .windowInsetsPadding(WindowInsets.safeDrawing) @@ -582,7 +586,7 @@ private fun PostListHistorySection( */ @Composable private fun PostListDivider() { - Divider( + HorizontalDivider( modifier = Modifier.padding(horizontal = 14.dp), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) ) @@ -649,6 +653,8 @@ private fun submitSearch( private fun PostTopBar( isFavorite: Boolean, onToggleFavorite: () -> Unit, + isMark: Boolean, + onToggleMark: () -> Unit, onSharePost: () -> Unit, modifier: Modifier = Modifier ) { @@ -658,7 +664,7 @@ private fun PostTopBar( modifier = modifier.padding(end = 16.dp) ) { Row(Modifier.padding(horizontal = 8.dp)) { - FavoriteButton(onClick = { /* Functionality not available */ }) + FavoriteButton(isFavorite = isMark, onClick = onToggleMark) BookmarkButton(isBookmarked = isFavorite, onClick = onToggleFavorite) ShareButton(onClick = onSharePost) TextSettingsButton(onClick = { /* Functionality not available */ }) @@ -732,6 +738,7 @@ fun PreviewHomeListDrawerScreen() { selectedPost = postsFeed.highlightedPost, isArticleOpen = false, favorites = emptySet(), + marks = emptySet(), isLoading = false, errorMessages = emptyList(), searchInput = "" @@ -768,6 +775,7 @@ fun PreviewHomeListNavRailScreen() { selectedPost = postsFeed.highlightedPost, isArticleOpen = false, favorites = emptySet(), + marks = emptySet(), isLoading = false, errorMessages = emptyList(), searchInput = "" @@ -800,12 +808,14 @@ fun PreviewHomeListDetailScreen() { selectedPost = postsFeed.highlightedPost, isArticleOpen = false, favorites = emptySet(), + marks = emptySet(), isLoading = false, errorMessages = emptyList(), searchInput = "" ), showTopAppBar = true, onToggleFavorite = {}, + onToggleMark = {}, onSelectPost = {}, onRefreshPosts = {}, onErrorDismiss = {}, diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeViewModel.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeViewModel.kt index c112b7f1a8..da4f1f533a 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeViewModel.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeViewModel.kt @@ -67,6 +67,7 @@ sealed interface HomeUiState { val selectedPost: Post, val isArticleOpen: Boolean, val favorites: Set, + val marks: Set, override val isLoading: Boolean, override val errorMessages: List, override val searchInput: String @@ -81,6 +82,7 @@ private data class HomeViewModelState( val selectedPostId: String? = null, // TODO back selectedPostId in a SavedStateHandle val isArticleOpen: Boolean = false, val favorites: Set = emptySet(), + val marks: Set = emptySet(), val isLoading: Boolean = false, val errorMessages: List = emptyList(), val searchInput: String = "", @@ -108,6 +110,7 @@ private data class HomeViewModelState( } ?: postsFeed.highlightedPost, isArticleOpen = isArticleOpen, favorites = favorites, + marks = marks, isLoading = isLoading, errorMessages = errorMessages, searchInput = searchInput @@ -149,6 +152,12 @@ class HomeViewModel( viewModelState.update { it.copy(favorites = favorites) } } } + + viewModelScope.launch { + postsRepository.observeMarks().collect { marks -> + viewModelState.update { it.copy(marks = marks) } + } + } } /** @@ -184,6 +193,15 @@ class HomeViewModel( } } + /** + * Toggle mark of a post + */ + fun toggleMark(postId: String) { + viewModelScope.launch { + postsRepository.toggleMark(postId) + } + } + /** * Selects the given article to view more information about it. */ diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/utils/JetnewsIcons.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/utils/JetnewsIcons.kt index ce0c69b0ae..9131d2fffc 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/utils/JetnewsIcons.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/utils/JetnewsIcons.kt @@ -20,6 +20,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.BookmarkBorder import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.ThumbUpAlt import androidx.compose.material.icons.filled.ThumbUpOffAlt import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -32,16 +33,32 @@ import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import com.example.jetnews.R +// FavoriteButton @Composable -fun FavoriteButton(onClick: () -> Unit) { - IconButton(onClick) { +fun FavoriteButton( + isFavorite: Boolean, + onClick: () -> Unit +) { + val clickLabel = stringResource( + if (isFavorite) R.string.unfavorite else R.string.favorite + ) + IconToggleButton( + checked = isFavorite, + onCheckedChange = { onClick() }, + modifier = Modifier.semantics { + // Use a custom click label that accessibility services can communicate to the user. + // We only want to override the label, not the actual action, so for the action we pass null. + this.onClick(label = clickLabel, action = null) + } + ) { Icon( - imageVector = Icons.Filled.ThumbUpOffAlt, - contentDescription = stringResource(R.string.cd_add_to_favorites) + imageVector = if (isFavorite) Icons.Filled.ThumbUpAlt else Icons.Filled.ThumbUpOffAlt, + contentDescription = null // handled by click label of parent ) } } +// BookmarkButton @Composable fun BookmarkButton( isBookmarked: Boolean, diff --git a/JetNews/app/src/main/res/values/strings.xml b/JetNews/app/src/main/res/values/strings.xml index cd9aa9b7cb..ce80d80f39 100644 --- a/JetNews/app/src/main/res/values/strings.xml +++ b/JetNews/app/src/main/res/values/strings.xml @@ -23,6 +23,8 @@ More actions unbookmark bookmark + unfavorite + favorite Search Interests Published in: \n%1$s diff --git a/JetNews/build.gradle.kts b/JetNews/build.gradle.kts index 08ccea3e70..29460fd024 100644 --- a/JetNews/build.gradle.kts +++ b/JetNews/build.gradle.kts @@ -21,6 +21,8 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.compose) apply false + id("androidx.room") version "2.6.0" apply false + id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle")