Add manga grouping in library!! This was inspired by J2ks version of the feature

This commit is contained in:
Jobobby04 2020-08-09 19:13:12 -04:00
parent 0e43234c23
commit e945de74f2
11 changed files with 367 additions and 10 deletions

View File

@ -282,4 +282,6 @@ object PreferenceKeys {
const val webtoonEnableZoomOut = "webtoon_enable_zoom_out"
const val startReadingButton = "start_reading_button"
const val groupLibraryBy = "group_library_by"
}

View File

@ -386,4 +386,6 @@ class PreferencesHelper(val context: Context) {
fun webtoonEnableZoomOut() = flowPrefs.getBoolean(Keys.webtoonEnableZoomOut, false)
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
}

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
@ -44,6 +45,7 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC
private var lastFilterJob: Job? = null
private val sourceManager: SourceManager by injectLazy()
private val trackManager: TrackManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val hasLoggedServices by lazy {
trackManager.hasLoggedServices()
}
@ -86,7 +88,7 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC
return currentItems.indexOfFirst { it.manga.id == manga.id }
}
fun canDrag() = (mode != Mode.MULTI || (mode == Mode.MULTI && selectedItemCount == 1)) && searchText.isBlank()
fun canDrag() = (mode != Mode.MULTI || (mode == Mode.MULTI && selectedItemCount == 1)) && searchText.isBlank() && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT
// EXH -->
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it

View File

@ -213,7 +213,13 @@ class LibraryController(
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
is LibrarySettingsSheet.Display.DisplayGroup -> reattachAdapter()
is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
// SY -->
is LibrarySettingsSheet.Display.ButtonsGroup -> onButtonSettingChanged()
// SY <--
is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
// SY -->
is LibrarySettingsSheet.Grouping.InternalGroup -> onGroupSettingChanged()
// SY <--
}
}
@ -336,6 +342,16 @@ class LibraryController(
presenter.requestBadgesUpdate()
}
// SY -->
private fun onButtonSettingChanged() {
presenter.requestButtonsUpdate()
}
private fun onGroupSettingChanged() {
presenter.requestGroupsUpdate()
}
// SY <--
private fun onTabsSettingsChanged() {
tabsVisibilityRelay.call(preferences.categoryTabs().get() && adapter?.categories?.size ?: 0 > 1)
updateTitle()

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.library
import eu.kanade.tachiyomi.R
object LibraryGroup {
const val BY_DEFAULT = 0
const val BY_SOURCE = 1
const val BY_STATUS = 2
const val BY_TRACK_STATUS = 3
const val UNGROUPED = 4
fun groupTypeStringRes(type: Int, hasCategories: Boolean = true): Int {
return when (type) {
BY_STATUS -> R.string.status
BY_SOURCE -> R.string.label_sources
BY_TRACK_STATUS -> R.string.tracking_status
UNGROUPED -> R.string.ungrouped
else -> if (hasCategories) R.string.categories else R.string.ungrouped
}
}
fun groupTypeDrawableRes(type: Int): Int {
return when (type) {
BY_STATUS -> R.drawable.ic_progress_clock_24dp
BY_TRACK_STATUS -> R.drawable.ic_sync_24dp
BY_SOURCE -> R.drawable.ic_explore_24dp
UNGROUPED -> R.drawable.ic_ungroup_24dp
else -> R.drawable.ic_label_24dp
}
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.library
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE
@ -89,9 +91,26 @@ class LibraryPresenter(
*/
private var librarySubscription: Subscription? = null
// --> EXH
// SY -->
val favoritesSync = FavoritesSyncHelper(context)
// <-- EXH
private var groupType = preferences.groupLibraryBy().get()
private val libraryIsGrouped
get() = groupType != LibraryGroup.UNGROUPED
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
/**
* Relay used to apply the UI update to the last emission of the library.
*/
private val buttonTriggerRelay = BehaviorRelay.create(Unit)
/**
* Relay used to apply the UI update to the last emission of the library.
*/
private val groupingTriggerRelay = BehaviorRelay.create(Unit)
// SY <--
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
@ -107,6 +126,15 @@ class LibraryPresenter(
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
lib.apply { setBadges(mangaMap) }
}
// SY -->
.combineLatest(buttonTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
lib.apply { setButtons(mangaMap) }
}
.combineLatest(groupingTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
val (map, categories) = applyGrouping(lib.mangaMap, lib.categories)
lib.copy(mangaMap = map, categories = categories)
}
// SY <--
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
lib.copy(mangaMap = applyFilters(lib.mangaMap))
}
@ -172,6 +200,21 @@ class LibraryPresenter(
return map.mapValues { entry -> entry.value.filter(filterFn) }
}
/**
* Sets the button on each manga.
*
* @param map the map of manga.
*/
private fun setButtons(map: LibraryMap) {
val startReadingButton = preferences.startReadingButton().get()
for ((_, itemList) in map) {
for (item in itemList) {
item.startReadingButton = startReadingButton
}
}
}
// SY <--
/**
@ -182,7 +225,6 @@ class LibraryPresenter(
private fun setBadges(map: LibraryMap) {
val showDownloadBadges = preferences.downloadBadge().get()
val showUnreadBadges = preferences.unreadBadge().get()
val startReadingButton = preferences.startReadingButton().get()
for ((_, itemList) in map) {
for (item in itemList) {
@ -199,10 +241,6 @@ class LibraryPresenter(
// Unset unread count if not enabled
-1
}
// SY -->
item.startReadingButton = startReadingButton
// SY <--
}
}
}
@ -294,6 +332,27 @@ class LibraryPresenter(
}
}
// SY -->
private fun applyGrouping(map: LibraryMap, categories: List<Category>): Pair<LibraryMap, List<Category>> {
groupType = preferences.groupLibraryBy().get()
var editedCategories: List<Category> = categories
val libraryMangaAsList = map.flatMap { it.value }.distinctBy { it.manga.id }
val items = if (groupType == LibraryGroup.BY_DEFAULT) {
map
} else if (!libraryIsGrouped) {
editedCategories = listOf(Category.create("All").apply { this.id = 0 })
libraryMangaAsList
.groupBy { 0 }
} else {
val (items, customCategories) = getGroupedMangaItems(libraryMangaAsList)
editedCategories = customCategories
items
}
return items to editedCategories
}
// SY <--
/**
* Get the categories from the database.
*
@ -331,6 +390,23 @@ class LibraryPresenter(
badgeTriggerRelay.call(Unit)
}
// SY -->
/**
* Requests the library to have buttons toggled.
*/
fun requestButtonsUpdate() {
buttonTriggerRelay.call(Unit)
}
/**
* Requests the library to have groups refreshed.
*/
fun requestGroupsUpdate() {
groupingTriggerRelay.call(Unit)
}
// SY <--
/**
* Requests the library to be sorted.
*/
@ -491,5 +567,109 @@ class LibraryPresenter(
chapters.sortedByDescending { it.source_order }.find { !it.read }
}
}
private fun getGroupedMangaItems(libraryManga: List<LibraryItem>): Pair<LibraryMap, List<Category>> {
val grouping: MutableList<Triple<String, Int, String>> = mutableListOf()
when (groupType) {
LibraryGroup.BY_STATUS -> libraryManga.distinctBy { it.manga.status }.map { it.manga.status }.forEachIndexed { index, status ->
grouping += Triple(status.toString(), index, mapStatus(status))
}
LibraryGroup.BY_SOURCE -> libraryManga.distinctBy { it.manga.source }.map { it.manga.source }.forEachIndexed { index, sourceLong ->
grouping += Triple(sourceLong.toString(), index, sourceManager.getOrStub(sourceLong).name)
}
LibraryGroup.BY_TRACK_STATUS -> {
grouping += Triple("1", 1, context.getString(R.string.reading))
grouping += Triple("2", 2, context.getString(R.string.repeating))
grouping += Triple("3", 3, context.getString(R.string.plan_to_read))
grouping += Triple("4", 4, context.getString(R.string.on_hold))
grouping += Triple("5", 5, context.getString(R.string.completed))
grouping += Triple("6", 6, context.getString(R.string.dropped))
grouping += Triple("7", 7, context.getString(R.string.not_tracked))
}
}
val map: MutableMap<Int, MutableList<LibraryItem>> = mutableMapOf()
libraryManga.forEach { libraryItem ->
when (groupType) {
LibraryGroup.BY_TRACK_STATUS -> {
val status: String = {
val tracks = db.getTracks(libraryItem.manga).executeAsBlocking()
val track = tracks.find { track ->
loggedServices.any { it.id == track?.sync_id }
}
val service = loggedServices.find { it.id == track?.sync_id }
if (track != null && service != null) {
service.getStatus(track.status)
} else {
"not tracked"
}
}()
val group = grouping.find { it.first == mapTrackingOrder(status) }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
} else {
map[7]?.plusAssign(libraryItem) ?: map.put(7, mutableListOf(libraryItem))
}
}
LibraryGroup.BY_SOURCE -> {
val group = grouping.find { it.first.toLongOrNull() == libraryItem.manga.source }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
} else {
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem))
}
}
else -> {
val group = grouping.find { it.first == libraryItem.manga.status.toString() }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
} else {
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem))
}
}
}
}
val categories = (
when (groupType) {
LibraryGroup.BY_SOURCE -> grouping.sortedBy { it.third.toLowerCase() }
LibraryGroup.BY_TRACK_STATUS -> grouping.filter { it.second in map.keys }
else -> grouping
}
).map {
val category = Category.create(it.third)
category.id = it.second
category
}
return map to categories
}
private fun mapTrackingOrder(status: String): String {
with(context) {
return when (status) {
getString(R.string.reading), getString(R.string.currently_reading) -> "1"
getString(R.string.repeating) -> "2"
getString(R.string.plan_to_read), getString(R.string.want_to_read) -> "3"
getString(R.string.on_hold), getString(R.string.paused) -> "4"
getString(R.string.completed) -> "5"
getString(R.string.dropped) -> "6"
else -> "7"
}
}
}
private fun mapStatus(status: Int): String {
return context.getString(
when (status) {
SManga.LICENSED -> R.string.licensed
SManga.ONGOING -> R.string.ongoing
SManga.COMPLETED -> R.string.completed
else -> R.string.unknown
}
)
}
// SY <--
}

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.util.AttributeSet
import android.view.View
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
@ -25,6 +26,7 @@ class LibrarySettingsSheet(
val filters: Filter
private val sort: Sort
private val display: Display
private val grouping: Grouping
init {
filters = Filter(activity)
@ -35,6 +37,9 @@ class LibrarySettingsSheet(
display = Display(activity)
display.onGroupClicked = onGroupClickListener
grouping = Grouping(activity)
grouping.onGroupClicked = onGroupClickListener
}
fun refreshSort() {
@ -44,13 +49,15 @@ class LibrarySettingsSheet(
override fun getTabViews(): List<View> = listOf(
filters,
sort,
display
display,
grouping
)
override fun getTabTitles(): List<Int> = listOf(
R.string.action_filter,
R.string.action_sort,
R.string.action_display
R.string.action_display,
R.string.group
)
/**
@ -198,6 +205,9 @@ class LibrarySettingsSheet(
override fun onItemClicked(item: Item) {
item as Item.MultiStateGroup
// SY -->
if (item == dragAndDrop && preferences.groupLibraryBy().get() != LibraryGroup.BY_DEFAULT) return
// SY <--
val prevState = item.state
item.group.items.forEach {
@ -355,6 +365,80 @@ class LibrarySettingsSheet(
}
}
// SY -->
inner class Grouping @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
init {
setGroups(listOf(InternalGroup()))
}
inner class InternalGroup : Group {
private val groupItems = mutableListOf<Item.DrawableSelection>()
private val db: DatabaseHelper = Injekt.get()
private val trackManager: TrackManager = Injekt.get()
private val hasCategories = db.getCategories().executeAsBlocking().size != 0
init {
val groupingItems = mutableListOf(
LibraryGroup.BY_DEFAULT,
LibraryGroup.BY_SOURCE,
LibraryGroup.BY_STATUS
)
if (trackManager.hasLoggedServices()) {
groupingItems.add(LibraryGroup.BY_TRACK_STATUS)
}
if (hasCategories) {
groupingItems.add(LibraryGroup.UNGROUPED)
}
groupItems += groupingItems.map { id ->
Item.DrawableSelection(
id,
this,
LibraryGroup.groupTypeStringRes(id, hasCategories),
LibraryGroup.groupTypeDrawableRes(id)
)
}
}
override val header = null
override val items = groupItems
override val footer = null
override fun initModels() {
val groupType = preferences.groupLibraryBy().get()
items.forEach {
it.state = if (it.id == groupType) {
Item.DrawableSelection.SELECTED
} else {
Item.DrawableSelection.NOT_SELECTED
}
}
}
override fun onItemClicked(item: Item) {
item as Item.DrawableSelection
if (item.id != LibraryGroup.BY_DEFAULT && preferences.librarySortingMode().get() == LibrarySort.DRAG_AND_DROP) {
preferences.librarySortingMode().set(LibrarySort.ALPHA)
preferences.librarySortingAscending().set(true)
refreshSort()
}
item.group.items.forEach {
(it as Item.DrawableSelection).state =
Item.DrawableSelection.NOT_SELECTED
}
item.state = Item.DrawableSelection.SELECTED
preferences.groupLibraryBy().set(item.id)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
}
}
// SY <--
open inner class Settings(context: Context, attrs: AttributeSet?) :
ExtendedNavigationView(context, attrs) {

View File

@ -109,6 +109,22 @@ open class ExtendedNavigationView @JvmOverloads constructor(
}
// SY -->
class DrawableSelection(val id: Int, group: Group, stringResId: Int, val drawable: Int) : MultiStateGroup(stringResId, group) {
companion object {
const val NOT_SELECTED = 0
const val SELECTED = 1
}
override fun getStateDrawable(context: Context): Drawable? {
return when (state) {
SELECTED -> tintVector(context, drawable, R.attr.colorAccent)
NOT_SELECTED -> tintVector(context, drawable, R.attr.colorOnSurface)
else -> null
}
}
}
class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) {
companion object {

View File

@ -0,0 +1,10 @@
<!-- drawable/progress_clock.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M13,2.03V2.05L13,4.05C17.39,4.59 20.5,8.58 19.96,12.97C19.5,16.61 16.64,19.5 13,19.93V21.93C18.5,21.38 22.5,16.5 21.95,11C21.5,6.25 17.73,2.5 13,2.03M11,2.06C9.05,2.25 7.19,3 5.67,4.26L7.1,5.74C8.22,4.84 9.57,4.26 11,4.06V2.06M4.26,5.67C3,7.19 2.25,9.04 2.05,11H4.05C4.24,9.58 4.8,8.23 5.69,7.1L4.26,5.67M2.06,13C2.26,14.96 3.03,16.81 4.27,18.33L5.69,16.9C4.81,15.77 4.24,14.42 4.06,13H2.06M7.1,18.37L5.67,19.74C7.18,21 9.04,21.79 11,22V20C9.58,19.82 8.23,19.25 7.1,18.37M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" />
</vector>

View File

@ -0,0 +1,8 @@
<!-- drawable/ungroup.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M2,2H6V3H13V2H17V6H16V9H18V8H22V12H21V18H22V22H18V21H12V22H8V18H9V16H6V17H2V13H3V6H2V2M18,12V11H16V13H17V17H13V16H11V18H12V19H18V18H19V12H18M13,6V5H6V6H5V13H6V14H9V12H8V8H12V9H14V6H13M12,12H11V14H13V13H14V11H12V12Z" />
</vector>

View File

@ -286,6 +286,11 @@
<string name="tracked">Tracked</string>
<string name="lewd">Lewd</string>
<!-- Library Grouping -->
<string name="tracking_status">Tracking status</string>
<string name="ungrouped">Ungrouped</string>
<string name="not_tracked">Not tracked</string>
<!-- Favorites Sync -->
<string name="sync_favorites">Sync favorites</string>
<string name="favorites_sync_error">Favorites sync error</string>