Drag & Drop Sorting in Library

This commit is contained in:
Jay 2020-01-05 23:04:29 -08:00
parent 5261864aba
commit b872ab837a
19 changed files with 194 additions and 20 deletions

View File

@ -18,7 +18,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
const val DATABASE_VERSION = 9
const val DATABASE_VERSION = 10
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -70,6 +70,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
if (oldVersion < 9) {
db.execSQL(MangaTable.addHideTitle)
}
if (oldVersion < 10) {
db.execSQL(CategoryTable.addMangaOrder)
}
}
override fun onConfigure(db: SupportSQLiteDatabase) {

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_MANGA_ORDER
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
@ -40,6 +41,9 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
put(COL_NAME, obj.name)
put(COL_ORDER, obj.order)
put(COL_FLAGS, obj.flags)
val orderString = obj.mangaOrder.joinToString("/")
put(COL_MANGA_ORDER, orderString)
}
}
@ -50,6 +54,9 @@ class CategoryGetResolver : DefaultGetResolver<Category>() {
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
mangaOrder = orderString?.split("/")?.mapNotNull { it.toLongOrNull() } ?: emptyList()
}
}

View File

@ -12,6 +12,8 @@ interface Category : Serializable {
var flags: Int
var mangaOrder:List<Long>
val nameLower: String
get() = name.toLowerCase()

View File

@ -10,6 +10,8 @@ class CategoryImpl : Category {
override var flags: Int = 0
override var mangaOrder: List<Long> = emptyList()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false

View File

@ -12,12 +12,18 @@ object CategoryTable {
const val COL_FLAGS = "flags"
const val COL_MANGA_ORDER = "manga_order"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_NAME TEXT NOT NULL,
$COL_ORDER INTEGER NOT NULL,
$COL_FLAGS INTEGER NOT NULL
$COL_FLAGS INTEGER NOT NULL,
$COL_MANGA_ORDER TEXT NOT NULL
)"""
val addMangaOrder: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT"
}

View File

@ -7,6 +7,7 @@ import androidx.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference
import com.f2prateek.rx.preferences.RxSharedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.Source
import java.io.File
@ -198,6 +199,8 @@ class PreferencesHelper(val context: Context) {
fun skipPreMigration() = rxPrefs.getBoolean(Keys.skipPreMigration, false)
fun defaultMangaOrder() = rxPrefs.getString("default_manga_order", "")
fun upgradeFilters() {
val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault()
val filterUn = rxPrefs.getBoolean(Keys.filterUnread, false).getOrDefault()

View File

@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.ui.library
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
/**
* Adapter storing a list of manga in a certain category.
@ -18,6 +20,8 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
*/
private var mangas: List<LibraryItem> = emptyList()
val onItemReleaseListener: CategoryAdapter.OnItemReleaseListener = view
/**
* Sets a list of manga in the adapter.
*

View File

@ -7,15 +7,16 @@ import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.library_category.view.*
@ -28,7 +29,9 @@ import uy.kohesive.injekt.injectLazy
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener {
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.OnItemMoveListener,
CategoryAdapter.OnItemReleaseListener {
/**
* Preferences.
@ -117,6 +120,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} else {
SelectableAdapter.Mode.SINGLE
}
val sortingMode = preferences.librarySortingMode().getOrDefault()
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
subscriptions += controller.searchRelay
.doOnNext { adapter.setFilter(it) }
@ -138,6 +143,27 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
controller.invalidateActionMode()
}
}
subscriptions += controller.reorganizeRelay
.subscribe {
if (it.first == category.id) {
var items = when (it.second) {
1, 2 -> adapter.currentItems.sortedBy {
if (preferences.removeArticles().getOrDefault())
it.manga.title.removeArticles()
else
it.manga.title
}
3, 4 -> adapter.currentItems.sortedBy { it.manga.last_update }
else -> adapter.currentItems.sortedBy { it.manga.title }
}
if (it.second % 2 == 0)
items = items.reversed()
adapter.setItems(items)
adapter.notifyDataSetChanged()
onItemReleased(0)
}
}
}
fun onRecycle() {
@ -158,8 +184,18 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
*/
fun onNextLibraryManga(event: LibraryMangaEvent) {
// Get the manga list for this category.
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
val sortingMode = preferences.librarySortingMode().getOrDefault()
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
var mangaForCategory = event.getMangaForCategory(category).orEmpty()
if (sortingMode == LibrarySort.DRAG_AND_DROP) {
if (category.name == "Default")
category.mangaOrder = preferences.defaultMangaOrder().getOrDefault().split("/")
.mapNotNull { it.toLongOrNull() }
mangaForCategory = mangaForCategory.sortedBy { category.mangaOrder.indexOf(it.manga
.id) }
}
// Update the category with its manga.
adapter.setItems(mangaForCategory)
@ -185,6 +221,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
is LibrarySelectionEvent.Selected -> {
if (adapter.mode != SelectableAdapter.Mode.MULTI) {
adapter.mode = SelectableAdapter.Mode.MULTI
adapter.isLongPressDragEnabled = false
}
findAndToggleSelection(event.manga)
}
@ -193,12 +230,16 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
if (adapter.indexOf(event.manga) != -1) lastClickPosition = -1
if (controller.selectedMangas.isEmpty()) {
adapter.mode = SelectableAdapter.Mode.SINGLE
adapter.isLongPressDragEnabled = preferences.librarySortingMode()
.getOrDefault() == LibrarySort.DRAG_AND_DROP
}
}
is LibrarySelectionEvent.Cleared -> {
adapter.mode = SelectableAdapter.Mode.SINGLE
adapter.clearSelection()
lastClickPosition = -1
adapter.isLongPressDragEnabled = preferences.librarySortingMode()
.getOrDefault() == LibrarySort.DRAG_AND_DROP
}
}
}
@ -249,6 +290,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
*/
override fun onItemLongClick(position: Int) {
controller.createActionModeIfNeeded()
adapter.isLongPressDragEnabled = false
when {
lastClickPosition == -1 -> setSelection(position)
lastClickPosition > position -> for (i in position until lastClickPosition)
@ -260,6 +302,36 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
lastClickPosition = position
}
override fun onItemMove(fromPosition: Int, toPosition: Int) {
}
override fun onItemReleased(position: Int) {
if (adapter.selectedItemCount == 0) {
val mangaIds = adapter.currentItems.mapNotNull { it.manga.id }
category.mangaOrder = mangaIds
val db: DatabaseHelper by injectLazy()
if (category.name == "Default")
preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
else
db.insertCategory(category).asRxObservable().subscribe()
}
}
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
if (adapter.selectedItemCount > 1)
return false
if (adapter.isSelected(fromPosition))
toggleSelection(fromPosition)
return true
}
override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
val position = viewHolder?.adapterPosition ?: return
if (actionState == 2)
onItemLongClick(position)
}
/**
* Opens a manga.
*

View File

@ -26,6 +26,7 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxbinding.support.v4.view.pageSelections
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
import com.jakewharton.rxbinding.view.visible
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
@ -120,6 +121,11 @@ class LibraryController(
*/
val selectAllRelay: PublishRelay<Int> = PublishRelay.create()
/**
* Relay to notify the library's viewpager to reotagnize all
*/
val reorganizeRelay: PublishRelay<Pair<Int, Int>> = PublishRelay.create()
/**
* Number of manga per row in grid mode.
*/
@ -328,6 +334,7 @@ class LibraryController(
* Called when the sorting mode is changed.
*/
private fun onSortChanged() {
activity?.invalidateOptionsMenu()
presenter.requestSortUpdate()
}
@ -364,6 +371,9 @@ class LibraryController(
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library, menu)
val reorganizeItem = menu.findItem(R.id.action_reorganize)
reorganizeItem.isVisible = preferences.librarySortingMode().getOrDefault() == LibrarySort.DRAG_AND_DROP
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
@ -417,12 +427,22 @@ class LibraryController(
R.id.action_source_migration -> {
router.pushController(MigrationController().withFadeTransaction())
}
R.id.action_alpha_asc -> reOrder(1)
R.id.action_alpha_dsc -> reOrder(2)
R.id.action_update_asc -> reOrder(3)
R.id.action_update_dsc -> reOrder(4)
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun reOrder(type: Int) {
adapter?.categories?.getOrNull(library_pager.currentItem)?.id?.let {
reorganizeRelay.call(it to type)
}
}
/**
* Invalidates the action mode, forcing it to refresh its content.
*/

View File

@ -20,7 +20,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
*/
class LibraryGridHolder(
private val view: View,
private val adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
) : LibraryHolder(view, adapter) {

View File

@ -15,7 +15,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
abstract class LibraryHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
val adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
) : BaseFlexibleViewHolder(view, adapter) {
/**
@ -26,4 +26,15 @@ abstract class LibraryHolder(
*/
abstract fun onSetValues(item: LibraryItem)
/**
* Called when an item is released.
*
* @param position The position of the released item.
*/
override fun onItemReleased(position: Int) {
super.onItemReleased(position)
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position)
}
}

View File

@ -51,6 +51,13 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference
holder.onSetValues(this)
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return true
}
/**
* Filters a manga depending on a query.
*

View File

@ -21,7 +21,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
class LibraryListHolder(
private val view: View,
private val adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
) : LibraryHolder(view, adapter) {
/**

View File

@ -131,7 +131,10 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
private val source = Item.MultiSort(R.string.manga_info_source_label, this)
override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source)
private val dragAndDrop = Item.MultiSort(R.string.action_sort_drag_and_drop, this)
override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source,
dragAndDrop)
override val header = Item.Header(R.string.action_sort)
@ -148,6 +151,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE
source.state = if (sorting == LibrarySort.SOURCE) order else SORT_NONE
dragAndDrop.state = if (sorting == LibrarySort.DRAG_AND_DROP) order else SORT_NONE
}
override fun onItemClicked(item: Item) {
@ -155,12 +159,15 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
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")
}
if (item == dragAndDrop)
item.state = SORT_ASC
else
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
@ -169,9 +176,10 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
unread -> LibrarySort.UNREAD
total -> LibrarySort.TOTAL
source -> LibrarySort.SOURCE
dragAndDrop -> LibrarySort.DRAG_AND_DROP
else -> throw Exception("Unknown sorting")
})
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
preferences.librarySortingAscending().set(item.state == SORT_ASC)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}

View File

@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.migration.MigrationFlags
import eu.kanade.tachiyomi.util.combineLatest
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
import eu.kanade.tachiyomi.util.removeArticles
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_EXCLUDE
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_IGNORE
@ -210,6 +211,9 @@ class LibraryPresenter(
val mangaCompare = source1Name.compareTo(source2Name)
if (mangaCompare == 0) sortAlphabetical(i1, i2) else mangaCompare
}
LibrarySort.DRAG_AND_DROP -> {
0
}
else -> throw Exception("Unknown sorting mode")
}
}
@ -228,10 +232,6 @@ class LibraryPresenter(
else i1.manga.title.compareTo(i2.manga.title, true)
}
private fun String.removeArticles(): String {
return this.replace(Regex("^(an|a|the) ", RegexOption.IGNORE_CASE), "")
}
/**
* Get the categories and all its manga from the database.
*

View File

@ -8,4 +8,5 @@ object LibrarySort {
const val UNREAD = 3
const val TOTAL = 4
const val SOURCE = 5
const val DRAG_AND_DROP = 6
}

View File

@ -14,6 +14,10 @@ fun String.chop(count: Int, replacement: String = "..."): String {
}
fun String.removeArticles(): String {
return this.replace(Regex("^(an|a|the) ", RegexOption.IGNORE_CASE), "")
}
/**
* Replaces the given string to have at most [count] characters using [replacement] near the center.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.

View File

@ -32,4 +32,24 @@
android:title="@string/label_migration"
app:showAsAction="never"/>
<item
android:id="@+id/action_reorganize"
android:title="@string/label_reorganize_by"
app:showAsAction="never">
<menu>
<item
android:id="@+id/action_alpha_asc"
android:title="@string/action_sort_alpha"/>
<item
android:id="@+id/action_alpha_dsc"
android:title="@string/label_alpha_reverse"/>
<item
android:id="@+id/action_update_asc"
android:title="@string/action_sort_last_updated"/>
<item
android:id="@+id/action_update_dsc"
android:title="@string/action_sort_first_updated"/>
</menu>
</item>
</menu>

View File

@ -22,6 +22,8 @@
<string name="label_selected">Selected: %1$d</string>
<string name="label_backup">Backup</string>
<string name="label_migration">Source migration</string>
<string name="label_reorganize_by">Re-order</string>
<string name="label_alpha_reverse">Alphabetically (descending)</string>
<string name="label_hide_title">Hide title</string>
<string name="label_show_title">Show title</string>
<string name="label_extensions">Extensions</string>
@ -43,6 +45,8 @@
<string name="action_sort_total">Total chapters</string>
<string name="action_sort_last_read">Last read</string>
<string name="action_sort_last_updated">Last updated</string>
<string name="action_sort_first_updated">First updated</string>
<string name="action_sort_drag_and_drop">Drag &amp; Drop</string>
<string name="action_search">Search</string>
<string name="action_skip_manga">Skip manga</string>
<string name="action_global_search">Global search</string>