Unix line endings
This commit is contained in:
@@ -1,48 +1,48 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
||||
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
|
||||
|
||||
private var mangas = emptyList<Manga>()
|
||||
|
||||
private var categories = emptyList<Category>()
|
||||
|
||||
private var preselected = emptyArray<Int>()
|
||||
|
||||
constructor(target: T, mangas: List<Manga>, categories: List<Category>,
|
||||
preselected: Array<Int>) : this() {
|
||||
|
||||
this.mangas = mangas
|
||||
this.categories = categories
|
||||
this.preselected = preselected
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.action_move_category)
|
||||
.items(categories.map { it.name })
|
||||
.itemsCallbackMultiChoice(preselected) { dialog, _, _ ->
|
||||
val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty()
|
||||
(targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories)
|
||||
true
|
||||
}
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
||||
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
|
||||
|
||||
private var mangas = emptyList<Manga>()
|
||||
|
||||
private var categories = emptyList<Category>()
|
||||
|
||||
private var preselected = emptyArray<Int>()
|
||||
|
||||
constructor(target: T, mangas: List<Manga>, categories: List<Category>,
|
||||
preselected: Array<Int>) : this() {
|
||||
|
||||
this.mangas = mangas
|
||||
this.categories = categories
|
||||
this.preselected = preselected
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.action_move_category)
|
||||
.items(categories.map { it.name })
|
||||
.itemsCallbackMultiChoice(preselected) { dialog, _, _ ->
|
||||
val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty()
|
||||
(targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories)
|
||||
true
|
||||
}
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +1,43 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.widget.DialogCheckboxView
|
||||
|
||||
class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
|
||||
DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener {
|
||||
|
||||
private var mangas = emptyList<Manga>()
|
||||
|
||||
constructor(target: T, mangas: List<Manga>) : this() {
|
||||
this.mangas = mangas
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val view = DialogCheckboxView(activity!!).apply {
|
||||
setDescription(R.string.confirm_delete_manga)
|
||||
setOptionDescription(R.string.also_delete_chapters)
|
||||
}
|
||||
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.action_remove)
|
||||
.customView(view, true)
|
||||
.positiveText(android.R.string.yes)
|
||||
.negativeText(android.R.string.no)
|
||||
.onPositive { _, _ ->
|
||||
val deleteChapters = view.isChecked()
|
||||
(targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean)
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.widget.DialogCheckboxView
|
||||
|
||||
class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
|
||||
DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener {
|
||||
|
||||
private var mangas = emptyList<Manga>()
|
||||
|
||||
constructor(target: T, mangas: List<Manga>) : this() {
|
||||
this.mangas = mangas
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val view = DialogCheckboxView(activity!!).apply {
|
||||
setDescription(R.string.confirm_delete_manga)
|
||||
setOptionDescription(R.string.also_delete_chapters)
|
||||
}
|
||||
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.action_remove)
|
||||
.customView(view, true)
|
||||
.positiveText(android.R.string.yes)
|
||||
.negativeText(android.R.string.no)
|
||||
.onPositive { _, _ ->
|
||||
val deleteChapters = view.isChecked()
|
||||
(targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +1,103 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||
|
||||
/**
|
||||
* This adapter stores the categories from the library, used with a ViewPager.
|
||||
*
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
|
||||
|
||||
/**
|
||||
* The categories to bind in the adapter.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private var boundViews = arrayListOf<View>()
|
||||
|
||||
/**
|
||||
* Creates a new view for this adapter.
|
||||
*
|
||||
* @return a new view.
|
||||
*/
|
||||
override fun createView(container: ViewGroup): View {
|
||||
val view = container.inflate(R.layout.library_category) as LibraryCategoryView
|
||||
view.onCreate(controller)
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a view with a position.
|
||||
*
|
||||
* @param view the view to bind.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun bindView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onBind(categories[position])
|
||||
boundViews.add(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recycles a view.
|
||||
*
|
||||
* @param view the view to recycle.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun recycleView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onRecycle()
|
||||
boundViews.remove(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of categories.
|
||||
*
|
||||
* @return the number of categories or 0 if the list is null.
|
||||
*/
|
||||
override fun getCount(): Int {
|
||||
return categories.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title to display for a category.
|
||||
*
|
||||
* @param position the position of the element.
|
||||
* @return the title to display.
|
||||
*/
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return categories[position].name
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the view.
|
||||
*/
|
||||
override fun getItemPosition(obj: Any): Int {
|
||||
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
|
||||
val index = categories.indexOfFirst { it.id == view.category.id }
|
||||
return if (index == -1) POSITION_NONE else index
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view of this adapter is being destroyed.
|
||||
*/
|
||||
fun onDestroy() {
|
||||
for (view in boundViews) {
|
||||
if (view is LibraryCategoryView) {
|
||||
view.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||
|
||||
/**
|
||||
* This adapter stores the categories from the library, used with a ViewPager.
|
||||
*
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
|
||||
|
||||
/**
|
||||
* The categories to bind in the adapter.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private var boundViews = arrayListOf<View>()
|
||||
|
||||
/**
|
||||
* Creates a new view for this adapter.
|
||||
*
|
||||
* @return a new view.
|
||||
*/
|
||||
override fun createView(container: ViewGroup): View {
|
||||
val view = container.inflate(R.layout.library_category) as LibraryCategoryView
|
||||
view.onCreate(controller)
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a view with a position.
|
||||
*
|
||||
* @param view the view to bind.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun bindView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onBind(categories[position])
|
||||
boundViews.add(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recycles a view.
|
||||
*
|
||||
* @param view the view to recycle.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun recycleView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onRecycle()
|
||||
boundViews.remove(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of categories.
|
||||
*
|
||||
* @return the number of categories or 0 if the list is null.
|
||||
*/
|
||||
override fun getCount(): Int {
|
||||
return categories.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title to display for a category.
|
||||
*
|
||||
* @param position the position of the element.
|
||||
* @return the title to display.
|
||||
*/
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return categories[position].name
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the view.
|
||||
*/
|
||||
override fun getItemPosition(obj: Any): Int {
|
||||
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
|
||||
val index = categories.indexOfFirst { it.id == view.category.id }
|
||||
return if (index == -1) POSITION_NONE else index
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view of this adapter is being destroyed.
|
||||
*/
|
||||
fun onDestroy() {
|
||||
for (view in boundViews) {
|
||||
if (view is LibraryCategoryView) {
|
||||
view.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
/**
|
||||
* Adapter storing a list of manga in a certain category.
|
||||
*
|
||||
* @param view the fragment containing this adapter.
|
||||
*/
|
||||
class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
||||
|
||||
/**
|
||||
* The list of manga in this category.
|
||||
*/
|
||||
private var mangas: List<LibraryItem> = emptyList()
|
||||
|
||||
/**
|
||||
* Sets a list of manga in the adapter.
|
||||
*
|
||||
* @param list the list to set.
|
||||
*/
|
||||
fun setItems(list: List<LibraryItem>) {
|
||||
// A copy of manga always unfiltered.
|
||||
mangas = list.toList()
|
||||
|
||||
performFilter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position in the adapter for the given manga.
|
||||
*
|
||||
* @param manga the manga to find.
|
||||
*/
|
||||
fun indexOf(manga: Manga): Int {
|
||||
return currentItems.indexOfFirst { it.manga.id == manga.id }
|
||||
}
|
||||
|
||||
fun performFilter() {
|
||||
updateDataSet(mangas.filter { it.filter(searchText) })
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
/**
|
||||
* Adapter storing a list of manga in a certain category.
|
||||
*
|
||||
* @param view the fragment containing this adapter.
|
||||
*/
|
||||
class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
||||
|
||||
/**
|
||||
* The list of manga in this category.
|
||||
*/
|
||||
private var mangas: List<LibraryItem> = emptyList()
|
||||
|
||||
/**
|
||||
* Sets a list of manga in the adapter.
|
||||
*
|
||||
* @param list the list to set.
|
||||
*/
|
||||
fun setItems(list: List<LibraryItem>) {
|
||||
// A copy of manga always unfiltered.
|
||||
mangas = list.toList()
|
||||
|
||||
performFilter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position in the adapter for the given manga.
|
||||
*
|
||||
* @param manga the manga to find.
|
||||
*/
|
||||
fun indexOf(manga: Manga): Int {
|
||||
return currentItems.indexOfFirst { it.manga.id == manga.id }
|
||||
}
|
||||
|
||||
fun performFilter() {
|
||||
updateDataSet(mangas.filter { it.filter(searchText) })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,247 +1,247 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.util.inflate
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.library_category.view.*
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Fragment containing the library manga for a certain category.
|
||||
*/
|
||||
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
FrameLayout(context, attrs),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener {
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* The fragment containing this view.
|
||||
*/
|
||||
private lateinit var controller: LibraryController
|
||||
|
||||
/**
|
||||
* Category for this view.
|
||||
*/
|
||||
lateinit var category: Category
|
||||
private set
|
||||
|
||||
/**
|
||||
* Recycler view of the list of manga.
|
||||
*/
|
||||
private lateinit var recycler: RecyclerView
|
||||
|
||||
/**
|
||||
* Adapter to hold the manga in this category.
|
||||
*/
|
||||
private lateinit var adapter: LibraryCategoryAdapter
|
||||
|
||||
/**
|
||||
* Subscriptions while the view is bound.
|
||||
*/
|
||||
private var subscriptions = CompositeSubscription()
|
||||
|
||||
fun onCreate(controller: LibraryController) {
|
||||
this.controller = controller
|
||||
|
||||
recycler = if (preferences.libraryAsList().getOrDefault()) {
|
||||
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
} else {
|
||||
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
||||
spanCount = controller.mangaPerRow
|
||||
}
|
||||
}
|
||||
|
||||
adapter = LibraryCategoryAdapter(this)
|
||||
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
swipe_refresh.addView(recycler)
|
||||
|
||||
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos <= 0
|
||||
}
|
||||
})
|
||||
|
||||
// Double the distance required to trigger sync
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.setOnRefreshListener {
|
||||
if (!LibraryUpdateService.isRunning(context)) {
|
||||
LibraryUpdateService.start(context, category)
|
||||
context.toast(R.string.updating_category)
|
||||
}
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onBind(category: Category) {
|
||||
this.category = category
|
||||
|
||||
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
|
||||
SelectableAdapter.Mode.MULTI
|
||||
} else {
|
||||
SelectableAdapter.Mode.SINGLE
|
||||
}
|
||||
|
||||
subscriptions += controller.searchRelay
|
||||
.doOnNext { adapter.searchText = it }
|
||||
.skip(1)
|
||||
.subscribe { adapter.performFilter() }
|
||||
|
||||
subscriptions += controller.libraryMangaRelay
|
||||
.subscribe { onNextLibraryManga(it) }
|
||||
|
||||
subscriptions += controller.selectionRelay
|
||||
.subscribe { onSelectionChanged(it) }
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
adapter.setItems(emptyList())
|
||||
adapter.clearSelection()
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
fun unsubscribe() {
|
||||
subscriptions.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
|
||||
* adapter.
|
||||
*
|
||||
* @param event the event received.
|
||||
*/
|
||||
fun onNextLibraryManga(event: LibraryMangaEvent) {
|
||||
// Get the manga list for this category.
|
||||
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
||||
|
||||
// Update the category with its manga.
|
||||
adapter.setItems(mangaForCategory)
|
||||
|
||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
controller.selectedMangas.forEach { manga ->
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1 && !adapter.isSelected(position)) {
|
||||
adapter.toggleSelection(position)
|
||||
(recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
|
||||
* depending on the type of event received.
|
||||
*
|
||||
* @param event the selection event received.
|
||||
*/
|
||||
private fun onSelectionChanged(event: LibrarySelectionEvent) {
|
||||
when (event) {
|
||||
is LibrarySelectionEvent.Selected -> {
|
||||
if (adapter.mode != SelectableAdapter.Mode.MULTI) {
|
||||
adapter.mode = SelectableAdapter.Mode.MULTI
|
||||
}
|
||||
findAndToggleSelection(event.manga)
|
||||
}
|
||||
is LibrarySelectionEvent.Unselected -> {
|
||||
findAndToggleSelection(event.manga)
|
||||
if (controller.selectedMangas.isEmpty()) {
|
||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
||||
}
|
||||
}
|
||||
is LibrarySelectionEvent.Cleared -> {
|
||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
||||
adapter.clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection for the given manga and updates the view if needed.
|
||||
*
|
||||
* @param manga the manga to toggle.
|
||||
*/
|
||||
private fun findAndToggleSelection(manga: Manga) {
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1) {
|
||||
adapter.toggleSelection(position)
|
||||
(recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
* @return true if the item should be selected, false otherwise.
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
// If the action mode is created and the position is valid, toggle the selection.
|
||||
val item = adapter.getItem(position) ?: return false
|
||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openManga(item.manga)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is long clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
controller.createActionModeIfNeeded()
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a manga.
|
||||
*
|
||||
* @param manga the manga to open.
|
||||
*/
|
||||
private fun openManga(manga: Manga) {
|
||||
controller.openManga(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the presenter to toggle the selection for the given position.
|
||||
*
|
||||
* @param position the position to toggle.
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val item = adapter.getItem(position) ?: return
|
||||
|
||||
controller.setSelection(item.manga, !adapter.isSelected(position))
|
||||
controller.invalidateActionMode()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.util.inflate
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.library_category.view.*
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Fragment containing the library manga for a certain category.
|
||||
*/
|
||||
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
FrameLayout(context, attrs),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener {
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* The fragment containing this view.
|
||||
*/
|
||||
private lateinit var controller: LibraryController
|
||||
|
||||
/**
|
||||
* Category for this view.
|
||||
*/
|
||||
lateinit var category: Category
|
||||
private set
|
||||
|
||||
/**
|
||||
* Recycler view of the list of manga.
|
||||
*/
|
||||
private lateinit var recycler: RecyclerView
|
||||
|
||||
/**
|
||||
* Adapter to hold the manga in this category.
|
||||
*/
|
||||
private lateinit var adapter: LibraryCategoryAdapter
|
||||
|
||||
/**
|
||||
* Subscriptions while the view is bound.
|
||||
*/
|
||||
private var subscriptions = CompositeSubscription()
|
||||
|
||||
fun onCreate(controller: LibraryController) {
|
||||
this.controller = controller
|
||||
|
||||
recycler = if (preferences.libraryAsList().getOrDefault()) {
|
||||
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
} else {
|
||||
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
||||
spanCount = controller.mangaPerRow
|
||||
}
|
||||
}
|
||||
|
||||
adapter = LibraryCategoryAdapter(this)
|
||||
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
swipe_refresh.addView(recycler)
|
||||
|
||||
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos <= 0
|
||||
}
|
||||
})
|
||||
|
||||
// Double the distance required to trigger sync
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.setOnRefreshListener {
|
||||
if (!LibraryUpdateService.isRunning(context)) {
|
||||
LibraryUpdateService.start(context, category)
|
||||
context.toast(R.string.updating_category)
|
||||
}
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onBind(category: Category) {
|
||||
this.category = category
|
||||
|
||||
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
|
||||
SelectableAdapter.Mode.MULTI
|
||||
} else {
|
||||
SelectableAdapter.Mode.SINGLE
|
||||
}
|
||||
|
||||
subscriptions += controller.searchRelay
|
||||
.doOnNext { adapter.searchText = it }
|
||||
.skip(1)
|
||||
.subscribe { adapter.performFilter() }
|
||||
|
||||
subscriptions += controller.libraryMangaRelay
|
||||
.subscribe { onNextLibraryManga(it) }
|
||||
|
||||
subscriptions += controller.selectionRelay
|
||||
.subscribe { onSelectionChanged(it) }
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
adapter.setItems(emptyList())
|
||||
adapter.clearSelection()
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
fun unsubscribe() {
|
||||
subscriptions.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
|
||||
* adapter.
|
||||
*
|
||||
* @param event the event received.
|
||||
*/
|
||||
fun onNextLibraryManga(event: LibraryMangaEvent) {
|
||||
// Get the manga list for this category.
|
||||
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
||||
|
||||
// Update the category with its manga.
|
||||
adapter.setItems(mangaForCategory)
|
||||
|
||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
controller.selectedMangas.forEach { manga ->
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1 && !adapter.isSelected(position)) {
|
||||
adapter.toggleSelection(position)
|
||||
(recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
|
||||
* depending on the type of event received.
|
||||
*
|
||||
* @param event the selection event received.
|
||||
*/
|
||||
private fun onSelectionChanged(event: LibrarySelectionEvent) {
|
||||
when (event) {
|
||||
is LibrarySelectionEvent.Selected -> {
|
||||
if (adapter.mode != SelectableAdapter.Mode.MULTI) {
|
||||
adapter.mode = SelectableAdapter.Mode.MULTI
|
||||
}
|
||||
findAndToggleSelection(event.manga)
|
||||
}
|
||||
is LibrarySelectionEvent.Unselected -> {
|
||||
findAndToggleSelection(event.manga)
|
||||
if (controller.selectedMangas.isEmpty()) {
|
||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
||||
}
|
||||
}
|
||||
is LibrarySelectionEvent.Cleared -> {
|
||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
||||
adapter.clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection for the given manga and updates the view if needed.
|
||||
*
|
||||
* @param manga the manga to toggle.
|
||||
*/
|
||||
private fun findAndToggleSelection(manga: Manga) {
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1) {
|
||||
adapter.toggleSelection(position)
|
||||
(recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
* @return true if the item should be selected, false otherwise.
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
// If the action mode is created and the position is valid, toggle the selection.
|
||||
val item = adapter.getItem(position) ?: return false
|
||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openManga(item.manga)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is long clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
controller.createActionModeIfNeeded()
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a manga.
|
||||
*
|
||||
* @param manga the manga to open.
|
||||
*/
|
||||
private fun openManga(manga: Manga) {
|
||||
controller.openManga(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the presenter to toggle the selection for the given position.
|
||||
*
|
||||
* @param position the position to toggle.
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val item = adapter.getItem(position) ?: return
|
||||
|
||||
controller.setSelection(item.manga, !adapter.isSelected(position))
|
||||
controller.invalidateActionMode()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,57 +1,57 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import kotlinx.android.synthetic.main.catalogue_grid_item.*
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
* All the elements from the layout file "item_catalogue_grid" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
class LibraryGridHolder(
|
||||
private val view: View,
|
||||
private val adapter: FlexibleAdapter<*>
|
||||
|
||||
) : LibraryHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param item the manga item to bind.
|
||||
*/
|
||||
override fun onSetValues(item: LibraryItem) {
|
||||
// Update the title of the manga.
|
||||
title.text = item.manga.title
|
||||
|
||||
// Update the unread count and its visibility.
|
||||
with(unread_text) {
|
||||
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
|
||||
text = item.manga.unread.toString()
|
||||
}
|
||||
// Update the download count and its visibility.
|
||||
with(download_text) {
|
||||
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
|
||||
text = item.downloadCount.toString()
|
||||
}
|
||||
//set local visibility if its local manga
|
||||
local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
|
||||
|
||||
// Update the cover.
|
||||
GlideApp.with(view.context).clear(thumbnail)
|
||||
GlideApp.with(view.context)
|
||||
.load(item.manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(thumbnail)
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import kotlinx.android.synthetic.main.catalogue_grid_item.*
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
* All the elements from the layout file "item_catalogue_grid" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
class LibraryGridHolder(
|
||||
private val view: View,
|
||||
private val adapter: FlexibleAdapter<*>
|
||||
|
||||
) : LibraryHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param item the manga item to bind.
|
||||
*/
|
||||
override fun onSetValues(item: LibraryItem) {
|
||||
// Update the title of the manga.
|
||||
title.text = item.manga.title
|
||||
|
||||
// Update the unread count and its visibility.
|
||||
with(unread_text) {
|
||||
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
|
||||
text = item.manga.unread.toString()
|
||||
}
|
||||
// Update the download count and its visibility.
|
||||
with(download_text) {
|
||||
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
|
||||
text = item.downloadCount.toString()
|
||||
}
|
||||
//set local visibility if its local manga
|
||||
local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
|
||||
|
||||
// Update the cover.
|
||||
GlideApp.with(view.context).clear(thumbnail)
|
||||
GlideApp.with(view.context)
|
||||
.load(item.manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(thumbnail)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
|
||||
/**
|
||||
* Generic class used to hold the displayed data of a manga in the library.
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to the single tap and long tap events.
|
||||
*/
|
||||
|
||||
abstract class LibraryHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<*>
|
||||
) : BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param item the manga item to bind.
|
||||
*/
|
||||
abstract fun onSetValues(item: LibraryItem)
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
|
||||
/**
|
||||
* Generic class used to hold the displayed data of a manga in the library.
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to the single tap and long tap events.
|
||||
*/
|
||||
|
||||
abstract class LibraryHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<*>
|
||||
) : BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param item the manga item to bind.
|
||||
*/
|
||||
abstract fun onSetValues(item: LibraryItem)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||
|
||||
class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
|
||||
AbstractFlexibleItem<LibraryHolder>(), IFilterable {
|
||||
|
||||
var downloadCount = -1
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return if (libraryAsList.getOrDefault())
|
||||
R.layout.catalogue_list_item
|
||||
else
|
||||
R.layout.catalogue_grid_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LibraryHolder {
|
||||
val parent = adapter.recyclerView
|
||||
return if (parent is AutofitRecyclerView) {
|
||||
view.apply {
|
||||
val coverHeight = parent.itemWidth / 3 * 4
|
||||
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
|
||||
gradient.layoutParams = FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
|
||||
}
|
||||
LibraryGridHolder(view, adapter)
|
||||
} else {
|
||||
LibraryListHolder(view, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||
holder: LibraryHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
|
||||
holder.onSetValues(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a manga depending on a query.
|
||||
*
|
||||
* @param constraint the query to apply.
|
||||
* @return true if the manga should be included, false otherwise.
|
||||
*/
|
||||
override fun filter(constraint: String): Boolean {
|
||||
return manga.title.contains(constraint, true) ||
|
||||
(manga.author?.contains(constraint, true) ?: false)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LibraryItem) {
|
||||
return manga.id == other.manga.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return manga.id!!.hashCode()
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||
|
||||
class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
|
||||
AbstractFlexibleItem<LibraryHolder>(), IFilterable {
|
||||
|
||||
var downloadCount = -1
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return if (libraryAsList.getOrDefault())
|
||||
R.layout.catalogue_list_item
|
||||
else
|
||||
R.layout.catalogue_grid_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LibraryHolder {
|
||||
val parent = adapter.recyclerView
|
||||
return if (parent is AutofitRecyclerView) {
|
||||
view.apply {
|
||||
val coverHeight = parent.itemWidth / 3 * 4
|
||||
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
|
||||
gradient.layoutParams = FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
|
||||
}
|
||||
LibraryGridHolder(view, adapter)
|
||||
} else {
|
||||
LibraryListHolder(view, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||
holder: LibraryHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
|
||||
holder.onSetValues(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a manga depending on a query.
|
||||
*
|
||||
* @param constraint the query to apply.
|
||||
* @return true if the manga should be included, false otherwise.
|
||||
*/
|
||||
override fun filter(constraint: String): Boolean {
|
||||
return manga.title.contains(constraint, true) ||
|
||||
(manga.author?.contains(constraint, true) ?: false)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LibraryItem) {
|
||||
return manga.id == other.manga.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return manga.id!!.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,65 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import kotlinx.android.synthetic.main.catalogue_list_item.*
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
* All the elements from the layout file "item_library_list" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
|
||||
class LibraryListHolder(
|
||||
private val view: View,
|
||||
private val adapter: FlexibleAdapter<*>
|
||||
) : LibraryHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param item the manga item to bind.
|
||||
*/
|
||||
override fun onSetValues(item: LibraryItem) {
|
||||
// Update the title of the manga.
|
||||
title.text = item.manga.title
|
||||
|
||||
// Update the unread count and its visibility.
|
||||
with(unread_text) {
|
||||
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
|
||||
text = item.manga.unread.toString()
|
||||
}
|
||||
// Update the download count and its visibility.
|
||||
with(download_text) {
|
||||
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
|
||||
text = "${item.downloadCount}"
|
||||
}
|
||||
//show local text badge if local manga
|
||||
local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
|
||||
|
||||
// Create thumbnail onclick to simulate long click
|
||||
thumbnail.setOnClickListener {
|
||||
// Simulate long click on this view to enter selection mode
|
||||
onLongClick(itemView)
|
||||
}
|
||||
|
||||
// Update the cover.
|
||||
GlideApp.with(itemView.context).clear(thumbnail)
|
||||
GlideApp.with(itemView.context)
|
||||
.load(item.manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.circleCrop()
|
||||
.dontAnimate()
|
||||
.into(thumbnail)
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import kotlinx.android.synthetic.main.catalogue_list_item.*
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
* All the elements from the layout file "item_library_list" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
|
||||
class LibraryListHolder(
|
||||
private val view: View,
|
||||
private val adapter: FlexibleAdapter<*>
|
||||
) : LibraryHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param item the manga item to bind.
|
||||
*/
|
||||
override fun onSetValues(item: LibraryItem) {
|
||||
// Update the title of the manga.
|
||||
title.text = item.manga.title
|
||||
|
||||
// Update the unread count and its visibility.
|
||||
with(unread_text) {
|
||||
visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
|
||||
text = item.manga.unread.toString()
|
||||
}
|
||||
// Update the download count and its visibility.
|
||||
with(download_text) {
|
||||
visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
|
||||
text = "${item.downloadCount}"
|
||||
}
|
||||
//show local text badge if local manga
|
||||
local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
|
||||
|
||||
// Create thumbnail onclick to simulate long click
|
||||
thumbnail.setOnClickListener {
|
||||
// Simulate long click on this view to enter selection mode
|
||||
onLongClick(itemView)
|
||||
}
|
||||
|
||||
// Update the cover.
|
||||
GlideApp.with(itemView.context).clear(thumbnail)
|
||||
GlideApp.with(itemView.context)
|
||||
.load(item.manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.circleCrop()
|
||||
.dontAnimate()
|
||||
.into(thumbnail)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,217 +1,217 @@
|
||||
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 lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
|
||||
|
||||
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
|
||||
|
||||
private val source = Item.MultiSort(R.string.manga_info_source_label, this)
|
||||
|
||||
override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source)
|
||||
|
||||
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
|
||||
lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
lastUpdated -> LibrarySort.LAST_UPDATED
|
||||
unread -> LibrarySort.UNREAD
|
||||
total -> LibrarySort.TOTAL
|
||||
source -> LibrarySort.SOURCE
|
||||
else -> throw Exception("Unknown sorting")
|
||||
})
|
||||
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
|
||||
|
||||
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(if (item == list) true else false)
|
||||
|
||||
item.group.items.forEach { adapter.notifyItemChanged(it) }
|
||||
}
|
||||
}
|
||||
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 lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
|
||||
|
||||
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
|
||||
|
||||
private val source = Item.MultiSort(R.string.manga_info_source_label, this)
|
||||
|
||||
override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source)
|
||||
|
||||
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
|
||||
lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
lastUpdated -> LibrarySort.LAST_UPDATED
|
||||
unread -> LibrarySort.UNREAD
|
||||
total -> LibrarySort.TOTAL
|
||||
source -> LibrarySort.SOURCE
|
||||
else -> throw Exception("Unknown sorting")
|
||||
})
|
||||
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
|
||||
|
||||
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(if (item == list) true else false)
|
||||
|
||||
item.group.items.forEach { adapter.notifyItemChanged(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,371 +1,371 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
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.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.combineLatest
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.ArrayList
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
|
||||
/**
|
||||
* Class containing library information.
|
||||
*/
|
||||
private data class Library(val categories: List<Category>, val mangaMap: LibraryMap)
|
||||
|
||||
/**
|
||||
* Typealias for the library manga, using the category as keys, and list of manga as values.
|
||||
*/
|
||||
private typealias LibraryMap = Map<Int, List<LibraryItem>>
|
||||
|
||||
/**
|
||||
* Presenter of [LibraryController].
|
||||
*/
|
||||
class LibraryPresenter(
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
) : BasePresenter<LibraryController>() {
|
||||
|
||||
private val context = preferences.context
|
||||
|
||||
/**
|
||||
* Categories of the library.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI filters to the last emission of the library.
|
||||
*/
|
||||
private val filterTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI update to the last emission of the library.
|
||||
*/
|
||||
private val downloadTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Relay used to apply the selected sorting method to the last emission of the library.
|
||||
*/
|
||||
private val sortTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Library subscription.
|
||||
*/
|
||||
private var librarySubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
subscribeLibrary()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to library if needed.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
if (librarySubscription.isNullOrUnsubscribed()) {
|
||||
librarySubscription = getLibraryObservable()
|
||||
.combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, _ -> lib.apply { setDownloadCount(mangaMap) } })
|
||||
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) })
|
||||
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) })
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache({ view, (categories, mangaMap) ->
|
||||
view.onNextLibraryUpdate(categories, mangaMap)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library filters to the given map of manga.
|
||||
*
|
||||
* @param map the map to filter.
|
||||
*/
|
||||
private fun applyFilters(map: LibraryMap): LibraryMap {
|
||||
val filterDownloaded = preferences.filterDownloaded().getOrDefault()
|
||||
|
||||
val filterUnread = preferences.filterUnread().getOrDefault()
|
||||
|
||||
val filterCompleted = preferences.filterCompleted().getOrDefault()
|
||||
|
||||
val filterFn: (LibraryItem) -> Boolean = f@ { item ->
|
||||
// Filter when there isn't unread chapters.
|
||||
if (filterUnread && item.manga.unread == 0) {
|
||||
return@f false
|
||||
}
|
||||
|
||||
if (filterCompleted && item.manga.status != SManga.COMPLETED) {
|
||||
return@f false
|
||||
}
|
||||
|
||||
// Filter when there are no downloads.
|
||||
if (filterDownloaded) {
|
||||
// Local manga are always downloaded
|
||||
if (item.manga.source == LocalSource.ID) {
|
||||
return@f true
|
||||
}
|
||||
// Don't bother with directory checking if download count has been set.
|
||||
if (item.downloadCount != -1) {
|
||||
return@f item.downloadCount > 0
|
||||
}
|
||||
|
||||
return@f downloadManager.getDownloadCount(item.manga) > 0
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets downloaded chapter count to each manga.
|
||||
*
|
||||
* @param map the map of manga.
|
||||
*/
|
||||
private fun setDownloadCount(map: LibraryMap) {
|
||||
if (!preferences.downloadBadge().getOrDefault()) {
|
||||
// Unset download count if the preference is not enabled.
|
||||
for ((_, itemList) in map) {
|
||||
for (item in itemList) {
|
||||
item.downloadCount = -1
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for ((_, itemList) in map) {
|
||||
for (item in itemList) {
|
||||
item.downloadCount = downloadManager.getDownloadCount(item.manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library sorting to the given map of manga.
|
||||
*
|
||||
* @param map the map to sort.
|
||||
*/
|
||||
private fun applySort(map: LibraryMap): LibraryMap {
|
||||
val sortingMode = preferences.librarySortingMode().getOrDefault()
|
||||
|
||||
val lastReadManga by lazy {
|
||||
var counter = 0
|
||||
db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||
}
|
||||
val totalChapterManga by lazy {
|
||||
var counter = 0
|
||||
db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||
}
|
||||
|
||||
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||
when (sortingMode) {
|
||||
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
|
||||
LibrarySort.LAST_READ -> {
|
||||
// Get index of manga, set equal to list if size unknown.
|
||||
val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
|
||||
val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size
|
||||
manga1LastRead.compareTo(manga2LastRead)
|
||||
}
|
||||
LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update)
|
||||
LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread)
|
||||
LibrarySort.TOTAL -> {
|
||||
val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
|
||||
val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
|
||||
manga1TotalChapter.compareTo(mange2TotalChapter)
|
||||
}
|
||||
LibrarySort.SOURCE -> {
|
||||
val source1Name = sourceManager.getOrStub(i1.manga.source).name
|
||||
val source2Name = sourceManager.getOrStub(i2.manga.source).name
|
||||
source1Name.compareTo(source2Name)
|
||||
}
|
||||
else -> throw Exception("Unknown sorting mode")
|
||||
}
|
||||
}
|
||||
|
||||
val comparator = if (preferences.librarySortingAscending().getOrDefault())
|
||||
Comparator(sortFn)
|
||||
else
|
||||
Collections.reverseOrder(sortFn)
|
||||
|
||||
return map.mapValues { entry -> entry.value.sortedWith(comparator) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories and all its manga from the database.
|
||||
*
|
||||
* @return an observable of the categories and its manga.
|
||||
*/
|
||||
private fun getLibraryObservable(): Observable<Library> {
|
||||
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
|
||||
{ dbCategories, libraryManga ->
|
||||
val categories = if (libraryManga.containsKey(0))
|
||||
arrayListOf(Category.createDefault()) + dbCategories
|
||||
else
|
||||
dbCategories
|
||||
|
||||
this.categories = categories
|
||||
Library(categories, libraryManga)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories from the database.
|
||||
*
|
||||
* @return an observable of the categories.
|
||||
*/
|
||||
private fun getCategoriesObservable(): Observable<List<Category>> {
|
||||
return db.getCategories().asRxObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the manga grouped by categories.
|
||||
*
|
||||
* @return an observable containing a map with the category id as key and a list of manga as the
|
||||
* value.
|
||||
*/
|
||||
private fun getLibraryMangasObservable(): Observable<LibraryMap> {
|
||||
val libraryAsList = preferences.libraryAsList()
|
||||
return db.getLibraryMangas().asRxObservable()
|
||||
.map { list ->
|
||||
list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be filtered.
|
||||
*/
|
||||
fun requestFilterUpdate() {
|
||||
filterTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to have download badges added.
|
||||
*/
|
||||
fun requestDownloadBadgesUpdate() {
|
||||
downloadTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be sorted.
|
||||
*/
|
||||
fun requestSortUpdate() {
|
||||
sortTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is opened.
|
||||
*/
|
||||
fun onOpenManga() {
|
||||
// Avoid further db updates for the library when it's not needed
|
||||
librarySubscription?.let { remove(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common categories for the given list of manga.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
return mangas.toSet()
|
||||
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
||||
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the selected manga from the library.
|
||||
*
|
||||
* @param mangas the list of manga to delete.
|
||||
* @param deleteChapters whether to also delete downloaded chapters.
|
||||
*/
|
||||
fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
|
||||
// Create a set of the list
|
||||
val mangaToDelete = mangas.distinctBy { it.id }
|
||||
mangaToDelete.forEach { it.favorite = false }
|
||||
|
||||
Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }
|
||||
.onErrorResumeNext { Observable.empty() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
|
||||
Observable.fromCallable {
|
||||
mangaToDelete.forEach { manga ->
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
if (deleteChapters) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
if (source != null) {
|
||||
downloadManager.deleteManga(manga, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given list of manga to categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param mangas the list of manga to move.
|
||||
*/
|
||||
fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
|
||||
val mc = ArrayList<MangaCategory>()
|
||||
|
||||
for (manga in mangas) {
|
||||
for (cat in categories) {
|
||||
mc.add(MangaCategory.create(manga, cat))
|
||||
}
|
||||
}
|
||||
|
||||
db.setMangaCategories(mc, mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cover with local file.
|
||||
*
|
||||
* @param inputStream the new cover.
|
||||
* @param manga the manga edited.
|
||||
* @return true if the cover is updated, false otherwise
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
|
||||
if (manga.source == LocalSource.ID) {
|
||||
LocalSource.updateCover(context, manga, inputStream)
|
||||
return true
|
||||
}
|
||||
|
||||
if (manga.thumbnail_url != null && manga.favorite) {
|
||||
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
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.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.combineLatest
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.ArrayList
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
|
||||
/**
|
||||
* Class containing library information.
|
||||
*/
|
||||
private data class Library(val categories: List<Category>, val mangaMap: LibraryMap)
|
||||
|
||||
/**
|
||||
* Typealias for the library manga, using the category as keys, and list of manga as values.
|
||||
*/
|
||||
private typealias LibraryMap = Map<Int, List<LibraryItem>>
|
||||
|
||||
/**
|
||||
* Presenter of [LibraryController].
|
||||
*/
|
||||
class LibraryPresenter(
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
) : BasePresenter<LibraryController>() {
|
||||
|
||||
private val context = preferences.context
|
||||
|
||||
/**
|
||||
* Categories of the library.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI filters to the last emission of the library.
|
||||
*/
|
||||
private val filterTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI update to the last emission of the library.
|
||||
*/
|
||||
private val downloadTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Relay used to apply the selected sorting method to the last emission of the library.
|
||||
*/
|
||||
private val sortTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Library subscription.
|
||||
*/
|
||||
private var librarySubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
subscribeLibrary()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to library if needed.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
if (librarySubscription.isNullOrUnsubscribed()) {
|
||||
librarySubscription = getLibraryObservable()
|
||||
.combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, _ -> lib.apply { setDownloadCount(mangaMap) } })
|
||||
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) })
|
||||
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) })
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache({ view, (categories, mangaMap) ->
|
||||
view.onNextLibraryUpdate(categories, mangaMap)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library filters to the given map of manga.
|
||||
*
|
||||
* @param map the map to filter.
|
||||
*/
|
||||
private fun applyFilters(map: LibraryMap): LibraryMap {
|
||||
val filterDownloaded = preferences.filterDownloaded().getOrDefault()
|
||||
|
||||
val filterUnread = preferences.filterUnread().getOrDefault()
|
||||
|
||||
val filterCompleted = preferences.filterCompleted().getOrDefault()
|
||||
|
||||
val filterFn: (LibraryItem) -> Boolean = f@ { item ->
|
||||
// Filter when there isn't unread chapters.
|
||||
if (filterUnread && item.manga.unread == 0) {
|
||||
return@f false
|
||||
}
|
||||
|
||||
if (filterCompleted && item.manga.status != SManga.COMPLETED) {
|
||||
return@f false
|
||||
}
|
||||
|
||||
// Filter when there are no downloads.
|
||||
if (filterDownloaded) {
|
||||
// Local manga are always downloaded
|
||||
if (item.manga.source == LocalSource.ID) {
|
||||
return@f true
|
||||
}
|
||||
// Don't bother with directory checking if download count has been set.
|
||||
if (item.downloadCount != -1) {
|
||||
return@f item.downloadCount > 0
|
||||
}
|
||||
|
||||
return@f downloadManager.getDownloadCount(item.manga) > 0
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets downloaded chapter count to each manga.
|
||||
*
|
||||
* @param map the map of manga.
|
||||
*/
|
||||
private fun setDownloadCount(map: LibraryMap) {
|
||||
if (!preferences.downloadBadge().getOrDefault()) {
|
||||
// Unset download count if the preference is not enabled.
|
||||
for ((_, itemList) in map) {
|
||||
for (item in itemList) {
|
||||
item.downloadCount = -1
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for ((_, itemList) in map) {
|
||||
for (item in itemList) {
|
||||
item.downloadCount = downloadManager.getDownloadCount(item.manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library sorting to the given map of manga.
|
||||
*
|
||||
* @param map the map to sort.
|
||||
*/
|
||||
private fun applySort(map: LibraryMap): LibraryMap {
|
||||
val sortingMode = preferences.librarySortingMode().getOrDefault()
|
||||
|
||||
val lastReadManga by lazy {
|
||||
var counter = 0
|
||||
db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||
}
|
||||
val totalChapterManga by lazy {
|
||||
var counter = 0
|
||||
db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||
}
|
||||
|
||||
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||
when (sortingMode) {
|
||||
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
|
||||
LibrarySort.LAST_READ -> {
|
||||
// Get index of manga, set equal to list if size unknown.
|
||||
val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
|
||||
val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size
|
||||
manga1LastRead.compareTo(manga2LastRead)
|
||||
}
|
||||
LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update)
|
||||
LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread)
|
||||
LibrarySort.TOTAL -> {
|
||||
val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
|
||||
val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
|
||||
manga1TotalChapter.compareTo(mange2TotalChapter)
|
||||
}
|
||||
LibrarySort.SOURCE -> {
|
||||
val source1Name = sourceManager.getOrStub(i1.manga.source).name
|
||||
val source2Name = sourceManager.getOrStub(i2.manga.source).name
|
||||
source1Name.compareTo(source2Name)
|
||||
}
|
||||
else -> throw Exception("Unknown sorting mode")
|
||||
}
|
||||
}
|
||||
|
||||
val comparator = if (preferences.librarySortingAscending().getOrDefault())
|
||||
Comparator(sortFn)
|
||||
else
|
||||
Collections.reverseOrder(sortFn)
|
||||
|
||||
return map.mapValues { entry -> entry.value.sortedWith(comparator) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories and all its manga from the database.
|
||||
*
|
||||
* @return an observable of the categories and its manga.
|
||||
*/
|
||||
private fun getLibraryObservable(): Observable<Library> {
|
||||
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
|
||||
{ dbCategories, libraryManga ->
|
||||
val categories = if (libraryManga.containsKey(0))
|
||||
arrayListOf(Category.createDefault()) + dbCategories
|
||||
else
|
||||
dbCategories
|
||||
|
||||
this.categories = categories
|
||||
Library(categories, libraryManga)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories from the database.
|
||||
*
|
||||
* @return an observable of the categories.
|
||||
*/
|
||||
private fun getCategoriesObservable(): Observable<List<Category>> {
|
||||
return db.getCategories().asRxObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the manga grouped by categories.
|
||||
*
|
||||
* @return an observable containing a map with the category id as key and a list of manga as the
|
||||
* value.
|
||||
*/
|
||||
private fun getLibraryMangasObservable(): Observable<LibraryMap> {
|
||||
val libraryAsList = preferences.libraryAsList()
|
||||
return db.getLibraryMangas().asRxObservable()
|
||||
.map { list ->
|
||||
list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be filtered.
|
||||
*/
|
||||
fun requestFilterUpdate() {
|
||||
filterTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to have download badges added.
|
||||
*/
|
||||
fun requestDownloadBadgesUpdate() {
|
||||
downloadTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be sorted.
|
||||
*/
|
||||
fun requestSortUpdate() {
|
||||
sortTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is opened.
|
||||
*/
|
||||
fun onOpenManga() {
|
||||
// Avoid further db updates for the library when it's not needed
|
||||
librarySubscription?.let { remove(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common categories for the given list of manga.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
return mangas.toSet()
|
||||
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
||||
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the selected manga from the library.
|
||||
*
|
||||
* @param mangas the list of manga to delete.
|
||||
* @param deleteChapters whether to also delete downloaded chapters.
|
||||
*/
|
||||
fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
|
||||
// Create a set of the list
|
||||
val mangaToDelete = mangas.distinctBy { it.id }
|
||||
mangaToDelete.forEach { it.favorite = false }
|
||||
|
||||
Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }
|
||||
.onErrorResumeNext { Observable.empty() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
|
||||
Observable.fromCallable {
|
||||
mangaToDelete.forEach { manga ->
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
if (deleteChapters) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
if (source != null) {
|
||||
downloadManager.deleteManga(manga, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given list of manga to categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param mangas the list of manga to move.
|
||||
*/
|
||||
fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
|
||||
val mc = ArrayList<MangaCategory>()
|
||||
|
||||
for (manga in mangas) {
|
||||
for (cat in categories) {
|
||||
mc.add(MangaCategory.create(manga, cat))
|
||||
}
|
||||
}
|
||||
|
||||
db.setMangaCategories(mc, mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cover with local file.
|
||||
*
|
||||
* @param inputStream the new cover.
|
||||
* @param manga the manga edited.
|
||||
* @return true if the cover is updated, false otherwise
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
|
||||
if (manga.source == LocalSource.ID) {
|
||||
LocalSource.updateCover(context, manga, inputStream)
|
||||
return true
|
||||
}
|
||||
|
||||
if (manga.thumbnail_url != null && manga.favorite) {
|
||||
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
object LibrarySort {
|
||||
|
||||
const val ALPHA = 0
|
||||
const val LAST_READ = 1
|
||||
const val LAST_UPDATED = 2
|
||||
const val UNREAD = 3
|
||||
const val TOTAL = 4
|
||||
const val SOURCE = 5
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
object LibrarySort {
|
||||
|
||||
const val ALPHA = 0
|
||||
const val LAST_READ = 1
|
||||
const val LAST_UPDATED = 2
|
||||
const val UNREAD = 3
|
||||
const val TOTAL = 4
|
||||
const val SOURCE = 5
|
||||
}
|
||||
Reference in New Issue
Block a user