diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java index 281248ca45..8b66a104a7 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java @@ -7,13 +7,13 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment; import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; @@ -48,14 +48,21 @@ public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPa String title = bundle.getString("categoryName"); int order = bundle.getInt("order"); final int orderItem = bundle.getInt("orderItem"); - if (order == 0) { - listFragment = new BookmarkPicturesFragment(); - } else { - listFragment = new BookmarkLocationsFragment(); + + switch (order){ + case 0: listFragment = new BookmarkPicturesFragment(); + break; + + case 1: listFragment = new BookmarkLocationsFragment(); + break; + + case 3: listFragment = new BookmarkCategoriesFragment(); + break; + } if(orderItem == 2) { listFragment = new BookmarkItemsFragment(); } - } + Bundle featuredArguments = new Bundle(); featuredArguments.putString("categoryName", title); listFragment.setArguments(featuredArguments); diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java index ea3a9a453a..f0620032a1 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java @@ -49,6 +49,13 @@ public class BookmarksPagerAdapter extends FragmentPagerAdapter { new BookmarkListRootFragment(locationBundle, this), context.getString(R.string.title_page_bookmarks_items))); } + final Bundle categoriesBundle = new Bundle(); + categoriesBundle.putString("categoryName", + context.getString(R.string.title_page_bookmarks_categories)); + categoriesBundle.putInt("order", 3); + pages.add(new BookmarkPages( + new BookmarkListRootFragment(categoriesBundle, this), + context.getString(R.string.title_page_bookmarks_categories))); notifyDataSetChanged(); } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesDao.kt new file mode 100644 index 0000000000..71a2d1ec98 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesDao.kt @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.bookmarks.category + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +/** + * Bookmark categories dao + * + * @constructor Create empty Bookmark categories dao + */ +@Dao +interface BookmarkCategoriesDao { + + /** + * Insert or Delete category bookmark into DB + * + * @param bookmarksCategoryModal + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(bookmarksCategoryModal: BookmarksCategoryModal) + + + /** + * Delete category bookmark from DB + * + * @param bookmarksCategoryModal + */ + @Delete + suspend fun delete(bookmarksCategoryModal: BookmarksCategoryModal) + + /** + * Checks if given category exist in DB + * + * @param categoryName + * @return + */ + @Query("SELECT EXISTS (SELECT 1 FROM bookmarks_categories WHERE categoryName = :categoryName)") + suspend fun doesExist(categoryName: String): Boolean + + /** + * Get all categories + * + * @return + */ + @Query("SELECT * FROM bookmarks_categories") + fun getAllCategories(): Flow> + +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesFragment.kt new file mode 100644 index 0000000000..ef5bc613df --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesFragment.kt @@ -0,0 +1,143 @@ +package fr.free.nrw.commons.bookmarks.category + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dagger.android.support.DaggerFragment +import fr.free.nrw.commons.R +import fr.free.nrw.commons.category.CategoryDetailsActivity +import javax.inject.Inject + +/** + * Tab fragment to show list of bookmarked Categories + */ +class BookmarkCategoriesFragment : DaggerFragment() { + + @Inject + lateinit var bookmarkCategoriesDao: BookmarkCategoriesDao + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MaterialTheme( + colorScheme = if (isSystemInDarkTheme()) darkColorScheme( + primary = colorResource(R.color.primaryDarkColor), + surface = colorResource(R.color.main_background_dark), + background = colorResource(R.color.main_background_dark) + ) else lightColorScheme( + primary = colorResource(R.color.primaryColor), + surface = colorResource(R.color.main_background_light), + background = colorResource(R.color.main_background_light) + ) + ) { + val listOfBookmarks by bookmarkCategoriesDao.getAllCategories() + .collectAsStateWithLifecycle(initialValue = emptyList()) + Surface(modifier = Modifier.fillMaxSize()) { + Box(contentAlignment = Alignment.Center) { + if (listOfBookmarks.isEmpty()) { + Text( + text = stringResource(R.string.bookmark_empty), + style = MaterialTheme.typography.bodyMedium, + color = if (isSystemInDarkTheme()) Color(0xB3FFFFFF) + else Color( + 0x8A000000 + ) + ) + } else { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(items = listOfBookmarks) { bookmarkItem -> + CategoryItem( + categoryName = bookmarkItem.categoryName, + onClick = { + val categoryDetailsIntent = Intent( + requireContext(), + CategoryDetailsActivity::class.java + ).putExtra("categoryName", it) + startActivity(categoryDetailsIntent) + } + ) + } + } + } + } + } + } + } + } + } + + + @Composable + fun CategoryItem( + modifier: Modifier = Modifier, + onClick: (String) -> Unit, + categoryName: String + ) { + Row(modifier = modifier.clickable { + onClick(categoryName) + }) { + ListItem( + leadingContent = { + Image( + modifier = Modifier.size(48.dp), + painter = painterResource(R.drawable.commons), + contentDescription = null + ) + }, + headlineContent = { + Text( + text = categoryName, + maxLines = 2, + color = if (isSystemInDarkTheme()) Color.White else Color.Black, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold + ) + } + ) + } + } + + @Preview + @Composable + private fun CategoryItemPreview() { + CategoryItem( + onClick = {}, + categoryName = "Test Category" + ) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt new file mode 100644 index 0000000000..ab679611f0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt @@ -0,0 +1,15 @@ +package fr.free.nrw.commons.bookmarks.category + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Data class representing bookmarked category in DB + * + * @property categoryName + * @constructor Create empty Bookmarks category modal + */ +@Entity(tableName = "bookmarks_categories") +data class BookmarksCategoryModal( + @PrimaryKey val categoryName: String +) diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt index ba1fcfdae2..a42d26fd62 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt @@ -7,8 +7,12 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View +import androidx.activity.viewModels import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import fr.free.nrw.commons.Media import fr.free.nrw.commons.R import fr.free.nrw.commons.Utils @@ -19,6 +23,8 @@ import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment import fr.free.nrw.commons.media.MediaDetailPagerFragment import fr.free.nrw.commons.theme.BaseActivity +import kotlinx.coroutines.launch +import javax.inject.Inject /** @@ -38,6 +44,11 @@ class CategoryDetailsActivity : BaseActivity(), private lateinit var binding: ActivityCategoryDetailsBinding + @Inject + lateinit var categoryViewModelFactory: CategoryDetailsViewModel.ViewModelFactory + + private val viewModel: CategoryDetailsViewModel by viewModels { categoryViewModelFactory } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -53,6 +64,15 @@ class CategoryDetailsActivity : BaseActivity(), supportActionBar?.setDisplayHomeAsUpEnabled(true) setTabs() setPageTitle() + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED){ + viewModel.bookmarkState.collect { + invalidateOptionsMenu() + } + } + } + } /** @@ -73,6 +93,8 @@ class CategoryDetailsActivity : BaseActivity(), categoriesMediaFragment.arguments = arguments subCategoryListFragment.arguments = arguments parentCategoriesFragment.arguments = arguments + + viewModel.onCheckIfBookmarked(categoryName!!) } fragmentList.add(categoriesMediaFragment) titleList.add("MEDIA") @@ -181,6 +203,14 @@ class CategoryDetailsActivity : BaseActivity(), Utils.handleWebUrl(this, Uri.parse(title.canonicalUri)) true } + + R.id.menu_bookmark_current_category -> { + categoryName?.let { + viewModel.onBookmarkClick(categoryName = it) + } + true + } + android.R.id.home -> { onBackPressed() true @@ -189,6 +219,22 @@ class CategoryDetailsActivity : BaseActivity(), } } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + menu?.run { + val bookmarkMenuItem = findItem(R.id.menu_bookmark_current_category) + if (bookmarkMenuItem != null) { + val icon = if(viewModel.bookmarkState.value){ + R.drawable.menu_ic_round_star_filled_24px + } else { + R.drawable.menu_ic_round_star_border_24px + } + + bookmarkMenuItem.setIcon(icon) + } + } + return super.onPrepareOptionsMenu(menu) + } + /** * This method is called on backPressed of anyFragment in the activity. * If condition is called when mediaDetailFragment is opened. diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsViewModel.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsViewModel.kt new file mode 100644 index 0000000000..a50f25669c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsViewModel.kt @@ -0,0 +1,109 @@ +package fr.free.nrw.commons.category + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao +import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * ViewModal for [CategoryDetailsActivity] + */ +class CategoryDetailsViewModel( + private val bookmarkCategoriesDao: BookmarkCategoriesDao +) : ViewModel() { + + private val _bookmarkState = MutableStateFlow(false) + val bookmarkState = _bookmarkState.asStateFlow() + + + /** + * Used to check if bookmark exists for the given category in DB + * based on that bookmark state is updated + * @param categoryName + */ + fun onCheckIfBookmarked(categoryName: String) { + viewModelScope.launch { + val isBookmarked = bookmarkCategoriesDao.doesExist(categoryName) + _bookmarkState.update { + isBookmarked + } + } + } + + /** + * Handles event when bookmark button is clicked from view + * based on that category is bookmarked or removed in/from in the DB + * and bookmark state is update as well + * @param categoryName + */ + fun onBookmarkClick(categoryName: String) { + if (_bookmarkState.value) { + deleteBookmark(categoryName) + _bookmarkState.update { + false + } + } else { + addBookmark(categoryName) + _bookmarkState.update { + true + } + } + } + + + /** + * Add bookmark into DB + * + * @param categoryName + */ + private fun addBookmark(categoryName: String) { + viewModelScope.launch { + val categoryItem = BookmarksCategoryModal( + categoryName = categoryName + ) + + bookmarkCategoriesDao.insert(categoryItem) + } + } + + + /** + * Delete bookmark from DB + * + * @param categoryName + */ + private fun deleteBookmark(categoryName: String) { + viewModelScope.launch { + bookmarkCategoriesDao.delete( + BookmarksCategoryModal( + categoryName = categoryName + ) + ) + } + } + + /** + * View model factory to create [CategoryDetailsViewModel] + * + * @property bookmarkCategoriesDao + * @constructor Create empty View model factory + */ + class ViewModelFactory @Inject constructor( + private val bookmarkCategoriesDao: BookmarkCategoriesDao + ) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + if (modelClass.isAssignableFrom(CategoryDetailsViewModel::class.java)) { + CategoryDetailsViewModel(bookmarkCategoriesDao) as T + } else { + throw IllegalArgumentException("Unknown class name") + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index 71947fa1a6..0c34bbdec3 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -3,6 +3,8 @@ package fr.free.nrw.commons.db import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters +import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao +import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.customselector.database.NotForUploadStatus @@ -21,8 +23,8 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao * */ @Database( - entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class], - version = 18, + entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class], + version = 19, exportSchema = false, ) @TypeConverters(Converters::class) @@ -38,4 +40,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun NotForUploadStatusDao(): NotForUploadStatusDao abstract fun ReviewDao(): ReviewDao + + abstract fun bookmarkCategoriesDao(): BookmarkCategoriesDao } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt index b195674a99..58d9039d53 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt @@ -15,6 +15,7 @@ import dagger.Provides import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.R import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao import fr.free.nrw.commons.customselector.database.UploadedStatusDao @@ -221,6 +222,10 @@ open class CommonsApplicationModule(private val applicationContext: Context) { fun providesReviewDao(appDatabase: AppDatabase): ReviewDao = appDatabase.ReviewDao() + @Provides + fun providesBookmarkCategoriesDao (appDatabase: AppDatabase): BookmarkCategoriesDao = + appDatabase.bookmarkCategoriesDao() + @Provides fun providesContentResolver(context: Context): ContentResolver = context.contentResolver diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt index bfdb90181a..0ef34e3551 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt @@ -4,6 +4,7 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import fr.free.nrw.commons.bookmarks.BookmarkFragment import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment +import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment @@ -97,6 +98,9 @@ abstract class FragmentBuilderModule { @ContributesAndroidInjector(modules = [BookmarkItemsFragmentModule::class]) abstract fun bindBookmarkItemListFragment(): BookmarkItemsFragment + @ContributesAndroidInjector + abstract fun bindBookmarkCategoriesListFragment(): BookmarkCategoriesFragment + @ContributesAndroidInjector abstract fun bindReviewOutOfContextFragment(): ReviewImageFragment diff --git a/app/src/main/res/layout/fragment_bookmarks.xml b/app/src/main/res/layout/fragment_bookmarks.xml index 930faf5922..1ffb7bc5d9 100644 --- a/app/src/main/res/layout/fragment_bookmarks.xml +++ b/app/src/main/res/layout/fragment_bookmarks.xml @@ -17,7 +17,7 @@ android:layout_below="@id/toolbar" android:background="?attr/tabBackground" app:tabIndicatorColor="?attr/tabIndicatorColor" - app:tabMode="fixed" + app:tabMode="scrollable" app:tabSelectedTextColor="?attr/tabSelectedTextColor" app:tabTextColor="?attr/tabTextColor" /> diff --git a/app/src/main/res/menu/fragment_category_detail.xml b/app/src/main/res/menu/fragment_category_detail.xml index 386e678fd3..0d2624238e 100644 --- a/app/src/main/res/menu/fragment_category_detail.xml +++ b/app/src/main/res/menu/fragment_category_detail.xml @@ -2,6 +2,12 @@ + + + No compatible map application could be found on your device. Please install a map application to use this feature. Pictures Locations + Categories Add/Remove to bookmarks Bookmarks You haven\'t added any bookmarks diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapterTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapterTests.kt index 9423c90958..bf143ecc06 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapterTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapterTests.kt @@ -37,7 +37,7 @@ class BookmarksPagerAdapterTests { @Test fun testGetCount() { - Assert.assertEquals(bookmarksPagerAdapter.count, 3) + Assert.assertEquals(bookmarksPagerAdapter.count, 4) } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/LoggedOutBookmarksPagerAdapterTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/LoggedOutBookmarksPagerAdapterTests.kt index 4c20bdda93..7ba7d559e7 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/LoggedOutBookmarksPagerAdapterTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/LoggedOutBookmarksPagerAdapterTests.kt @@ -53,7 +53,7 @@ class LoggedOutBookmarksPagerAdapterTests { */ @Test fun testGetCount() { - Assert.assertEquals(bookmarksPagerAdapter.count, 1) + Assert.assertEquals(bookmarksPagerAdapter.count, 2) } /**