diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 07ae1ab3e..01b51935e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -24,10 +24,6 @@ interface Manga : SManga { setFlags(order, SORT_MASK) } - private fun setFlags(flag: Int, mask: Int) { - chapter_flags = chapter_flags and mask.inv() or (flag and mask) - } - fun sortDescending(): Boolean { return chapter_flags and SORT_MASK == SORT_DESC } @@ -36,6 +32,10 @@ interface Manga : SManga { return genre?.split(", ")?.map { it.trim() } } + private fun setFlags(flag: Int, mask: Int) { + chapter_flags = chapter_flags and mask.inv() or (flag and mask) + } + // Used to display the chapter's title one way or another var displayMode: Int get() = chapter_flags and DISPLAY_MASK diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 90ab1a6a4..1ce8a688c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -13,7 +13,6 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode -import androidx.core.graphics.drawable.DrawableCompat import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -51,6 +50,7 @@ import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight import eu.kanade.tachiyomi.ui.manga.chapter.ChapterHolder import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter +import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersSettingsSheet import eu.kanade.tachiyomi.ui.manga.chapter.DeleteChaptersDialog import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter @@ -61,7 +61,6 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.hasCustomCover -import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.getCoordinates import eu.kanade.tachiyomi.util.view.gone @@ -124,6 +123,11 @@ class MangaController : private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null private var chaptersAdapter: ChaptersAdapter? = null + /** + * Sheet containing filter/sort/display items. + */ + private var settingsSheet: ChaptersSettingsSheet? = null + private var actionFabScrollListener: RecyclerView.OnScrollListener? = null /** @@ -178,7 +182,7 @@ class MangaController : // Init RecyclerView and adapter mangaInfoAdapter = MangaInfoHeaderAdapter(this, fromSource) - chaptersHeaderAdapter = MangaChaptersHeaderAdapter() + chaptersHeaderAdapter = MangaChaptersHeaderAdapter(this) chaptersAdapter = ChaptersAdapter(this, view.context) binding.recycler.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter) @@ -205,6 +209,19 @@ class MangaController : .launchIn(scope) binding.actionToolbar.offsetAppbarHeight(activity!!) + + settingsSheet = ChaptersSettingsSheet(activity!!, presenter) { group -> + if (group is ChaptersSettingsSheet.Filter.FilterGroup) { + updateFilterIconState() + chaptersAdapter?.notifyDataSetChanged() + } + } + + updateFilterIconState() + } + + private fun updateFilterIconState() { + chaptersHeaderAdapter?.setHasActiveFilters(settingsSheet?.filters?.hasActiveFilters() == true) } override fun configureFab(fab: ExtendedFloatingActionButton) { @@ -249,6 +266,7 @@ class MangaController : mangaInfoAdapter = null chaptersHeaderAdapter = null chaptersAdapter = null + settingsSheet = null super.onDestroyView(view) } @@ -266,50 +284,10 @@ class MangaController : } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.chapters, menu) + inflater.inflate(R.menu.manga, menu) } override fun onPrepareOptionsMenu(menu: Menu) { - // Initialize menu items. - val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return - val menuFilterUnread = menu.findItem(R.id.action_filter_unread) - val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) - val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) - val menuFilterEmpty = menu.findItem(R.id.action_filter_empty) - - // Set correct checkbox values. - menuFilterRead.isChecked = presenter.onlyRead() - menuFilterUnread.isChecked = presenter.onlyUnread() - menuFilterDownloaded.isChecked = presenter.onlyDownloaded() - menuFilterDownloaded.isEnabled = !presenter.forceDownloaded() - menuFilterBookmarked.isChecked = presenter.onlyBookmarked() - - val filterSet = presenter.onlyRead() || presenter.onlyUnread() || presenter.onlyDownloaded() || presenter.onlyBookmarked() - if (filterSet) { - val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) - DrawableCompat.setTint(menu.findItem(R.id.action_filter).icon, filterColor) - } - - // Only show remove filter option if there's a filter set. - menuFilterEmpty.isVisible = filterSet - - // Display mode submenu - if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { - menu.findItem(R.id.display_title).isChecked = true - } else { - menu.findItem(R.id.display_chapter_number).isChecked = true - } - - // Sorting mode submenu - val sortingItem = when (presenter.manga.sorting) { - Manga.SORTING_SOURCE -> R.id.sort_by_source - Manga.SORTING_NUMBER -> R.id.sort_by_number - Manga.SORTING_UPLOAD_DATE -> R.id.sort_by_upload_date - else -> throw NotImplementedError("Unimplemented sorting method") - } - menu.findItem(sortingItem).isChecked = true - menu.findItem(R.id.action_sort_descending).isChecked = presenter.manga.sortDescending() - // Hide download options for local manga menu.findItem(R.id.download_group).isVisible = !isLocalSource @@ -321,61 +299,10 @@ class MangaController : override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.display_title -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NAME) - } - R.id.display_chapter_number -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NUMBER) - } - - R.id.sort_by_source -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_SOURCE) - } - R.id.sort_by_number -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_NUMBER) - } - R.id.sort_by_upload_date -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_UPLOAD_DATE) - } - R.id.action_sort_descending -> { - presenter.reverseSortOrder() - activity?.invalidateOptionsMenu() - } - R.id.download_next, R.id.download_next_5, R.id.download_next_10, R.id.download_custom, R.id.download_unread, R.id.download_all -> downloadChapters(item.itemId) - R.id.action_filter_unread -> { - item.isChecked = !item.isChecked - presenter.setUnreadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_read -> { - item.isChecked = !item.isChecked - presenter.setReadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_downloaded -> { - item.isChecked = !item.isChecked - presenter.setDownloadedFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity?.invalidateOptionsMenu() - } - R.id.action_edit_categories -> onCategoriesClick() R.id.action_edit_cover -> handleChangeCover() R.id.action_migrate -> migrateManga() @@ -756,6 +683,10 @@ class MangaController : chaptersAdapter?.notifyDataSetChanged() } + fun showSettingsSheet() { + settingsSheet?.show() + } + // SELECTIONS & ACTION MODE private fun toggleSelection(position: Int) { @@ -958,11 +889,6 @@ class MangaController : // OVERFLOW MENU DIALOGS - private fun setDisplayMode(id: Int) { - presenter.setDisplayMode(id) - chaptersAdapter?.notifyDataSetChanged() - } - private fun getUnreadChaptersSorted() = presenter.chapters .filter { !it.read && it.status == Download.NOT_DOWNLOADED } .distinctBy { it.name } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 0bc875b08..a0adc55d4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -531,17 +531,6 @@ class MangaPresenter( refreshChapters() } - /** - * Removes all filters and requests an UI update. - */ - fun removeFilters() { - manga.readFilter = Manga.SHOW_ALL - manga.downloadedFilter = Manga.SHOW_ALL - manga.bookmarkedFilter = Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - /** * Sets the active display mode. * @param mode the mode to set. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt new file mode 100644 index 000000000..af34bfbf5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt @@ -0,0 +1,239 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Activity +import android.content.Context +import android.util.AttributeSet +import android.view.View +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.manga.MangaPresenter +import eu.kanade.tachiyomi.widget.ExtendedNavigationView +import eu.kanade.tachiyomi.widget.TabbedBottomSheetDialog + +class ChaptersSettingsSheet( + activity: Activity, + private val presenter: MangaPresenter, + onGroupClickListener: (ExtendedNavigationView.Group) -> Unit +) : TabbedBottomSheetDialog(activity) { + + val filters: Filter + private val sort: Sort + private val display: Display + + init { + filters = Filter(activity) + filters.onGroupClicked = onGroupClickListener + + sort = Sort(activity) + sort.onGroupClicked = onGroupClickListener + + display = Display(activity) + display.onGroupClicked = onGroupClickListener + } + + override fun getTabViews(): List = listOf( + filters, + sort, + display + ) + + override fun getTabTitles(): List = listOf( + R.string.action_filter, + R.string.action_sort, + R.string.action_display + ) + + /** + * Filters group (unread, downloaded, ...). + */ + inner class Filter @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + Settings(context, attrs) { + + private val filterGroup = FilterGroup() + + init { + setGroups(listOf(filterGroup)) + } + + /** + * Returns true if there's at least one filter from [FilterGroup] active. + */ + fun hasActiveFilters(): Boolean { + return filterGroup.items.any { it.checked } + } + + inner class FilterGroup : Group { + + private val read = Item.CheckboxGroup(R.string.action_filter_read, this) + private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this) + private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this) + private val bookmarked = Item.CheckboxGroup(R.string.action_filter_bookmarked, this) + + override val header = null + override val items = listOf(read, unread, downloaded, bookmarked) + override val footer = null + + override fun initModels() { + read.checked = presenter.onlyRead() + unread.checked = presenter.onlyUnread() + downloaded.checked = presenter.onlyDownloaded() + downloaded.enabled = !presenter.forceDownloaded() + bookmarked.checked = presenter.onlyBookmarked() + } + + override fun onItemClicked(item: Item) { + item as Item.CheckboxGroup + item.checked = !item.checked + when (item) { + read -> presenter.setReadFilter(item.checked) + unread -> presenter.setUnreadFilter(item.checked) + downloaded -> presenter.setDownloadedFilter(item.checked) + bookmarked -> presenter.setBookmarkedFilter(item.checked) + } + + initModels() + item.group.items.forEach { adapter.notifyItemChanged(it) } + } + } + } + + /** + * Sorting group (alphabetically, by last read, ...) and ascending or descending. + */ + inner class Sort @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + Settings(context, attrs) { + + init { + setGroups(listOf(SortGroup())) + } + + inner class SortGroup : Group { + + private val source = Item.MultiSort(R.string.sort_by_source, this) + private val chapterNum = Item.MultiSort(R.string.sort_by_number, this) + private val uploadDate = Item.MultiSort(R.string.sort_by_upload_date, this) + + override val header = null + override val items = listOf(source, uploadDate, chapterNum) + override val footer = null + + override fun initModels() { + val sorting = presenter.manga.sorting + val order = if (presenter.manga.sortDescending()) { + Item.MultiSort.SORT_DESC + } else { + Item.MultiSort.SORT_ASC + } + + source.state = + if (sorting == Manga.SORTING_SOURCE) order else Item.MultiSort.SORT_NONE + chapterNum.state = + if (sorting == Manga.SORTING_NUMBER) order else Item.MultiSort.SORT_NONE + uploadDate.state = + if (sorting == Manga.SORTING_UPLOAD_DATE) order else Item.MultiSort.SORT_NONE + } + + override fun onItemClicked(item: Item) { + item as Item.MultiStateGroup + val prevState = item.state + + item.group.items.forEach { + (it as Item.MultiStateGroup).state = + Item.MultiSort.SORT_NONE + } + item.state = when (prevState) { + Item.MultiSort.SORT_NONE -> Item.MultiSort.SORT_ASC + Item.MultiSort.SORT_ASC -> Item.MultiSort.SORT_DESC + Item.MultiSort.SORT_DESC -> Item.MultiSort.SORT_ASC + else -> throw Exception("Unknown state") + } + + when (item) { + source -> presenter.setSorting(Manga.SORTING_SOURCE) + chapterNum -> presenter.setSorting(Manga.SORTING_NUMBER) + uploadDate -> presenter.setSorting(Manga.SORTING_UPLOAD_DATE) + else -> throw Exception("Unknown sorting") + } + + presenter.reverseSortOrder() + + item.group.items.forEach { adapter.notifyItemChanged(it) } + } + } + } + + /** + * Display group, to show the library as a list or a grid. + */ + inner class Display @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + Settings(context, attrs) { + + init { + setGroups(listOf(DisplayGroup())) + } + + inner class DisplayGroup : Group { + + private val displayTitle = Item.Radio(R.string.show_title, this) + private val displayChapterNum = Item.Radio(R.string.show_chapter_number, this) + + override val header = null + override val items = listOf(displayTitle, displayChapterNum) + override val footer = null + + override fun initModels() { + val mode = presenter.manga.displayMode + displayTitle.checked = mode == Manga.DISPLAY_NAME + displayChapterNum.checked = mode == Manga.DISPLAY_NUMBER + } + + override fun onItemClicked(item: Item) { + item as Item.Radio + if (item.checked) return + + item.group.items.forEach { (it as Item.Radio).checked = false } + item.checked = true + + when (item) { + displayTitle -> presenter.setDisplayMode(Manga.DISPLAY_NAME) + displayChapterNum -> presenter.setDisplayMode(Manga.DISPLAY_NUMBER) + else -> throw NotImplementedError("Unknown display mode") + } + + item.group.items.forEach { adapter.notifyItemChanged(it) } + } + } + } + + open inner class Settings(context: Context, attrs: AttributeSet?) : + ExtendedNavigationView(context, attrs) { + + lateinit var adapter: Adapter + + /** + * Click listener to notify the parent fragment when an item from a group is clicked. + */ + var onGroupClicked: (Group) -> Unit = {} + + fun setGroups(groups: List) { + adapter = Adapter(groups.map { it.createItems() }.flatten()) + recycler.adapter = adapter + + groups.forEach { it.initModels() } + addView(recycler) + } + + /** + * Adapter of the recycler view. + */ + inner class Adapter(items: List) : ExtendedNavigationView.Adapter(items) { + + override fun onItemClicked(item: Item) { + if (item is GroupedItem) { + item.group.onItemClicked(item) + onGroupClicked(item.group) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaChaptersHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaChaptersHeaderAdapter.kt index bb0ad2d39..067b1e953 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaChaptersHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaChaptersHeaderAdapter.kt @@ -3,17 +3,28 @@ package eu.kanade.tachiyomi.ui.manga.chapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.graphics.drawable.DrawableCompat import androidx.recyclerview.widget.RecyclerView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.MangaChaptersHeaderBinding +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.system.getResourceColor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.view.clicks -class MangaChaptersHeaderAdapter : +class MangaChaptersHeaderAdapter( + private val controller: MangaController +) : RecyclerView.Adapter() { private var numChapters: Int? = null + private var hasActiveFilters: Boolean = false private val scope = CoroutineScope(Job() + Dispatchers.Main) private lateinit var binding: MangaChaptersHeaderBinding @@ -35,13 +46,31 @@ class MangaChaptersHeaderAdapter : notifyDataSetChanged() } + fun setHasActiveFilters(hasActiveFilters: Boolean) { + this.hasActiveFilters = hasActiveFilters + + notifyDataSetChanged() + } + inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + @ExperimentalCoroutinesApi fun bind() { binding.chaptersLabel.text = if (numChapters == null) { view.context.getString(R.string.chapters) } else { view.context.resources.getQuantityString(R.plurals.manga_num_chapters, numChapters!!, numChapters) } + + val filterColor = if (hasActiveFilters) { + view.context.getResourceColor(R.attr.colorFilterActive) + } else { + view.context.getResourceColor(R.attr.colorOnPrimary) + } + DrawableCompat.setTint(binding.btnChaptersFilter.icon, filterColor) + + merge(view.clicks(), binding.btnChaptersFilter.clicks()) + .onEach { controller.showSettingsSheet() } + .launchIn(scope) } } } diff --git a/app/src/main/res/layout/manga_chapters_header.xml b/app/src/main/res/layout/manga_chapters_header.xml index f23333a69..8da2f17dd 100644 --- a/app/src/main/res/layout/manga_chapters_header.xml +++ b/app/src/main/res/layout/manga_chapters_header.xml @@ -1,20 +1,38 @@ - + android:textIsSelectable="false" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/btn_chapters_filter" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + + + diff --git a/app/src/main/res/menu/chapters.xml b/app/src/main/res/menu/chapters.xml deleted file mode 100644 index 05423a2b8..000000000 --- a/app/src/main/res/menu/chapters.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/manga.xml b/app/src/main/res/menu/manga.xml new file mode 100644 index 000000000..a3837c201 --- /dev/null +++ b/app/src/main/res/menu/manga.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + +