Use bottom sheet for library settings

This commit is contained in:
arkon 2020-03-28 17:17:21 -04:00
parent 0a5461cbea
commit 488f81ef74
7 changed files with 270 additions and 267 deletions

View File

@ -15,8 +15,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
@ -33,19 +31,16 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.inflate
import java.io.IOException import java.io.IOException
import kotlinx.android.synthetic.main.library_controller.action_toolbar import kotlinx.android.synthetic.main.library_controller.action_toolbar
import kotlinx.android.synthetic.main.library_controller.empty_view import kotlinx.android.synthetic.main.library_controller.empty_view
import kotlinx.android.synthetic.main.library_controller.library_pager import kotlinx.android.synthetic.main.library_controller.library_pager
import kotlinx.android.synthetic.main.main_activity.drawer
import kotlinx.android.synthetic.main.main_activity.tabs import kotlinx.android.synthetic.main.main_activity.tabs
import rx.Subscription import rx.Subscription
import timber.log.Timber import timber.log.Timber
@ -58,7 +53,6 @@ class LibraryController(
) : NucleusController<LibraryPresenter>(bundle), ) : NucleusController<LibraryPresenter>(bundle),
RootController, RootController,
TabbedController, TabbedController,
SecondaryDrawerController,
ActionMode.Callback, ActionMode.Callback,
ChangeMangaCategoriesDialog.Listener, ChangeMangaCategoriesDialog.Listener,
DeleteLibraryMangasDialog.Listener { DeleteLibraryMangasDialog.Listener {
@ -118,14 +112,9 @@ class LibraryController(
private var adapter: LibraryAdapter? = null private var adapter: LibraryAdapter? = null
/** /**
* Navigation view containing filter/sort/display items. * Sheet containing filter/sort/display items.
*/ */
private var navView: LibraryNavigationView? = null private var settingsSheet: LibrarySettingsSheet? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null
private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false) private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
@ -169,6 +158,15 @@ class LibraryController(
if (selectedMangas.isNotEmpty()) { if (selectedMangas.isNotEmpty()) {
createActionModeIfNeeded() createActionModeIfNeeded()
} }
settingsSheet = LibrarySettingsSheet(activity!!) { group ->
when (group) {
is LibrarySettingsSheet.Settings.FilterGroup -> onFilterChanged()
is LibrarySettingsSheet.Settings.SortGroup -> onSortChanged()
is LibrarySettingsSheet.Settings.DisplayGroup -> reattachAdapter()
is LibrarySettingsSheet.Settings.BadgeGroup -> onDownloadBadgeChanged()
}
}
} }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
@ -184,32 +182,12 @@ class LibraryController(
action_toolbar.destroy() action_toolbar.destroy()
adapter?.onDestroy() adapter?.onDestroy()
adapter = null adapter = null
settingsSheet = null
tabsVisibilitySubscription?.unsubscribe() tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null tabsVisibilitySubscription = null
super.onDestroyView(view) super.onDestroyView(view)
} }
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
navView = view
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)
navView?.onGroupClicked = { group ->
when (group) {
is LibraryNavigationView.FilterGroup -> onFilterChanged()
is LibraryNavigationView.SortGroup -> onSortChanged()
is LibraryNavigationView.DisplayGroup -> reattachAdapter()
is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged()
}
}
return view
}
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
navView = null
}
override fun configureTabs(tabs: TabLayout) { override fun configureTabs(tabs: TabLayout) {
with(tabs) { with(tabs) {
tabGravity = TabLayout.GRAVITY_CENTER tabGravity = TabLayout.GRAVITY_CENTER
@ -231,6 +209,10 @@ class LibraryController(
tabsVisibilitySubscription = null tabsVisibilitySubscription = null
} }
fun showSettingsSheet() {
settingsSheet?.show()
}
fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<LibraryItem>>) { fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<LibraryItem>>) {
val view = view ?: return val view = view ?: return
val adapter = adapter ?: return val adapter = adapter ?: return
@ -364,12 +346,12 @@ class LibraryController(
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
val navView = navView ?: return val settingsSheet = settingsSheet ?: return
val filterItem = menu.findItem(R.id.action_filter) val filterItem = menu.findItem(R.id.action_filter)
// Tint icon if there's a filter active // Tint icon if there's a filter active
if (navView.hasActiveFilters()) { if (settingsSheet.hasActiveFilters()) {
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
DrawableCompat.setTint(filterItem.icon, filterColor) DrawableCompat.setTint(filterItem.icon, filterColor)
} }
@ -378,9 +360,7 @@ class LibraryController(
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_search -> expandActionViewFromInteraction = true R.id.action_search -> expandActionViewFromInteraction = true
R.id.action_filter -> { R.id.action_filter -> showSettingsSheet()
navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) }
}
R.id.action_update_library -> { R.id.action_update_library -> {
activity?.let { LibraryUpdateService.start(it) } activity?.let { LibraryUpdateService.start(it) }
} }

View File

@ -1,216 +0,0 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.util.AttributeSet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE
import uy.kohesive.injekt.injectLazy
/**
* The navigation view shown in a drawer with the different options to show the library.
*/
class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
ExtendedNavigationView(context, attrs) {
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* List of groups shown in the view.
*/
private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup())
/**
* Adapter instance.
*/
private val adapter = Adapter(groups.map { it.createItems() }.flatten())
/**
* Click listener to notify the parent fragment when an item from a group is clicked.
*/
var onGroupClicked: (Group) -> Unit = {}
init {
recycler.adapter = adapter
addView(recycler)
groups.forEach { it.initModels() }
}
/**
* Returns true if there's at least one filter from [FilterGroup] active.
*/
fun hasActiveFilters(): Boolean {
return (groups[0] as FilterGroup).items.any { it.checked }
}
/**
* Adapter of the recycler view.
*/
inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
override fun onItemClicked(item: Item) {
if (item is GroupedItem) {
item.group.onItemClicked(item)
onGroupClicked(item.group)
}
}
}
/**
* Filters group (unread, downloaded, ...).
*/
inner class FilterGroup : Group {
private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
private val completed = Item.CheckboxGroup(R.string.completed, this)
override val items = listOf(downloaded, unread, completed)
override val header = Item.Header(R.string.action_filter)
override val footer = Item.Separator()
override fun initModels() {
downloaded.checked = preferences.filterDownloaded().getOrDefault()
unread.checked = preferences.filterUnread().getOrDefault()
completed.checked = preferences.filterCompleted().getOrDefault()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
when (item) {
downloaded -> preferences.filterDownloaded().set(item.checked)
unread -> preferences.filterUnread().set(item.checked)
completed -> preferences.filterCompleted().set(item.checked)
}
adapter.notifyItemChanged(item)
}
}
/**
* Sorting group (alphabetically, by last read, ...) and ascending or descending.
*/
inner class SortGroup : Group {
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
private val total = Item.MultiSort(R.string.action_sort_total, this)
private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
private val lastChecked = Item.MultiSort(R.string.action_sort_last_checked, this)
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
override val items = listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter)
override val header = Item.Header(R.string.action_sort)
override val footer = Item.Separator()
override fun initModels() {
val sorting = preferences.librarySortingMode().getOrDefault()
val order = if (preferences.librarySortingAscending().getOrDefault())
SORT_ASC else SORT_DESC
alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE
lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE
lastChecked.state = if (sorting == LibrarySort.LAST_CHECKED) order else SORT_NONE
unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE
latestChapter.state = if (sorting == LibrarySort.LATEST_CHAPTER) order else SORT_NONE
}
override fun onItemClicked(item: Item) {
item as Item.MultiStateGroup
val prevState = item.state
item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE }
item.state = when (prevState) {
SORT_NONE -> SORT_ASC
SORT_ASC -> SORT_DESC
SORT_DESC -> SORT_ASC
else -> throw Exception("Unknown state")
}
preferences.librarySortingMode().set(when (item) {
alphabetically -> LibrarySort.ALPHA
lastRead -> LibrarySort.LAST_READ
lastChecked -> LibrarySort.LAST_CHECKED
unread -> LibrarySort.UNREAD
total -> LibrarySort.TOTAL
latestChapter -> LibrarySort.LATEST_CHAPTER
else -> throw Exception("Unknown sorting")
})
preferences.librarySortingAscending().set(item.state == SORT_ASC)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
}
inner class BadgeGroup : Group {
private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this)
override val header = null
override val footer = null
override val items = listOf(downloadBadge)
override fun initModels() {
downloadBadge.checked = preferences.downloadBadge().getOrDefault()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
preferences.downloadBadge().set((item.checked))
adapter.notifyItemChanged(item)
}
}
/**
* Display group, to show the library as a list or a grid.
*/
inner class DisplayGroup : Group {
private val grid = Item.Radio(R.string.action_display_grid, this)
private val list = Item.Radio(R.string.action_display_list, this)
override val items = listOf(grid, list)
override val header = Item.Header(R.string.action_display)
override val footer = null
override fun initModels() {
val asList = preferences.libraryAsList().getOrDefault()
grid.checked = !asList
list.checked = asList
}
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
preferences.libraryAsList().set(item == list)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
}
}

View File

@ -0,0 +1,243 @@
package eu.kanade.tachiyomi.ui.library
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.bottomsheet.BottomSheetDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.injectLazy
class LibrarySettingsSheet(
activity: Activity,
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
) : BottomSheetDialog(activity) {
private var navView: Settings
init {
navView = Settings(activity)
navView.onGroupClicked = onGroupClickListener
setContentView(navView)
}
fun hasActiveFilters(): Boolean {
return navView.hasActiveFilters()
}
/**
* The navigation view shown in the sheet with the different options to show the library.
*/
class Settings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
ExtendedNavigationView(context, attrs) {
private val preferences: PreferencesHelper by injectLazy()
/**
* List of groups shown in the view.
*/
private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup())
/**
* Adapter instance.
*/
private val adapter = Adapter(groups.map { it.createItems() }.flatten())
/**
* Click listener to notify the parent fragment when an item from a group is clicked.
*/
var onGroupClicked: (Group) -> Unit = {}
init {
recycler.adapter = adapter
addView(recycler)
groups.forEach { it.initModels() }
}
/**
* Returns true if there's at least one filter from [FilterGroup] active.
*/
fun hasActiveFilters(): Boolean {
return (groups[0] as FilterGroup).items.any { it.checked }
}
/**
* Adapter of the recycler view.
*/
inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
override fun onItemClicked(item: Item) {
if (item is GroupedItem) {
item.group.onItemClicked(item)
onGroupClicked(item.group)
}
}
}
/**
* Filters group (unread, downloaded, ...).
*/
inner class FilterGroup : Group {
private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
private val completed = Item.CheckboxGroup(R.string.completed, this)
override val items = listOf(downloaded, unread, completed)
override val header = Item.Header(R.string.action_filter)
override val footer = Item.Separator()
override fun initModels() {
downloaded.checked = preferences.filterDownloaded().getOrDefault()
unread.checked = preferences.filterUnread().getOrDefault()
completed.checked = preferences.filterCompleted().getOrDefault()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
when (item) {
downloaded -> preferences.filterDownloaded().set(item.checked)
unread -> preferences.filterUnread().set(item.checked)
completed -> preferences.filterCompleted().set(item.checked)
}
adapter.notifyItemChanged(item)
}
}
/**
* Sorting group (alphabetically, by last read, ...) and ascending or descending.
*/
inner class SortGroup : Group {
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
private val total = Item.MultiSort(R.string.action_sort_total, this)
private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
private val lastChecked = Item.MultiSort(R.string.action_sort_last_checked, this)
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
override val items =
listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter)
override val header = Item.Header(R.string.action_sort)
override val footer = Item.Separator()
override fun initModels() {
val sorting = preferences.librarySortingMode().getOrDefault()
val order = if (preferences.librarySortingAscending().getOrDefault())
Item.MultiSort.SORT_ASC else Item.MultiSort.SORT_DESC
alphabetically.state =
if (sorting == LibrarySort.ALPHA) order else Item.MultiSort.SORT_NONE
lastRead.state =
if (sorting == LibrarySort.LAST_READ) order else Item.MultiSort.SORT_NONE
lastChecked.state =
if (sorting == LibrarySort.LAST_CHECKED) order else Item.MultiSort.SORT_NONE
unread.state =
if (sorting == LibrarySort.UNREAD) order else Item.MultiSort.SORT_NONE
total.state = if (sorting == LibrarySort.TOTAL) order else Item.MultiSort.SORT_NONE
latestChapter.state =
if (sorting == LibrarySort.LATEST_CHAPTER) 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")
}
preferences.librarySortingMode().set(
when (item) {
alphabetically -> LibrarySort.ALPHA
lastRead -> LibrarySort.LAST_READ
lastChecked -> LibrarySort.LAST_CHECKED
unread -> LibrarySort.UNREAD
total -> LibrarySort.TOTAL
latestChapter -> LibrarySort.LATEST_CHAPTER
else -> throw Exception("Unknown sorting")
}
)
preferences.librarySortingAscending().set(item.state == Item.MultiSort.SORT_ASC)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
}
inner class BadgeGroup : Group {
private val downloadBadge =
Item.CheckboxGroup(R.string.action_display_download_badge, this)
override val header = null
override val footer = null
override val items = listOf(downloadBadge)
override fun initModels() {
downloadBadge.checked = preferences.downloadBadge().getOrDefault()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
preferences.downloadBadge().set((item.checked))
adapter.notifyItemChanged(item)
}
}
/**
* Display group, to show the library as a list or a grid.
*/
inner class DisplayGroup : Group {
private val grid = Item.Radio(R.string.action_display_grid, this)
private val list = Item.Radio(R.string.action_display_list, this)
override val items = listOf(grid, list)
override val header = Item.Header(R.string.action_display)
override val footer = null
override fun initModels() {
val asList = preferences.libraryAsList().getOrDefault()
grid.checked = !asList
list.checked = asList
}
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
preferences.libraryAsList().set(item == list)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
}
}
}

View File

@ -86,7 +86,12 @@ class MainActivity : BaseActivity() {
R.id.nav_more -> setRoot(MoreController(), id) R.id.nav_more -> setRoot(MoreController(), id)
} }
} else { } else {
router.popToRoot() when (id) {
R.id.nav_library -> {
val controller = router.getControllerWithTag(id.toString()) as? LibraryController
controller?.showSettingsSheet()
}
}
} }
true true
} }

View File

@ -20,8 +20,7 @@ open class ExtendedNavigationView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : ) : SimpleNavigationView(context, attrs, defStyleAttr) {
SimpleNavigationView(context, attrs, defStyleAttr) {
/** /**
* Every item of the nav view. Generic items must belong to this list, custom items could be * Every item of the nav view. Generic items must belong to this list, custom items could be

View File

@ -28,8 +28,7 @@ open class SimpleNavigationView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : ) : ScrimInsetsFrameLayout(context, attrs, defStyleAttr) {
ScrimInsetsFrameLayout(context, attrs, defStyleAttr) {
/** /**
* Max width of the navigation view. * Max width of the navigation view.

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.ui.library.LibraryNavigationView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/nav_view2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:fitsSystemWindows="false" />