From 6df56c2d77f98c16a014204a69ca36017e7afb1a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 7 Jul 2022 16:36:47 +0300 Subject: [PATCH] Bookmarks screen --- app/src/main/AndroidManifest.xml | 7 +- .../kotatsu/base/domain/ReversibleHandle.kt | 7 +- .../ui/list/SectionedSelectionController.kt | 18 ++ .../kotatsu/bookmarks/BookmarksModule.kt | 4 + .../bookmarks/data/BookmarkWithManga.kt | 23 --- .../kotatsu/bookmarks/data/BookmarksDao.kt | 15 +- .../kotatsu/bookmarks/data/EntityMapping.kt | 14 +- .../bookmarks/domain/BookmarksRepository.kt | 51 +++++ .../kotatsu/bookmarks/ui/BookmarksActivity.kt | 41 ++++ .../kotatsu/bookmarks/ui/BookmarksFragment.kt | 176 ++++++++++++++++++ .../ui/BookmarksSelectionDecoration.kt | 18 ++ .../bookmarks/ui/BookmarksViewModel.kt | 52 ++++++ .../ui/{ => adapter}/BookmarkListAD.kt | 2 +- .../ui/{ => adapter}/BookmarksAdapter.kt | 4 +- .../bookmarks/ui/adapter/BookmarksGroupAD.kt | 74 ++++++++ .../ui/adapter/BookmarksGroupAdapter.kt | 67 +++++++ .../bookmarks/ui/model/BookmarksGroup.kt | 31 +++ .../kotatsu/core/db/entity/MangaEntity.kt | 41 +++- .../kotatsu/core/db/entity/MangaWithTags.kt | 21 ++- .../kotatsu/core/db/entity/TagEntity.kt | 25 ++- .../kotatsu/details/ui/DetailsFragment.kt | 5 +- .../kotatsu/explore/ui/ExploreFragment.kt | 4 + .../kotatsu/library/ui/LibraryFragment.kt | 11 +- .../kotatsu/search/ui/MangaListActivity.kt | 7 +- .../search/ui/multi/MultiSearchActivity.kt | 9 +- .../org/koitharu/kotatsu/utils/ext/ViewExt.kt | 6 + .../main/res/layout/fragment_list_simple.xml | 17 ++ .../main/res/layout/item_bookmarks_group.xml | 53 ++++++ app/src/main/res/menu/mode_bookmarks.xml | 12 ++ app/src/main/res/values/strings.xml | 3 + 30 files changed, 754 insertions(+), 64 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt rename app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/{ => adapter}/BookmarkListAD.kt (97%) rename app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/{ => adapter}/BookmarksAdapter.kt (86%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/model/BookmarksGroup.kt create mode 100644 app/src/main/res/layout/fragment_list_simple.xml create mode 100644 app/src/main/res/layout/item_bookmarks_group.xml create mode 100644 app/src/main/res/menu/mode_bookmarks.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d2e667908..83b0d3cf3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -58,11 +58,14 @@ android:name="org.koitharu.kotatsu.search.ui.MangaListActivity" android:label="@string/search_manga" /> + ( } != null } + fun getSectionCount(section: T): Int { + return decorations[section]?.checkedItemsCount ?: 0 + } + + fun addToSelection(section: T, ids: Collection): Boolean { + val decoration = getDecoration(section) + startActionMode() + return actionMode?.also { + decoration.checkAll(ids) + notifySelectionChanged() + } != null + } + + fun clearSelection(section: T) { + decorations[section]?.clearSelection() ?: return + notifySelectionChanged() + } + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { return callback.onCreateActionMode(mode, menu) } diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt index 4a8294765..b21099509 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt @@ -1,10 +1,14 @@ package org.koitharu.kotatsu.bookmarks +import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository +import org.koitharu.kotatsu.bookmarks.ui.BookmarksViewModel val bookmarksModule get() = module { factory { BookmarksRepository(get()) } + + viewModel { BookmarksViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt deleted file mode 100644 index 4bd63d65d..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.koitharu.kotatsu.bookmarks.data - -import androidx.room.Embedded -import androidx.room.Junction -import androidx.room.Relation -import org.koitharu.kotatsu.core.db.entity.MangaEntity -import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity -import org.koitharu.kotatsu.core.db.entity.TagEntity - -class BookmarkWithManga( - @Embedded val bookmark: BookmarkEntity, - @Relation( - parentColumn = "manga_id", - entityColumn = "manga_id" - ) - val manga: MangaEntity, - @Relation( - parentColumn = "manga_id", - entityColumn = "tag_id", - associateBy = Junction(MangaTagsEntity::class) - ) - val tags: List, -) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt index dd023be7a..076b19a3c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt @@ -1,20 +1,27 @@ package org.koitharu.kotatsu.bookmarks.data -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Query +import androidx.room.* import kotlinx.coroutines.flow.Flow +import org.koitharu.kotatsu.core.db.entity.MangaWithTags @Dao abstract class BookmarksDao { + @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId") + abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity? + @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page") abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC") abstract fun observe(mangaId: Long): Flow> + @Transaction + @Query( + "SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at" + ) + abstract fun observe(): Flow>> + @Insert abstract suspend fun insert(entity: BookmarkEntity) diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt index 0ab69dd18..a8b2e0912 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt @@ -1,15 +1,9 @@ package org.koitharu.kotatsu.bookmarks.data import org.koitharu.kotatsu.bookmarks.domain.Bookmark -import org.koitharu.kotatsu.core.db.entity.toManga -import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.parsers.model.Manga import java.util.* -fun BookmarkWithManga.toBookmark() = bookmark.toBookmark( - manga.toManga(tags.toMangaTags()) -) - fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark( manga = manga, pageId = pageId, @@ -30,4 +24,10 @@ fun Bookmark.toEntity() = BookmarkEntity( imageUrl = imageUrl, createdAt = createdAt.time, percent = percent, -) \ No newline at end of file +) + +fun Collection.toBookmarks(manga: Manga) = map { + it.toBookmark(manga) +} + +fun Collection.ids() = map { it.pageId } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt index df63c03aa..ff8842647 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt @@ -1,15 +1,21 @@ package org.koitharu.kotatsu.bookmarks.domain +import android.database.SQLException import androidx.room.withTransaction import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import org.koitharu.kotatsu.base.domain.ReversibleHandle +import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity import org.koitharu.kotatsu.bookmarks.data.toBookmark +import org.koitharu.kotatsu.bookmarks.data.toBookmarks import org.koitharu.kotatsu.bookmarks.data.toEntity import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntity +import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.mapItems +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug class BookmarksRepository( private val db: MangaDatabase, @@ -23,6 +29,17 @@ class BookmarksRepository( return db.bookmarksDao.observe(manga.id).mapItems { it.toBookmark(manga) } } + fun observeBookmarks(): Flow>> { + return db.bookmarksDao.observe().map { map -> + val res = LinkedHashMap>(map.size) + for ((k, v) in map) { + val manga = k.toManga() + res[manga] = v.toBookmarks(manga) + } + res + } + } + suspend fun addBookmark(bookmark: Bookmark) { db.withTransaction { val tags = bookmark.manga.tags.toEntities() @@ -35,4 +52,38 @@ class BookmarksRepository( suspend fun removeBookmark(mangaId: Long, pageId: Long) { db.bookmarksDao.delete(mangaId, pageId) } + + suspend fun removeBookmarks(ids: Map>): ReversibleHandle { + val entities = ArrayList(ids.size) + db.withTransaction { + val dao = db.bookmarksDao + for ((manga, idSet) in ids) { + for (pageId in idSet) { + val e = dao.find(manga.id, pageId) + if (e != null) { + entities.add(e) + } + dao.delete(manga.id, pageId) + } + } + } + return BookmarksRestorer(entities) + } + + private inner class BookmarksRestorer( + private val entities: Collection, + ) : ReversibleHandle { + + override suspend fun reverse() { + db.withTransaction { + for (e in entities) { + try { + db.bookmarksDao.insert(e) + } catch (e: SQLException) { + e.printStackTraceDebug() + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt new file mode 100644 index 000000000..ee6c53efb --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.bookmarks.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.graphics.Insets +import androidx.core.view.updatePadding +import androidx.fragment.app.commit +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.databinding.ActivityContainerBinding + +class BookmarksActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityContainerBinding.inflate(layoutInflater)) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + val fm = supportFragmentManager + if (fm.findFragmentById(R.id.container) == null) { + fm.commit { + val fragment = BookmarksFragment.newInstance() + replace(R.id.container, fragment) + } + } + } + + override fun onWindowInsetsChanged(insets: Insets) { + with(binding.toolbar) { + updatePadding( + left = insets.left, + right = insets.right + ) + } + } + + companion object { + + fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt new file mode 100644 index 000000000..2be43dc13 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt @@ -0,0 +1,176 @@ +package org.koitharu.kotatsu.bookmarks.ui + +import android.os.Bundle +import android.view.* +import androidx.appcompat.view.ActionMode +import androidx.core.graphics.Insets +import androidx.core.view.updatePadding +import com.google.android.material.snackbar.Snackbar +import org.koin.android.ext.android.get +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.domain.reverseAsync +import org.koitharu.kotatsu.base.ui.BaseFragment +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.base.ui.util.ReversibleAction +import org.koitharu.kotatsu.bookmarks.data.ids +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter +import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup +import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding +import org.koitharu.kotatsu.details.ui.DetailsActivity +import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations + +class BookmarksFragment : BaseFragment(), ListStateHolderListener, + OnListItemClickListener, SectionedSelectionController.Callback { + + private val viewModel by viewModel() + private var adapter: BookmarksGroupAdapter? = null + private var selectionController: SectionedSelectionController? = null + + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding { + return FragmentListSimpleBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + selectionController = SectionedSelectionController( + activity = requireActivity(), + registryOwner = this, + callback = this, + ) + adapter = BookmarksGroupAdapter( + lifecycleOwner = viewLifecycleOwner, + coil = get(), + listener = this, + selectionController = checkNotNull(selectionController), + bookmarkClickListener = this, + groupClickListener = OnGroupClickListener(), + ) + binding.recyclerView.adapter = adapter + binding.recyclerView.setHasFixedSize(true) + val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing)) + binding.recyclerView.addItemDecoration(spacingDecoration) + + viewModel.content.observe(viewLifecycleOwner, ::onListChanged) + viewModel.onError.observe(viewLifecycleOwner, ::onError) + viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone) + } + + override fun onDestroyView() { + super.onDestroyView() + adapter = null + selectionController = null + } + + override fun onItemClick(item: Bookmark, view: View) { + if (selectionController?.onItemClick(item.manga, item.pageId) != true) { + val intent = ReaderActivity.newIntent(view.context, item) + startActivity(intent) + } + } + + override fun onItemLongClick(item: Bookmark, view: View): Boolean { + return selectionController?.onItemLongClick(item.manga, item.pageId) ?: false + } + + override fun onRetryClick(error: Throwable) = Unit + + override fun onEmptyActionClick() = Unit + + override fun onSelectionChanged(count: Int) { + binding.recyclerView.invalidateNestedItemDecorations() + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.mode_bookmarks, menu) + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.title = selectionController?.count?.toString() + return true + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_remove -> { + val ids = selectionController?.snapshot() ?: return false + viewModel.removeBookmarks(ids) + mode.finish() + true + } + else -> false + } + } + + override fun onCreateItemDecoration(section: Manga): AbstractSelectionItemDecoration { + return BookmarksSelectionDecoration(requireContext()) + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.root.updatePadding( + left = insets.left, + right = insets.right, + ) + binding.recyclerView.updatePadding( + bottom = insets.bottom, + ) + } + + private fun onListChanged(list: List) { + adapter?.items = list + } + + private fun onError(e: Throwable) { + Snackbar.make( + binding.recyclerView, + e.getDisplayMessage(resources), + Snackbar.LENGTH_SHORT + ).show() + } + + private fun onActionDone(action: ReversibleAction) { + val handle = action.handle + val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG + val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length) + if (handle != null) { + snackbar.setAction(R.string.undo) { handle.reverseAsync() } + } + snackbar.show() + } + + private inner class OnGroupClickListener : OnListItemClickListener { + + override fun onItemClick(item: BookmarksGroup, view: View) { + val controller = selectionController + if (controller != null && controller.count > 0) { + if (controller.getSectionCount(item.manga) == item.bookmarks.size) { + controller.clearSelection(item.manga) + } else { + controller.addToSelection(item.manga, item.bookmarks.ids()) + } + return + } + val intent = DetailsActivity.newIntent(view.context, item.manga) + startActivity(intent) + } + + override fun onItemLongClick(item: BookmarksGroup, view: View): Boolean { + return selectionController?.addToSelection(item.manga, item.bookmarks.ids()) ?: false + } + } + + companion object { + + fun newInstance() = BookmarksFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt new file mode 100644 index 000000000..025acb882 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.bookmarks.ui + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration +import org.koitharu.kotatsu.utils.ext.getItem + +class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { + + override fun getItemId(parent: RecyclerView, child: View): Long { + val holder = parent.getChildViewHolder(child) ?: return RecyclerView.NO_ID + val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID + return item.pageId + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt new file mode 100644 index 000000000..623bfc834 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt @@ -0,0 +1,52 @@ +package org.koitharu.kotatsu.bookmarks.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.base.ui.util.ReversibleAction +import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository +import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup +import org.koitharu.kotatsu.list.ui.model.EmptyState +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorState +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.utils.SingleLiveEvent +import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct + +class BookmarksViewModel( + private val repository: BookmarksRepository, +) : BaseViewModel() { + + val onActionDone = SingleLiveEvent() + + val content: LiveData> = repository.observeBookmarks() + .map { list -> + if (list.isEmpty()) { + listOf( + EmptyState( + icon = R.drawable.ic_empty_favourites, + textPrimary = R.string.no_bookmarks_yet, + textSecondary = R.string.no_bookmarks_summary, + actionStringRes = 0, + ) + ) + } else list.map { (manga, bookmarks) -> + BookmarksGroup(manga, bookmarks) + } + } + .catch { e -> e.toErrorState(canRetry = false) } + .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + + + fun removeBookmarks(ids: Map>) { + launchJob(Dispatchers.Default) { + val handle = repository.removeBookmarks(ids) + onActionDone.postCall(ReversibleAction(R.string.bookmarks_removed, handle)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt rename to app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt index f8aa0e638..94414c932 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.bookmarks.ui +package org.koitharu.kotatsu.bookmarks.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt rename to app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt index 92040bc97..2f3022b8e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.bookmarks.ui +package org.koitharu.kotatsu.bookmarks.ui.adapter import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil @@ -19,7 +19,7 @@ class BookmarksAdapter( private class DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { - return oldItem.manga.id == newItem.manga.id && oldItem.chapterId == newItem.chapterId + return oldItem.manga.id == newItem.manga.id && oldItem.pageId == newItem.pageId } override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt new file mode 100644 index 000000000..c6d9ea4f9 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt @@ -0,0 +1,74 @@ +package org.koitharu.kotatsu.bookmarks.ui.adapter + +import android.view.View +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.RecyclerView +import coil.ImageLoader +import coil.request.Disposable +import coil.size.Scale +import coil.util.CoilUtils +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup +import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.utils.ext.clearItemDecorations +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.newImageRequest +import org.koitharu.kotatsu.utils.ext.referer + +fun bookmarksGroupAD( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + sharedPool: RecyclerView.RecycledViewPool, + selectionController: SectionedSelectionController, + bookmarkClickListener: OnListItemClickListener, + groupClickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemBookmarksGroupBinding.inflate(layoutInflater, parent, false) } +) { + + val viewListenerAdapter = object : View.OnClickListener, View.OnLongClickListener { + override fun onClick(v: View) = groupClickListener.onItemClick(item, v) + override fun onLongClick(v: View) = groupClickListener.onItemLongClick(item, v) + } + var imageRequest: Disposable? = null + val adapter = BookmarksAdapter(coil, lifecycleOwner, bookmarkClickListener) + binding.recyclerView.setRecycledViewPool(sharedPool) + binding.recyclerView.adapter = adapter + val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing)) + binding.recyclerView.addItemDecoration(spacingDecoration) + binding.root.setOnClickListener(viewListenerAdapter) + binding.root.setOnLongClickListener(viewListenerAdapter) + + bind { payloads -> + if (payloads.isEmpty()) { + binding.recyclerView.clearItemDecorations() + binding.recyclerView.addItemDecoration(spacingDecoration) + selectionController.attachToRecyclerView(item.manga, binding.recyclerView) + } + imageRequest = binding.imageViewCover.newImageRequest(item.manga.coverUrl) + .referer(item.manga.publicUrl) + .placeholder(R.drawable.ic_placeholder) + .fallback(R.drawable.ic_placeholder) + .error(R.drawable.ic_placeholder) + .scale(Scale.FILL) + .allowRgb565(true) + .lifecycle(lifecycleOwner) + .enqueueWith(coil) + binding.textViewTitle.text = item.manga.title + adapter.items = item.bookmarks + } + + onViewRecycled { + imageRequest?.dispose() + imageRequest = null + CoilUtils.dispose(binding.imageViewCover) + binding.imageViewCover.setImageDrawable(null) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt new file mode 100644 index 000000000..c34b2fe54 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt @@ -0,0 +1,67 @@ +package org.koitharu.kotatsu.bookmarks.ui.adapter + +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup +import org.koitharu.kotatsu.list.ui.adapter.* +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.Manga +import kotlin.jvm.internal.Intrinsics + +class BookmarksGroupAdapter( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + selectionController: SectionedSelectionController, + listener: ListStateHolderListener, + bookmarkClickListener: OnListItemClickListener, + groupClickListener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + val pool = RecyclerView.RecycledViewPool() + delegatesManager + .addDelegate( + bookmarksGroupAD( + coil = coil, + lifecycleOwner = lifecycleOwner, + sharedPool = pool, + selectionController = selectionController, + bookmarkClickListener = bookmarkClickListener, + groupClickListener = groupClickListener, + ) + ) + .addDelegate(loadingStateAD()) + .addDelegate(loadingFooterAD()) + .addDelegate(emptyStateListAD(listener)) + .addDelegate(errorStateListAD(listener)) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return when { + oldItem is BookmarksGroup && newItem is BookmarksGroup -> { + oldItem.manga.id == newItem.manga.id + } + else -> oldItem.javaClass == newItem.javaClass + } + } + + override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return Intrinsics.areEqual(oldItem, newItem) + } + + override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? { + return when { + oldItem is BookmarksGroup && newItem is BookmarksGroup -> Unit + else -> super.getChangePayload(oldItem, newItem) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/model/BookmarksGroup.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/model/BookmarksGroup.kt new file mode 100644 index 000000000..c9dc2e129 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/model/BookmarksGroup.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.bookmarks.ui.model + +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.areItemsEquals + +class BookmarksGroup( + val manga: Manga, + val bookmarks: List, +) : ListModel { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BookmarksGroup + + if (manga != other.manga) return false + + return bookmarks.areItemsEquals(other.bookmarks) { a, b -> + a.imageUrl == b.imageUrl + } + } + + override fun hashCode(): Int { + var result = manga.hashCode() + result = 31 * result + bookmarks.sumOf { it.imageUrl.hashCode() } + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt index c425f0e68..d7bf917f4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt @@ -19,4 +19,43 @@ class MangaEntity( @ColumnInfo(name = "state") val state: String?, @ColumnInfo(name = "author") val author: String?, @ColumnInfo(name = "source") val source: String -) \ No newline at end of file +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MangaEntity + + if (id != other.id) return false + if (title != other.title) return false + if (altTitle != other.altTitle) return false + if (url != other.url) return false + if (publicUrl != other.publicUrl) return false + if (rating != other.rating) return false + if (isNsfw != other.isNsfw) return false + if (coverUrl != other.coverUrl) return false + if (largeCoverUrl != other.largeCoverUrl) return false + if (state != other.state) return false + if (author != other.author) return false + if (source != other.source) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + (altTitle?.hashCode() ?: 0) + result = 31 * result + url.hashCode() + result = 31 * result + publicUrl.hashCode() + result = 31 * result + rating.hashCode() + result = 31 * result + isNsfw.hashCode() + result = 31 * result + coverUrl.hashCode() + result = 31 * result + (largeCoverUrl?.hashCode() ?: 0) + result = 31 * result + (state?.hashCode() ?: 0) + result = 31 * result + (author?.hashCode() ?: 0) + result = 31 * result + source.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt index 8c35c376e..dd814dace 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt @@ -12,4 +12,23 @@ class MangaWithTags( associateBy = Junction(MangaTagsEntity::class) ) val tags: List, -) \ No newline at end of file +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MangaWithTags + + if (manga != other.manga) return false + if (tags != other.tags) return false + + return true + } + + override fun hashCode(): Int { + var result = manga.hashCode() + result = 31 * result + tags.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt index fe813a02b..58bb45d5b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt @@ -11,4 +11,27 @@ class TagEntity( @ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "key") val key: String, @ColumnInfo(name = "source") val source: String -) \ No newline at end of file +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TagEntity + + if (id != other.id) return false + if (title != other.title) return false + if (key != other.key) return false + if (source != other.source) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + key.hashCode() + result = 31 * result + source.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 912a09681..d997f1f4e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui import android.app.ActivityOptions import android.os.Bundle -import android.text.Spanned import android.text.method.LinkMovementMethod import android.view.* import androidx.appcompat.widget.PopupMenu @@ -10,7 +9,6 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.Insets import androidx.core.net.toFile import androidx.core.net.toUri -import androidx.core.text.parseAsHtml import androidx.core.view.MenuProvider import androidx.core.view.isGone import androidx.core.view.isVisible @@ -21,7 +19,6 @@ import coil.size.Scale import coil.util.CoilUtils import com.google.android.material.chip.Chip import kotlinx.coroutines.launch -import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koitharu.kotatsu.R @@ -30,7 +27,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.bookmarks.domain.Bookmark -import org.koitharu.kotatsu.bookmarks.ui.BookmarksAdapter +import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoBottomSheet diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt index e1510ca14..1d9006857 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -13,11 +13,13 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity import org.koitharu.kotatsu.databinding.FragmentExploreBinding import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener import org.koitharu.kotatsu.explore.ui.model.ExploreItem import org.koitharu.kotatsu.history.ui.HistoryActivity +import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.settings.SettingsActivity @@ -74,6 +76,8 @@ class ExploreFragment : BaseFragment(), override fun onClick(v: View) { val intent = when (v.id) { R.id.button_history -> HistoryActivity.newIntent(v.context) + R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL) + R.id.button_bookmarks -> BookmarksActivity.newIntent(v.context) else -> return } startActivity(intent) diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt index fc3322e4c..0160a3bf0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryFragment.kt @@ -5,7 +5,6 @@ import android.view.* import androidx.appcompat.view.ActionMode import androidx.core.graphics.Insets import androidx.core.view.updatePadding -import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel @@ -31,8 +30,8 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.flattenTo import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.addMenuProvider -import org.koitharu.kotatsu.utils.ext.findViewsByType import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations class LibraryFragment : BaseFragment(), LibraryListEventListener, SectionedSelectionController.Callback { @@ -141,7 +140,7 @@ class LibraryFragment : BaseFragment(), LibraryListEvent } override fun onSelectionChanged(count: Int) { - invalidateItemDecorations() + binding.recyclerView.invalidateNestedItemDecorations() } override fun onCreateItemDecoration(section: LibrarySectionModel): AbstractSelectionItemDecoration { @@ -164,12 +163,6 @@ class LibraryFragment : BaseFragment(), LibraryListEvent return viewModel.getManga(snapshot.values.flattenTo(HashSet())) } - private fun invalidateItemDecorations() { - binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach { - it.invalidateItemDecorations() - } - } - private fun onListChanged(list: List) { adapter?.items = list } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt index 821a7d491..3ecc7d9ed 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags import org.koitharu.kotatsu.databinding.ActivityContainerBinding +import org.koitharu.kotatsu.local.ui.LocalListFragment import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment @@ -35,7 +36,11 @@ class MangaListActivity : BaseActivity() { return } fm.commit { - val fragment = RemoteListFragment.newInstance(source) + val fragment = if (source == MangaSource.LOCAL) { + LocalListFragment.newInstance() + } else { + RemoteListFragment.newInstance(source) + } replace(R.id.container, fragment) if (!tags.isNullOrEmpty()) { runOnCommit(ApplyFilterRunnable(fragment, tags)) diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt index b5f40b007..a94949202 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt @@ -6,12 +6,9 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View -import android.view.ViewGroup import androidx.appcompat.view.ActionMode import androidx.core.graphics.Insets -import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding -import androidx.recyclerview.widget.RecyclerView import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -31,7 +28,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter import org.koitharu.kotatsu.utils.ShareHelper -import org.koitharu.kotatsu.utils.ext.findViewsByType +import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations class MultiSearchActivity : BaseActivity(), MangaListListener, ListSelectionController.Callback { @@ -144,9 +141,7 @@ class MultiSearchActivity : BaseActivity(), MangaLis } override fun onSelectionChanged(count: Int) { - binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach { - it.invalidateItemDecorations() - } + binding.recyclerView.invalidateNestedItemDecorations() } private fun collectSelectedItems(): Set { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index bfd3959a9..41e2cd2b8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -138,4 +138,10 @@ fun ViewGroup.findViewsByType(clazz: Class): Sequence { } } } +} + +fun RecyclerView.invalidateNestedItemDecorations() { + findViewsByType(RecyclerView::class.java).forEach { + it.invalidateItemDecorations() + } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_list_simple.xml b/app/src/main/res/layout/fragment_list_simple.xml new file mode 100644 index 000000000..a8c22162a --- /dev/null +++ b/app/src/main/res/layout/fragment_list_simple.xml @@ -0,0 +1,17 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_bookmarks_group.xml b/app/src/main/res/layout/item_bookmarks_group.xml new file mode 100644 index 000000000..3fee1940b --- /dev/null +++ b/app/src/main/res/layout/item_bookmarks_group.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/mode_bookmarks.xml b/app/src/main/res/menu/mode_bookmarks.xml new file mode 100644 index 000000000..52860b082 --- /dev/null +++ b/app/src/main/res/menu/mode_bookmarks.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 991751763..8e2172e9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -324,4 +324,7 @@ Last 2 hours History cleared Manage + No bookmarks yet + You can create bookmark while reading manga + Bookmarks removed \ No newline at end of file