diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 0bfa15438..ec97527eb 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -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" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index c773a5876..bd33d5dd3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 1315cc660..5a3ff8420 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 5ac09c5d6..63e84fe94 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGroup.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGroup.kt new file mode 100644 index 000000000..64559c522 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGroup.kt @@ -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 + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 0c96e1258..8e139b55d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -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().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): Pair> { + groupType = preferences.groupLibraryBy().get() + var editedCategories: List = 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): Pair> { + val grouping: MutableList> = 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> = 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 <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt index b6b86bc6b..5c6f3a3ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt @@ -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 = listOf( filters, sort, - display + display, + grouping ) override fun getTabTitles(): List = 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() + 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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt index 779d48de8..d7b84ab47 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt @@ -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 { diff --git a/app/src/main/res/drawable/ic_progress_clock_24dp.xml b/app/src/main/res/drawable/ic_progress_clock_24dp.xml new file mode 100644 index 000000000..a0f4b9cc0 --- /dev/null +++ b/app/src/main/res/drawable/ic_progress_clock_24dp.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_ungroup_24dp.xml b/app/src/main/res/drawable/ic_ungroup_24dp.xml new file mode 100644 index 000000000..8ac8c7a22 --- /dev/null +++ b/app/src/main/res/drawable/ic_ungroup_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index e5f00baf0..a8b564728 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -286,6 +286,11 @@ Tracked Lewd + + Tracking status + Ungrouped + Not tracked + Sync favorites Favorites sync error