Rename catalogue classes/layouts -> source
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card.title
|
||||
|
||||
class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
fun bind(item: LangItem) {
|
||||
title.text = LocaleHelper.getDisplayName(item.code, itemView.context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
/**
|
||||
* Item that contains the language header.
|
||||
*
|
||||
* @param code The lang code.
|
||||
*/
|
||||
data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
|
||||
|
||||
/**
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_main_controller_card
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LangHolder {
|
||||
return LangHolder(view, adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds this item to the given view holder.
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: LangHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
holder.bind(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
/**
|
||||
* Adapter that holds the catalogue cards.
|
||||
*
|
||||
* @param controller instance of [SourceController].
|
||||
*/
|
||||
class SourceAdapter(val controller: SourceController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
val cardBackground = controller.activity!!.getResourceColor(R.attr.colorSurface)
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for browse item clicks.
|
||||
*/
|
||||
val browseClickListener: OnBrowseClickListener = controller
|
||||
|
||||
/**
|
||||
* Listener for latest item clicks.
|
||||
*/
|
||||
val latestClickListener: OnLatestClickListener = controller
|
||||
|
||||
/**
|
||||
* Listener which should be called when user clicks browse.
|
||||
* Note: Should only be handled by [SourceController]
|
||||
*/
|
||||
interface OnBrowseClickListener {
|
||||
fun onBrowseClick(position: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener which should be called when user clicks latest.
|
||||
* Note: Should only be handled by [SourceController]
|
||||
*/
|
||||
interface OnLatestClickListener {
|
||||
fun onLatestClick(position: Int)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.source.latest.LatestUpdatesController
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* This controller shows and manages the different catalogues enabled by the user.
|
||||
* This controller should only handle UI actions, IO actions should be done by [SourcePresenter]
|
||||
* [SourceAdapter.OnBrowseClickListener] call function data on browse item click.
|
||||
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
||||
*/
|
||||
class SourceController : NucleusController<SourcePresenter>(),
|
||||
RootController,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
SourceAdapter.OnBrowseClickListener,
|
||||
SourceAdapter.OnLatestClickListener {
|
||||
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
/**
|
||||
* Adapter containing sources.
|
||||
*/
|
||||
private var adapter: SourceAdapter? = null
|
||||
|
||||
private lateinit var binding: SourceMainControllerBinding
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.label_sources)
|
||||
}
|
||||
|
||||
override fun createPresenter(): SourcePresenter {
|
||||
return SourcePresenter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the view with [R.layout.source_main_controller].
|
||||
*
|
||||
* @param inflater used to load the layout xml.
|
||||
* @param container containing parent views.
|
||||
* @return inflated view.
|
||||
*/
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = SourceMainControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = SourceAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||
adapter?.fastScroller = binding.fastScroller
|
||||
|
||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
|
||||
presenter.updateSources()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||
val source = item.source
|
||||
openCatalogue(source, BrowseSourceController(source))
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||
|
||||
val isPinned = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false
|
||||
|
||||
MaterialDialog.Builder(activity)
|
||||
.title(item.source.name)
|
||||
.items(
|
||||
activity.getString(R.string.action_hide),
|
||||
activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin)
|
||||
)
|
||||
.itemsCallback { _, _, which, _ ->
|
||||
when (which) {
|
||||
0 -> hideCatalogue(item.source)
|
||||
1 -> pinCatalogue(item.source, isPinned)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun hideCatalogue(source: Source) {
|
||||
val current = preferences.hiddenCatalogues().getOrDefault()
|
||||
preferences.hiddenCatalogues().set(current + source.id.toString())
|
||||
|
||||
presenter.updateSources()
|
||||
}
|
||||
|
||||
private fun pinCatalogue(source: Source, isPinned: Boolean) {
|
||||
val current = preferences.pinnedCatalogues().getOrDefault()
|
||||
if (isPinned) {
|
||||
preferences.pinnedCatalogues().set(current - source.id.toString())
|
||||
} else {
|
||||
preferences.pinnedCatalogues().set(current + source.id.toString())
|
||||
}
|
||||
|
||||
presenter.updateSources()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when browse is clicked in [SourceAdapter]
|
||||
*/
|
||||
override fun onBrowseClick(position: Int) {
|
||||
onItemClick(view!!, position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when latest is clicked in [SourceAdapter]
|
||||
*/
|
||||
override fun onLatestClick(position: Int) {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||
openCatalogue(item.source, LatestUpdatesController(item.source))
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a catalogue with the given controller.
|
||||
*/
|
||||
private fun openCatalogue(source: CatalogueSource, controller: BrowseSourceController) {
|
||||
preferences.lastUsedCatalogueSource().set(source.id)
|
||||
router.pushController(controller.withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the options menu.
|
||||
*
|
||||
* @param menu menu containing options.
|
||||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// Inflate menu
|
||||
inflater.inflate(R.menu.source_main, menu)
|
||||
|
||||
// Initialize search option.
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
// Change hint to show global search.
|
||||
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
|
||||
|
||||
// Create query listener which opens the global search view.
|
||||
searchView.queryTextChangeEvents()
|
||||
.filter { it.isSubmitted }
|
||||
.subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) }
|
||||
}
|
||||
|
||||
fun performGlobalSearch(query: String) {
|
||||
router.pushController(GlobalSearchController(query).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an option menu item has been selected by the user.
|
||||
*
|
||||
* @param item The selected item.
|
||||
* @return True if this event has been consumed, false if it has not.
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
// Initialize option to open catalogue settings.
|
||||
R.id.action_settings -> {
|
||||
router.pushController((RouterTransaction.with(SettingsSourcesController()))
|
||||
.popChangeHandler(SettingsSourcesFadeChangeHandler())
|
||||
.pushChangeHandler(FadeChangeHandler()))
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update adapter containing sources.
|
||||
*/
|
||||
fun setSources(sources: List<IFlexible<*>>) {
|
||||
adapter?.updateDataSet(sources)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to set the last used catalogue at the top of the view.
|
||||
*/
|
||||
fun setLastUsedSource(item: SourceItem?) {
|
||||
adapter?.removeAllScrollableHeaders()
|
||||
if (item != null) {
|
||||
adapter?.addScrollableHeader(item)
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val divider: Drawable
|
||||
|
||||
init {
|
||||
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
|
||||
divider = a.getDrawable(0)!!
|
||||
a.recycle()
|
||||
}
|
||||
|
||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val childCount = parent.childCount
|
||||
for (i in 0 until childCount - 1) {
|
||||
val child = parent.getChildAt(i)
|
||||
val holder = parent.getChildViewHolder(child)
|
||||
if (holder is SourceHolder &&
|
||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
|
||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
||||
val top = child.bottom + params.bottomMargin
|
||||
val bottom = top + divider.intrinsicHeight
|
||||
val left = parent.paddingStart + holder.margin
|
||||
val right = parent.width - parent.paddingEnd - holder.margin
|
||||
|
||||
divider.setBounds(left, top, right, bottom)
|
||||
divider.draw(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.roundTextIcon
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import io.github.mthli.slice.Slice
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_browse
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
||||
|
||||
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter),
|
||||
SlicedHolder {
|
||||
|
||||
override val slice = Slice(card).apply {
|
||||
setColor(adapter.cardBackground)
|
||||
}
|
||||
|
||||
override val viewToSlice: View
|
||||
get() = card
|
||||
|
||||
init {
|
||||
source_browse.setOnClickListener {
|
||||
adapter.browseClickListener.onBrowseClick(adapterPosition)
|
||||
}
|
||||
|
||||
source_latest.setOnClickListener {
|
||||
adapter.latestClickListener.onLatestClick(adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: SourceItem) {
|
||||
val source = item.source
|
||||
setCardEdges(item)
|
||||
|
||||
// Set source name
|
||||
title.text = source.name
|
||||
|
||||
// Set circle letter image.
|
||||
itemView.post {
|
||||
val icon = source.icon()
|
||||
if (icon != null) image.setImageDrawable(icon)
|
||||
else image.roundTextIcon(source.name)
|
||||
}
|
||||
|
||||
source_browse.setText(R.string.browse)
|
||||
if (source.supportsLatest) {
|
||||
source_latest.visible()
|
||||
} else {
|
||||
source_latest.gone()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
|
||||
/**
|
||||
* Item that contains source information.
|
||||
*
|
||||
* @param source Instance of [CatalogueSource] containing source information.
|
||||
* @param header The header for this item.
|
||||
*/
|
||||
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) :
|
||||
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
||||
|
||||
/**
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_main_controller_card_item
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
||||
return SourceHolder(view, adapter as SourceAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds this item to the given view holder.
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: SourceHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
holder.bind(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import java.util.TreeMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [SourceController]
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*
|
||||
* @param sourceManager manages the different sources.
|
||||
* @param preferences application preferences.
|
||||
*/
|
||||
class SourcePresenter(
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<SourceController>() {
|
||||
|
||||
var sources = getEnabledSources()
|
||||
|
||||
/**
|
||||
* Subscription for retrieving enabled sources.
|
||||
*/
|
||||
private var sourceSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Load enabled and last used sources
|
||||
loadSources()
|
||||
loadLastUsedSource()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe and create a new subscription to fetch enabled sources.
|
||||
*/
|
||||
private fun loadSources() {
|
||||
sourceSubscription?.unsubscribe()
|
||||
|
||||
val pinnedSources = mutableListOf<SourceItem>()
|
||||
val pinnedCatalogues = preferences.pinnedCatalogues().getOrDefault()
|
||||
|
||||
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
|
||||
// Catalogues without a lang defined will be placed at the end
|
||||
when {
|
||||
d1 == "" && d2 != "" -> 1
|
||||
d2 == "" && d1 != "" -> -1
|
||||
else -> d1.compareTo(d2)
|
||||
}
|
||||
}
|
||||
val byLang = sources.groupByTo(map, { it.lang })
|
||||
var sourceItems = byLang.flatMap {
|
||||
val langItem = LangItem(it.key)
|
||||
it.value.map { source ->
|
||||
if (source.id.toString() in pinnedCatalogues) {
|
||||
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY)))
|
||||
}
|
||||
|
||||
SourceItem(source, langItem)
|
||||
}
|
||||
}
|
||||
|
||||
if (pinnedSources.isNotEmpty()) {
|
||||
sourceItems = pinnedSources + sourceItems
|
||||
}
|
||||
|
||||
sourceSubscription = Observable.just(sourceItems)
|
||||
.subscribeLatestCache(SourceController::setSources)
|
||||
}
|
||||
|
||||
private fun loadLastUsedSource() {
|
||||
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
|
||||
|
||||
// Emit the first item immediately but delay subsequent emissions by 500ms.
|
||||
Observable.merge(
|
||||
sharedObs.take(1),
|
||||
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
|
||||
.distinctUntilChanged()
|
||||
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
|
||||
.subscribeLatestCache(SourceController::setLastUsedSource)
|
||||
}
|
||||
|
||||
fun updateSources() {
|
||||
sources = getEnabledSources()
|
||||
loadSources()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of enabled sources ordered by language and name.
|
||||
*
|
||||
* @return list containing enabled sources.
|
||||
*/
|
||||
private fun getEnabledSources(): List<CatalogueSource> {
|
||||
val languages = preferences.enabledLanguages().getOrDefault()
|
||||
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" } +
|
||||
sourceManager.get(LocalSource.ID) as LocalSource
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PINNED_KEY = "pinned"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
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.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.inflate
|
||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||
import eu.kanade.tachiyomi.util.view.snack
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import eu.kanade.tachiyomi.widget.EmptyView
|
||||
import java.util.concurrent.TimeUnit
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Controller to manage the catalogues available in the app.
|
||||
*/
|
||||
open class BrowseSourceController(bundle: Bundle) :
|
||||
NucleusController<BrowseSourcePresenter>(bundle),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
ChangeMangaCategoriesDialog.Listener {
|
||||
|
||||
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||
putLong(SOURCE_ID_KEY, source.id)
|
||||
})
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Adapter containing the list of manga from the catalogue.
|
||||
*/
|
||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||
|
||||
/**
|
||||
* Snackbar containing an error message when a request fails.
|
||||
*/
|
||||
private var snack: Snackbar? = null
|
||||
|
||||
/**
|
||||
* Sheet containing filter items.
|
||||
*/
|
||||
private var filterSheet: SourceFilterSheet? = null
|
||||
|
||||
/**
|
||||
* Recycler view with the list of results.
|
||||
*/
|
||||
private var recycler: RecyclerView? = null
|
||||
|
||||
/**
|
||||
* Subscription for the search view.
|
||||
*/
|
||||
private var searchViewSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for the number of manga per row.
|
||||
*/
|
||||
private var numColumnsSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Endless loading item.
|
||||
*/
|
||||
private var progressItem: ProgressItem? = null
|
||||
|
||||
private lateinit var binding: SourceControllerBinding
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return presenter.source.name
|
||||
}
|
||||
|
||||
override fun createPresenter(): BrowseSourcePresenter {
|
||||
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = SourceControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Prepare filter sheet
|
||||
initFilterSheet()
|
||||
|
||||
// Initialize adapter, scroll listener and recycler views
|
||||
adapter = FlexibleAdapter(null, this)
|
||||
setupRecycler(view)
|
||||
|
||||
binding.progress.visible()
|
||||
}
|
||||
|
||||
open fun initFilterSheet() {
|
||||
if (presenter.sourceFilters.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
filterSheet = SourceFilterSheet(
|
||||
activity!!,
|
||||
onSearchClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
|
||||
},
|
||||
onResetClicked = {
|
||||
presenter.appliedFilters = FilterList()
|
||||
val newFilters = presenter.source.getFilterList()
|
||||
presenter.sourceFilters = newFilters
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
}
|
||||
)
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
|
||||
binding.fabFilter.setOnClickListener { filterSheet?.show() }
|
||||
binding.fabFilter.visible()
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
numColumnsSubscription = null
|
||||
searchViewSubscription?.unsubscribe()
|
||||
searchViewSubscription = null
|
||||
adapter = null
|
||||
snack = null
|
||||
recycler = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
private fun setupRecycler(view: View) {
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
|
||||
var oldPosition = RecyclerView.NO_POSITION
|
||||
val oldRecycler = binding.catalogueView.getChildAt(1)
|
||||
if (oldRecycler is RecyclerView) {
|
||||
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
oldRecycler.adapter = null
|
||||
|
||||
binding.catalogueView.removeView(oldRecycler)
|
||||
}
|
||||
|
||||
val recycler = if (presenter.isListMode) {
|
||||
RecyclerView(view.context).apply {
|
||||
id = R.id.recycler
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
}
|
||||
} else {
|
||||
(binding.catalogueView.inflate(R.layout.source_recycler_autofit) as AutofitRecyclerView).apply {
|
||||
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
|
||||
.doOnNext { spanCount = it }
|
||||
.skip(1)
|
||||
// Set again the adapter to recalculate the covers height
|
||||
.subscribe { adapter = this@BrowseSourceController.adapter }
|
||||
|
||||
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (adapter?.getItemViewType(position)) {
|
||||
R.layout.source_grid_item, null -> 1
|
||||
else -> spanCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterSheet != null) {
|
||||
// Add bottom padding if filter FAB is visible
|
||||
recycler.setPadding(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
view.resources.getDimensionPixelOffset(R.dimen.fab_list_padding)
|
||||
)
|
||||
recycler.clipToPadding = false
|
||||
|
||||
binding.fabFilter.shrinkOnScroll(recycler)
|
||||
}
|
||||
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
|
||||
binding.catalogueView.addView(recycler, 1)
|
||||
|
||||
if (oldPosition != RecyclerView.NO_POSITION) {
|
||||
recycler.layoutManager?.scrollToPosition(oldPosition)
|
||||
}
|
||||
this.recycler = recycler
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.source_browse, menu)
|
||||
|
||||
// Initialize search menu
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
val query = presenter.query
|
||||
if (!query.isBlank()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
}
|
||||
|
||||
val searchEventsObservable = searchView.queryTextChangeEvents()
|
||||
.skip(1)
|
||||
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
|
||||
.share()
|
||||
val writingObservable = searchEventsObservable
|
||||
.filter { !it.isSubmitted }
|
||||
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
val submitObservable = searchEventsObservable
|
||||
.filter { it.isSubmitted }
|
||||
|
||||
searchViewSubscription?.unsubscribe()
|
||||
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
|
||||
.map { it.queryText().toString() }
|
||||
.subscribeUntilDestroy { searchWithQuery(it) }
|
||||
|
||||
searchItem.fixExpand(
|
||||
onExpand = { invalidateMenuOnExpand() },
|
||||
onCollapse = {
|
||||
searchWithQuery("")
|
||||
true
|
||||
}
|
||||
)
|
||||
|
||||
// Show next display mode
|
||||
menu.findItem(R.id.action_display_mode).apply {
|
||||
val icon = if (presenter.isListMode)
|
||||
R.drawable.ic_view_module_white_24dp
|
||||
else
|
||||
R.drawable.ic_view_list_white_24dp
|
||||
setIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
|
||||
val isHttpSource = presenter.source is HttpSource
|
||||
menu.findItem(R.id.action_open_in_web_view).isVisible = isHttpSource
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_search -> expandActionViewFromInteraction = true
|
||||
R.id.action_display_mode -> swapDisplayMode()
|
||||
R.id.action_open_in_web_view -> openInWebView()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun openInWebView() {
|
||||
val source = presenter.source as? HttpSource ?: return
|
||||
|
||||
val activity = activity ?: return
|
||||
val intent = WebViewActivity.newIntent(activity, source.baseUrl, source.id, presenter.source.name)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the request with a new query.
|
||||
*
|
||||
* @param newQuery the new query.
|
||||
*/
|
||||
fun searchWithQuery(newQuery: String) {
|
||||
// If text didn't change, do nothing
|
||||
if (presenter.query == newQuery)
|
||||
return
|
||||
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
|
||||
presenter.restartPager(newQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the network request is received.
|
||||
*
|
||||
* @param page the current page.
|
||||
* @param mangas the list of manga of the page.
|
||||
*/
|
||||
fun onAddPage(page: Int, mangas: List<SourceItem>) {
|
||||
val adapter = adapter ?: return
|
||||
hideProgressBar()
|
||||
if (page == 1) {
|
||||
adapter.clear()
|
||||
resetProgressItem()
|
||||
}
|
||||
adapter.onLoadMoreComplete(mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the network request fails.
|
||||
*
|
||||
* @param error the error received.
|
||||
*/
|
||||
fun onAddPageError(error: Throwable) {
|
||||
Timber.e(error)
|
||||
val adapter = adapter ?: return
|
||||
adapter.onLoadMoreComplete(null)
|
||||
hideProgressBar()
|
||||
|
||||
snack?.dismiss()
|
||||
|
||||
val message = getErrorMessage(error)
|
||||
val retryAction = View.OnClickListener {
|
||||
|
||||
// If not the first page, show bottom progress bar.
|
||||
if (adapter.mainItemCount > 0 && progressItem != null) {
|
||||
adapter.addScrollableFooterWithDelay(progressItem!!, 0, true)
|
||||
} else {
|
||||
showProgressBar()
|
||||
}
|
||||
presenter.requestNext()
|
||||
}
|
||||
|
||||
if (adapter.isEmpty) {
|
||||
val actions = mutableListOf(EmptyView.Action(R.string.action_retry, retryAction))
|
||||
if (presenter.source is HttpSource) {
|
||||
val openInWebViewAction = View.OnClickListener {
|
||||
openInWebView()
|
||||
}
|
||||
actions += EmptyView.Action(R.string.action_open_in_web_view, openInWebViewAction)
|
||||
}
|
||||
|
||||
binding.emptyView.show(message, actions)
|
||||
} else {
|
||||
snack = binding.catalogueView.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
||||
setAction(R.string.action_retry, retryAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getErrorMessage(error: Throwable): String {
|
||||
if (error is NoResultsException) {
|
||||
return binding.catalogueView.context.getString(R.string.no_results_found)
|
||||
}
|
||||
|
||||
return when {
|
||||
error.message == null -> ""
|
||||
error.message!!.startsWith("HTTP error") -> "${error.message}: ${binding.catalogueView.context.getString(R.string.http_error_hint)}"
|
||||
else -> error.message!!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new progress item and reenables the scroll listener.
|
||||
*/
|
||||
private fun resetProgressItem() {
|
||||
progressItem = ProgressItem()
|
||||
adapter?.endlessTargetCount = 0
|
||||
adapter?.setEndlessScrollListener(this, progressItem!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the adapter when scrolled near the bottom.
|
||||
*/
|
||||
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
|
||||
if (presenter.hasNextPage()) {
|
||||
presenter.requestNext()
|
||||
} else {
|
||||
adapter?.onLoadMoreComplete(null)
|
||||
adapter?.endlessTargetCount = 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun noMoreLoad(newItemsSize: Int) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the manga initialized
|
||||
*/
|
||||
fun onMangaInitialized(manga: Manga) {
|
||||
getHolder(manga)?.setImage(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the current display mode.
|
||||
*/
|
||||
fun swapDisplayMode() {
|
||||
val view = view ?: return
|
||||
val adapter = adapter ?: return
|
||||
|
||||
presenter.swapDisplayMode()
|
||||
val isListMode = presenter.isListMode
|
||||
activity?.invalidateOptionsMenu()
|
||||
setupRecycler(view)
|
||||
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
|
||||
// Initialize mangas if going to grid view or if over wifi when going to list view
|
||||
val mangas = (0 until adapter.itemCount).mapNotNull {
|
||||
(adapter.getItem(it) as? SourceItem)?.manga
|
||||
}
|
||||
presenter.initializeMangas(mangas)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a preference for the number of manga per row based on the current orientation.
|
||||
*
|
||||
* @return the preference.
|
||||
*/
|
||||
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
||||
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
|
||||
preferences.portraitColumns()
|
||||
else
|
||||
preferences.landscapeColumns()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view holder for the given manga.
|
||||
*
|
||||
* @param manga the manga to find.
|
||||
* @return the holder of the manga or null if it's not bound.
|
||||
*/
|
||||
private fun getHolder(manga: Manga): SourceHolder? {
|
||||
val adapter = adapter ?: return null
|
||||
|
||||
adapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = adapter.getItem(holder.adapterPosition) as? SourceItem
|
||||
if (item != null && item.manga.id!! == manga.id!!) {
|
||||
return holder as SourceHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress bar.
|
||||
*/
|
||||
private fun showProgressBar() {
|
||||
binding.progress.visible()
|
||||
snack?.dismiss()
|
||||
snack = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides active progress bars.
|
||||
*/
|
||||
private fun hideProgressBar() {
|
||||
binding.emptyView.hide()
|
||||
binding.progress.gone()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(view: View, position: Int): Boolean {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||
router.pushController(MangaController(item.manga, true).withFadeTransaction())
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is long clicked.
|
||||
*
|
||||
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
|
||||
* in, the list consists of the default category plus the user's categories. The default category is preselected on
|
||||
* new manga, and on already favorited manga the manga's categories are preselected.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
||||
|
||||
if (manga.favorite) {
|
||||
MaterialDialog.Builder(activity)
|
||||
.items(activity.getString(R.string.remove_from_library))
|
||||
.itemsCallback { _, _, which, _ ->
|
||||
when (which) {
|
||||
0 -> {
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
activity.toast(activity.getString(R.string.manga_removed_library))
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
val categories = presenter.getCategories()
|
||||
val defaultCategoryId = preferences.defaultCategory()
|
||||
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||
|
||||
when {
|
||||
// Default category set
|
||||
defaultCategory != null -> {
|
||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
||||
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
activity.toast(activity.getString(R.string.manga_added_library))
|
||||
}
|
||||
|
||||
// Automatic 'Default' or no categories
|
||||
defaultCategoryId == 0 || categories.isEmpty() -> {
|
||||
presenter.moveMangaToCategory(manga, null)
|
||||
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
activity.toast(activity.getString(R.string.manga_added_library))
|
||||
}
|
||||
|
||||
// Choose a category
|
||||
else -> {
|
||||
val ids = presenter.getMangaCategoryIds(manga)
|
||||
val preselected = ids.mapNotNull { id ->
|
||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||
}.toTypedArray()
|
||||
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
.showDialog(router)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update manga to use selected categories.
|
||||
*
|
||||
* @param mangas The list of manga to move to categories.
|
||||
* @param categories The list of categories where manga will be placed.
|
||||
*/
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||
val manga = mangas.firstOrNull() ?: return
|
||||
|
||||
presenter.changeMangaFavorite(manga)
|
||||
presenter.updateMangaCategories(manga, categories)
|
||||
|
||||
val position = adapter?.currentItems?.indexOfFirst { it -> (it as SourceItem).manga.id == manga.id }
|
||||
if (position != null) {
|
||||
adapter?.notifyItemChanged(position)
|
||||
}
|
||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||
}
|
||||
|
||||
protected companion object {
|
||||
const val SOURCE_ID_KEY = "sourceId"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
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.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.source.filter.CheckboxItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.CheckboxSectionItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.GroupItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.HeaderItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.SelectItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.SelectSectionItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.SeparatorItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.SortGroup
|
||||
import eu.kanade.tachiyomi.ui.source.filter.SortItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.TextItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.TextSectionItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.TriStateItem
|
||||
import eu.kanade.tachiyomi.ui.source.filter.TriStateSectionItem
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [BrowseSourceController].
|
||||
*/
|
||||
open class BrowseSourcePresenter(
|
||||
sourceId: Long,
|
||||
sourceManager: SourceManager = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val prefs: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<BrowseSourceController>() {
|
||||
|
||||
/**
|
||||
* Selected source.
|
||||
*/
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = ""
|
||||
private set
|
||||
|
||||
/**
|
||||
* Modifiable list of filters.
|
||||
*/
|
||||
var sourceFilters = FilterList()
|
||||
set(value) {
|
||||
field = value
|
||||
filterItems = value.toItems()
|
||||
}
|
||||
|
||||
var filterItems: List<IFlexible<*>> = emptyList()
|
||||
|
||||
/**
|
||||
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
|
||||
*/
|
||||
var appliedFilters = FilterList()
|
||||
|
||||
/**
|
||||
* Pager containing a list of manga results.
|
||||
*/
|
||||
private lateinit var pager: Pager
|
||||
|
||||
/**
|
||||
* Subject that initializes a list of manga.
|
||||
*/
|
||||
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
|
||||
|
||||
/**
|
||||
* Whether the view is in list mode or not.
|
||||
*/
|
||||
var isListMode: Boolean = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subscription for the pager.
|
||||
*/
|
||||
private var pagerSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for one request from the pager.
|
||||
*/
|
||||
private var pageSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to initialize manga details.
|
||||
*/
|
||||
private var initializerSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
sourceFilters = source.getFilterList()
|
||||
|
||||
if (savedState != null) {
|
||||
query = savedState.getString(::query.name, "")
|
||||
}
|
||||
|
||||
add(prefs.catalogueAsList().asObservable()
|
||||
.subscribe { setDisplayMode(it) })
|
||||
|
||||
restartPager()
|
||||
}
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
state.putString(::query.name, query)
|
||||
super.onSave(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the pager for the active source with the provided query and filters.
|
||||
*
|
||||
* @param query the query.
|
||||
* @param filters the current state of the filters (for search mode).
|
||||
*/
|
||||
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
|
||||
this.query = query
|
||||
this.appliedFilters = filters
|
||||
|
||||
subscribeToMangaInitializer()
|
||||
|
||||
// Create a new pager.
|
||||
pager = createPager(query, filters)
|
||||
|
||||
val sourceId = source.id
|
||||
|
||||
val catalogueAsList = prefs.catalogueAsList()
|
||||
|
||||
// Prepare the pager.
|
||||
pagerSubscription?.let { remove(it) }
|
||||
pagerSubscription = pager.results()
|
||||
.observeOn(Schedulers.io())
|
||||
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
|
||||
.doOnNext { initializeMangas(it.second) }
|
||||
.map { it.first to it.second.map { SourceItem(it, catalogueAsList) } }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeReplay({ view, (page, mangas) ->
|
||||
view.onAddPage(page, mangas)
|
||||
}, { _, error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
|
||||
// Request first page.
|
||||
requestNext()
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the next page for the active pager.
|
||||
*/
|
||||
fun requestNext() {
|
||||
if (!hasNextPage()) return
|
||||
|
||||
pageSubscription?.let { remove(it) }
|
||||
pageSubscription = Observable.defer { pager.requestNext() }
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Nothing to do when onNext is emitted.
|
||||
}, BrowseSourceController::onAddPageError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the last fetched page has a next page.
|
||||
*/
|
||||
fun hasNextPage(): Boolean {
|
||||
return pager.hasNextPage
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display mode.
|
||||
*
|
||||
* @param asList whether the current mode is in list or not.
|
||||
*/
|
||||
private fun setDisplayMode(asList: Boolean) {
|
||||
isListMode = asList
|
||||
subscribeToMangaInitializer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||
*/
|
||||
private fun subscribeToMangaInitializer() {
|
||||
initializerSubscription?.let { remove(it) }
|
||||
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.thumbnail_url == null && !it.initialized }
|
||||
.concatMap { getMangaDetailsObservable(it) }
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ manga ->
|
||||
@Suppress("DEPRECATION")
|
||||
view?.onMangaInitialized(manga)
|
||||
}, { error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
.apply { add(this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||
* if the manga is not yet in the database.
|
||||
*
|
||||
* @param sManga the manga from the source.
|
||||
* @return a manga from the database.
|
||||
*/
|
||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||
if (localManga == null) {
|
||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||
newManga.copyFrom(sManga)
|
||||
val result = db.insertManga(newManga).executeAsBlocking()
|
||||
newManga.id = result.insertedId()
|
||||
localManga = newManga
|
||||
}
|
||||
return localManga
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a list of manga.
|
||||
*
|
||||
* @param mangas the list of manga to initialize.
|
||||
*/
|
||||
fun initializeMangas(mangas: List<Manga>) {
|
||||
mangaDetailSubject.onNext(mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of manga that initializes the given manga.
|
||||
*
|
||||
* @param manga the manga to initialize.
|
||||
* @return an observable of the manga to initialize
|
||||
*/
|
||||
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.flatMap { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
Observable.just(manga)
|
||||
}
|
||||
.onErrorResumeNext { Observable.just(manga) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes a manga from the library.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
fun changeMangaFavorite(manga: Manga) {
|
||||
manga.favorite = !manga.favorite
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active display mode.
|
||||
*/
|
||||
fun swapDisplayMode() {
|
||||
prefs.catalogueAsList().set(!isListMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter states for the current source.
|
||||
*
|
||||
* @param filters a list of active filters.
|
||||
*/
|
||||
fun setSourceFilter(filters: FilterList) {
|
||||
restartPager(filters = filters)
|
||||
}
|
||||
|
||||
open fun createPager(query: String, filters: FilterList): Pager {
|
||||
return SourcePager(source, query, filters)
|
||||
}
|
||||
|
||||
private fun FilterList.toItems(): List<IFlexible<*>> {
|
||||
return mapNotNull {
|
||||
when (it) {
|
||||
is Filter.Header -> HeaderItem(it)
|
||||
is Filter.Separator -> SeparatorItem(it)
|
||||
is Filter.CheckBox -> CheckboxItem(it)
|
||||
is Filter.TriState -> TriStateItem(it)
|
||||
is Filter.Text -> TextItem(it)
|
||||
is Filter.Select<*> -> SelectItem(it)
|
||||
is Filter.Group<*> -> {
|
||||
val group = GroupItem(it)
|
||||
val subItems = it.state.mapNotNull {
|
||||
when (it) {
|
||||
is Filter.CheckBox -> CheckboxSectionItem(it)
|
||||
is Filter.TriState -> TriStateSectionItem(it)
|
||||
is Filter.Text -> TextSectionItem(it)
|
||||
is Filter.Select<*> -> SelectSectionItem(it)
|
||||
else -> null
|
||||
} as? ISectionable<*, *>
|
||||
}
|
||||
subItems.forEach { it.header = group }
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
is Filter.Sort -> {
|
||||
val group = SortGroup(it)
|
||||
val subItems = it.values.map {
|
||||
SortItem(it, group)
|
||||
}
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user categories.
|
||||
*
|
||||
* @return List of categories, not including the default category
|
||||
*/
|
||||
fun getCategories(): List<Category> {
|
||||
return db.getCategories().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||
*
|
||||
* @param manga the manga to get categories from.
|
||||
* @return Array of category ids the manga is in, if none returns default id
|
||||
*/
|
||||
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
return categories.mapNotNull { it.id }.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||
db.setMangaCategories(mc, listOf(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to the category.
|
||||
*
|
||||
* @param category the selected category.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||
moveMangaToCategories(manga, listOfNotNull(category))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update manga to use selected categories.
|
||||
*
|
||||
* @param manga needed to change
|
||||
* @param selectedCategories selected categories
|
||||
*/
|
||||
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
|
||||
if (!manga.favorite) {
|
||||
changeMangaFavorite(manga)
|
||||
}
|
||||
|
||||
moveMangaToCategories(manga, selectedCategories)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
class NoResultsException : Exception()
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* A general pager for source requests (latest updates, popular, search)
|
||||
*/
|
||||
abstract class Pager(var currentPage: Int = 1) {
|
||||
|
||||
var hasNextPage = true
|
||||
private set
|
||||
|
||||
protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create()
|
||||
|
||||
fun results(): Observable<Pair<Int, List<SManga>>> {
|
||||
return results.asObservable()
|
||||
}
|
||||
|
||||
abstract fun requestNext(): Observable<MangasPage>
|
||||
|
||||
fun onPageReceived(mangasPage: MangasPage) {
|
||||
val page = currentPage
|
||||
currentPage++
|
||||
hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
|
||||
results.call(Pair(page, mangasPage.mangas))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
|
||||
class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
|
||||
|
||||
private var loadMore = true
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_progress_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>) {
|
||||
holder.progressBar.gone()
|
||||
holder.progressMessage.gone()
|
||||
|
||||
if (!adapter.isEndlessScrollEnabled) {
|
||||
loadMore = false
|
||||
}
|
||||
|
||||
if (loadMore) {
|
||||
holder.progressBar.visible()
|
||||
} else {
|
||||
holder.progressMessage.visible()
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return this === other
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
val progressBar: ProgressBar = view.findViewById(R.id.progress_bar)
|
||||
val progressMessage: TextView = view.findViewById(R.id.progress_message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.view.inflate
|
||||
import eu.kanade.tachiyomi.widget.SimpleNavigationView
|
||||
import kotlinx.android.synthetic.main.source_filter_sheet.view.reset_btn
|
||||
import kotlinx.android.synthetic.main.source_filter_sheet.view.search_btn
|
||||
|
||||
class SourceFilterSheet(
|
||||
activity: Activity,
|
||||
onSearchClicked: () -> Unit,
|
||||
onResetClicked: () -> Unit
|
||||
) : BottomSheetDialog(activity) {
|
||||
|
||||
private var filterNavView: FilterNavigationView
|
||||
|
||||
init {
|
||||
filterNavView = FilterNavigationView(activity)
|
||||
filterNavView.onSearchClicked = {
|
||||
onSearchClicked()
|
||||
this.dismiss()
|
||||
}
|
||||
filterNavView.onResetClicked = onResetClicked
|
||||
|
||||
setContentView(filterNavView)
|
||||
}
|
||||
|
||||
fun setFilters(items: List<IFlexible<*>>) {
|
||||
filterNavView.adapter.updateDataSet(items)
|
||||
}
|
||||
|
||||
class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
SimpleNavigationView(context, attrs) {
|
||||
|
||||
var onSearchClicked = {}
|
||||
var onResetClicked = {}
|
||||
|
||||
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
|
||||
.setDisplayHeadersAtStartUp(true)
|
||||
.setStickyHeaders(true)
|
||||
|
||||
init {
|
||||
recycler.adapter = adapter
|
||||
recycler.setHasFixedSize(true)
|
||||
val view = inflate(R.layout.source_filter_sheet)
|
||||
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
|
||||
addView(view)
|
||||
search_btn.setOnClickListener { onSearchClicked() }
|
||||
reset_btn.setOnClickListener { onResetClicked() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
import kotlinx.android.synthetic.main.source_grid_item.progress
|
||||
import kotlinx.android.synthetic.main.source_grid_item.thumbnail
|
||||
import kotlinx.android.synthetic.main.source_grid_item.title
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||
* All the elements from the layout file "item_source_grid" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @constructor creates a new catalogue holder.
|
||||
*/
|
||||
class SourceGridHolder(private val view: View, private val adapter: FlexibleAdapter<*>) :
|
||||
SourceHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
override fun onSetValues(manga: Manga) {
|
||||
// Set manga title
|
||||
title.text = manga.title
|
||||
|
||||
// Set alpha of thumbnail.
|
||||
thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||
|
||||
setImage(manga)
|
||||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
GlideApp.with(view.context).clear(thumbnail)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(thumbnail, progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
|
||||
/**
|
||||
* Generic class used to hold the displayed data of a manga in the catalogue.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
*/
|
||||
abstract class SourceHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
abstract fun onSetValues(manga: Manga)
|
||||
|
||||
/**
|
||||
* Updates the image for this holder. Useful to update the image when the manga is initialized
|
||||
* and the url is now known.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
abstract fun setImage(manga: Manga)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.source_grid_item.view.card
|
||||
import kotlinx.android.synthetic.main.source_grid_item.view.gradient
|
||||
|
||||
class SourceItem(val manga: Manga, private val catalogueAsList: Preference<Boolean>) :
|
||||
AbstractFlexibleItem<SourceHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return if (catalogueAsList.getOrDefault())
|
||||
R.layout.source_list_item
|
||||
else
|
||||
R.layout.source_grid_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||
): SourceHolder {
|
||||
val parent = adapter.recyclerView
|
||||
return if (parent is AutofitRecyclerView) {
|
||||
view.apply {
|
||||
card.layoutParams = FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, parent.itemWidth / 3 * 4)
|
||||
gradient.layoutParams = FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
|
||||
}
|
||||
SourceGridHolder(view, adapter)
|
||||
} else {
|
||||
SourceListHolder(view, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: SourceHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.onSetValues(manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is SourceItem) {
|
||||
return manga.id!! == other.manga.id!!
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return manga.id!!.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import kotlinx.android.synthetic.main.source_list_item.thumbnail
|
||||
import kotlinx.android.synthetic.main.source_list_item.title
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||
* All the elements from the layout file "item_catalogue_list" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @constructor creates a new catalogue holder.
|
||||
*/
|
||||
class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
|
||||
SourceHolder(view, adapter) {
|
||||
|
||||
private val favoriteColor = view.context.getResourceColor(android.R.attr.textColorHint)
|
||||
private val unfavoriteColor = view.context.getResourceColor(android.R.attr.textColorPrimary)
|
||||
|
||||
/**
|
||||
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
override fun onSetValues(manga: Manga) {
|
||||
title.text = manga.title
|
||||
title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
|
||||
|
||||
setImage(manga)
|
||||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
GlideApp.with(view.context).clear(thumbnail)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.circleCrop()
|
||||
.dontAnimate()
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.ui.source.browse
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
|
||||
open class SourcePager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() {
|
||||
|
||||
override fun requestNext(): Observable<MangasPage> {
|
||||
val page = currentPage
|
||||
|
||||
val observable = if (query.isBlank() && filters.isEmpty())
|
||||
source.fetchPopularManga(page)
|
||||
else
|
||||
source.fetchSearchManga(page, query, filters)
|
||||
|
||||
return observable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext {
|
||||
if (it.mangas.isNotEmpty()) {
|
||||
onPageReceived(it)
|
||||
} else {
|
||||
throw NoResultsException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<CheckboxItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_checkbox
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
val view = holder.check
|
||||
view.text = filter.name
|
||||
view.isChecked = filter.state
|
||||
holder.itemView.setOnClickListener {
|
||||
view.toggle()
|
||||
filter.state = view.isChecked
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as CheckboxItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.ExpandableViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||
|
||||
class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
|
||||
|
||||
init {
|
||||
isExpanded = false
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_group
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return 101
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.title.text = filter.name
|
||||
|
||||
holder.icon.setVectorCompat(if (isExpanded)
|
||||
R.drawable.ic_expand_more_white_24dp
|
||||
else
|
||||
R.drawable.ic_chevron_right_white_24dp)
|
||||
|
||||
holder.itemView.setOnClickListener(holder)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as GroupItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
|
||||
val title: TextView = itemView.findViewById(R.id.title)
|
||||
val icon: ImageView = itemView.findViewById(R.id.expand_icon)
|
||||
|
||||
override fun shouldNotifyParentOnClick(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.R
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Holder>() {
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.design_navigation_item_subheader
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
val view = holder.itemView as TextView
|
||||
view.text = filter.name
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as HeaderItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable<TriStateItem.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TriStateSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<TextItem.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TextSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISectionable<CheckboxItem.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as CheckboxSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISectionable<SelectItem.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SelectSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||
|
||||
open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<SelectItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_spinner
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.text.text = filter.name + ": "
|
||||
|
||||
val spinner = holder.spinner
|
||||
spinner.prompt = filter.name
|
||||
spinner.adapter = ArrayAdapter<Any>(holder.itemView.context,
|
||||
android.R.layout.simple_spinner_item, filter.values).apply {
|
||||
setDropDownViewResource(R.layout.common_spinner_item)
|
||||
}
|
||||
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos ->
|
||||
filter.state = pos
|
||||
}
|
||||
spinner.setSelection(filter.state)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SelectItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
|
||||
val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.R
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<SeparatorItem.Holder>() {
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.design_navigation_item_separator
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SeparatorItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||
|
||||
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
|
||||
|
||||
init {
|
||||
isExpanded = false
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_group
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return 100
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.title.text = filter.name
|
||||
|
||||
holder.icon.setVectorCompat(if (isExpanded)
|
||||
R.drawable.ic_expand_more_white_24dp
|
||||
else
|
||||
R.drawable.ic_chevron_right_white_24dp)
|
||||
|
||||
holder.itemView.setOnClickListener(holder)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SortGroup).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_checkedtext
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return 102
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
val view = holder.text
|
||||
view.text = name
|
||||
val filter = group.filter
|
||||
|
||||
val i = filter.values.indexOf(name)
|
||||
|
||||
fun getIcon() = when (filter.state) {
|
||||
Filter.Sort.Selection(i, false) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_arrow_down_white_32dp, null)
|
||||
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
|
||||
Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_arrow_up_white_32dp, null)
|
||||
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
|
||||
else -> ContextCompat.getDrawable(view.context, R.drawable.empty_drawable_32dp)
|
||||
}
|
||||
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
|
||||
holder.itemView.setOnClickListener {
|
||||
val pre = filter.state?.index ?: i
|
||||
if (pre != i) {
|
||||
filter.state = Filter.Sort.Selection(i, false)
|
||||
} else {
|
||||
filter.state = Filter.Sort.Selection(i, filter.state?.ascending == false)
|
||||
}
|
||||
|
||||
group.subItems.forEach { adapter.notifyItemChanged(adapter.getGlobalPositionOf(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as SortItem
|
||||
return name == other.name && group == other.group
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + group.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
val text: CheckedTextView = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||
|
||||
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_text
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.wrapper.hint = filter.name
|
||||
holder.edit.setText(filter.state)
|
||||
holder.edit.addTextChangedListener(object : SimpleTextWatcher() {
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
filter.state = s.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TextItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper)
|
||||
val edit: EditText = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package eu.kanade.tachiyomi.ui.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
import com.google.android.material.R
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R as TR
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriStateItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return TR.layout.navigation_view_checkedtext
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return 103
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
val view = holder.text
|
||||
view.text = filter.name
|
||||
|
||||
fun getIcon() = VectorDrawableCompat.create(view.resources, when (filter.state) {
|
||||
Filter.TriState.STATE_IGNORE -> TR.drawable.ic_check_box_outline_blank_24dp
|
||||
Filter.TriState.STATE_INCLUDE -> TR.drawable.ic_check_box_24dp
|
||||
Filter.TriState.STATE_EXCLUDE -> TR.drawable.ic_check_box_x_24dp
|
||||
else -> throw Exception("Unknown state")
|
||||
}, null)?.apply {
|
||||
val color = if (filter.state == Filter.TriState.STATE_INCLUDE)
|
||||
R.attr.colorAccent
|
||||
else
|
||||
android.R.attr.textColorSecondary
|
||||
|
||||
setTint(view.context.getResourceColor(color))
|
||||
}
|
||||
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
|
||||
holder.itemView.setOnClickListener {
|
||||
filter.state = (filter.state + 1) % 3
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TriStateItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item)
|
||||
|
||||
init {
|
||||
// Align with native checkbox
|
||||
text.setPadding(4.dpToPx, 0, 0, 0)
|
||||
text.compoundDrawablePadding = 20.dpToPx
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package eu.kanade.tachiyomi.ui.source.global_search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseArray
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
|
||||
/**
|
||||
* Adapter that holds the search cards.
|
||||
*
|
||||
* @param controller instance of [GlobalSearchController].
|
||||
*/
|
||||
class GlobalSearchAdapter(val controller: GlobalSearchController) :
|
||||
FlexibleAdapter<GlobalSearchItem>(null, controller, true) {
|
||||
|
||||
/**
|
||||
* Bundle where the view state of the holders is saved.
|
||||
*/
|
||||
private var bundle = Bundle()
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
|
||||
super.onBindViewHolder(holder, position, payloads)
|
||||
restoreHolderState(holder)
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
saveHolderState(holder, bundle)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
val holdersBundle = Bundle()
|
||||
allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) }
|
||||
outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the view state of the given holder.
|
||||
*
|
||||
* @param holder The holder to save.
|
||||
* @param outState The bundle where the state is saved.
|
||||
*/
|
||||
private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
|
||||
val key = "holder_${holder.adapterPosition}"
|
||||
val holderState = SparseArray<Parcelable>()
|
||||
holder.itemView.saveHierarchyState(holderState)
|
||||
outState.putSparseParcelableArray(key, holderState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the view state of the given holder.
|
||||
*
|
||||
* @param holder The holder to restore.
|
||||
*/
|
||||
private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
|
||||
val key = "holder_${holder.adapterPosition}"
|
||||
val holderState = bundle.getSparseParcelableArray<Parcelable>(key)
|
||||
if (holderState != null) {
|
||||
holder.itemView.restoreHierarchyState(holderState)
|
||||
bundle.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val HOLDER_BUNDLE_KEY = "holder_bundle"
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.ui.source.global_search
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
/**
|
||||
* Adapter that holds the manga items from search results.
|
||||
*
|
||||
* @param controller instance of [GlobalSearchController].
|
||||
*/
|
||||
class GlobalSearchCardAdapter(controller: GlobalSearchController) :
|
||||
FlexibleAdapter<GlobalSearchCardItem>(null, controller, true) {
|
||||
|
||||
/**
|
||||
* Listen for browse item clicks.
|
||||
*/
|
||||
val mangaClickListener: OnMangaClickListener = controller
|
||||
|
||||
/**
|
||||
* Listener which should be called when user clicks browse.
|
||||
* Note: Should only be handled by [GlobalSearchController]
|
||||
*/
|
||||
interface OnMangaClickListener {
|
||||
fun onMangaClick(manga: Manga)
|
||||
fun onMangaLongClick(manga: Manga)
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package eu.kanade.tachiyomi.ui.source.global_search
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
import kotlinx.android.synthetic.main.global_search_controller_card_item.itemImage
|
||||
import kotlinx.android.synthetic.main.global_search_controller_card_item.progress
|
||||
import kotlinx.android.synthetic.main.global_search_controller_card_item.tvTitle
|
||||
|
||||
class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
init {
|
||||
// Call onMangaClickListener when item is pressed.
|
||||
itemView.setOnClickListener {
|
||||
val item = adapter.getItem(adapterPosition)
|
||||
if (item != null) {
|
||||
adapter.mangaClickListener.onMangaClick(item.manga)
|
||||
}
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
val item = adapter.getItem(adapterPosition)
|
||||
if (item != null) {
|
||||
adapter.mangaClickListener.onMangaLongClick(item.manga)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(manga: Manga) {
|
||||
tvTitle.text = manga.title
|
||||
// Set alpha of thumbnail.
|
||||
itemImage.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||
|
||||
setImage(manga)
|
||||
}
|
||||
|
||||
fun setImage(manga: Manga) {
|
||||
GlideApp.with(itemView.context).clear(itemImage)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(itemView.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(itemImage, progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.ui.source.global_search
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
class GlobalSearchCardItem(val manga: Manga) : AbstractFlexibleItem<GlobalSearchCardHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.global_search_controller_card_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): GlobalSearchCardHolder {
|
||||
return GlobalSearchCardHolder(view, adapter as GlobalSearchCardAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: GlobalSearchCardHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is GlobalSearchCardItem) {
|
||||
return manga.id == other.manga.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return manga.id?.toInt() ?: 0
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package eu.kanade.tachiyomi.ui.source.global_search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
|
||||
/**
|
||||
* This controller shows and manages the different search result in global search.
|
||||
* This controller should only handle UI actions, IO actions should be done by [GlobalSearchPresenter]
|
||||
* [GlobalSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class GlobalSearchController(
|
||||
protected val initialQuery: String? = null,
|
||||
protected val extensionFilter: String? = null
|
||||
) : NucleusController<GlobalSearchPresenter>(),
|
||||
GlobalSearchCardAdapter.OnMangaClickListener {
|
||||
|
||||
/**
|
||||
* Adapter containing search results grouped by lang.
|
||||
*/
|
||||
protected var adapter: GlobalSearchAdapter? = null
|
||||
|
||||
private lateinit var binding: GlobalSearchControllerBinding
|
||||
|
||||
/**
|
||||
* Called when controller is initialized.
|
||||
*/
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the view with [R.layout.global_search_controller].
|
||||
*
|
||||
* @param inflater used to load the layout xml.
|
||||
* @param container containing parent views.
|
||||
* @return inflated view
|
||||
*/
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = GlobalSearchControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of controller.
|
||||
*
|
||||
* @return title.
|
||||
*/
|
||||
override fun getTitle(): String? {
|
||||
return presenter.query
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [GlobalSearchPresenter] used in controller.
|
||||
*
|
||||
* @return instance of [GlobalSearchPresenter]
|
||||
*/
|
||||
override fun createPresenter(): GlobalSearchPresenter {
|
||||
return GlobalSearchPresenter(initialQuery, extensionFilter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when manga in global search is clicked, opens manga.
|
||||
*
|
||||
* @param manga clicked item containing manga information.
|
||||
*/
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
// Open MangaController.
|
||||
router.pushController(MangaController(manga, true).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when manga in global search is long clicked.
|
||||
*
|
||||
* @param manga clicked item containing manga information.
|
||||
*/
|
||||
override fun onMangaLongClick(manga: Manga) {
|
||||
// Delegate to single click by default.
|
||||
onMangaClick(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the options menu.
|
||||
*
|
||||
* @param menu menu containing options.
|
||||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// Inflate menu.
|
||||
inflater.inflate(R.menu.global_search, menu)
|
||||
|
||||
// Initialize search menu
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
searchView.onActionViewExpanded() // Required to show the query in the view
|
||||
searchView.setQuery(presenter.query, false)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
searchView.queryTextChangeEvents()
|
||||
.filter { it.isSubmitted }
|
||||
.subscribeUntilDestroy {
|
||||
presenter.search(it.queryText().toString())
|
||||
searchItem.collapseActionView()
|
||||
setTitle() // Update toolbar title
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
*
|
||||
* @param view view of controller
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = GlobalSearchAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onSaveViewState(view: View, outState: Bundle) {
|
||||
super.onSaveViewState(view, outState)
|
||||
adapter?.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
|
||||
super.onRestoreViewState(view, savedViewState)
|
||||
adapter?.onRestoreInstanceState(savedViewState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view holder for the given manga.
|
||||
*
|
||||
* @param source used to find holder containing source
|
||||
* @return the holder of the manga or null if it's not bound.
|
||||
*/
|
||||
private fun getHolder(source: CatalogueSource): GlobalSearchHolder? {
|
||||
val adapter = adapter ?: return null
|
||||
|
||||
adapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = adapter.getItem(holder.adapterPosition)
|
||||
if (item != null && source.id == item.source.id) {
|
||||
return holder as GlobalSearchHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Add search result to adapter.
|
||||
*
|
||||
* @param searchResult result of search.
|
||||
*/
|
||||
fun setItems(searchResult: List<GlobalSearchItem>) {
|
||||
adapter?.updateDataSet(searchResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the initialized manga.
|
||||
*/
|
||||
fun onMangaInitialized(source: CatalogueSource, manga: Manga) {
|
||||
getHolder(source)?.setImage(manga)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package eu.kanade.tachiyomi.ui.source.global_search
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import kotlinx.android.synthetic.main.global_search_controller_card.progress
|
||||
import kotlinx.android.synthetic.main.global_search_controller_card.recycler
|
||||
import kotlinx.android.synthetic.main.global_search_controller_card.source_card
|
||||
import kotlinx.android.synthetic.main.global_search_controller_card.title
|
||||
|
||||
/**
|
||||
* Holder that binds the [GlobalSearchItem] containing catalogue cards.
|
||||
*
|
||||
* @param view view of [GlobalSearchItem]
|
||||
* @param adapter instance of [GlobalSearchAdapter]
|
||||
*/
|
||||
class GlobalSearchHolder(view: View, val adapter: GlobalSearchAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Adapter containing manga from search results.
|
||||
*/
|
||||
private val mangaAdapter = GlobalSearchCardAdapter(adapter.controller)
|
||||
|
||||
private var lastBoundResults: List<GlobalSearchCardItem>? = null
|
||||
|
||||
init {
|
||||
// Set layout horizontal.
|
||||
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
recycler.adapter = mangaAdapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the loading of source search result.
|
||||
*
|
||||
* @param item item of card.
|
||||
*/
|
||||
fun bind(item: GlobalSearchItem) {
|
||||
val source = item.source
|
||||
val results = item.results
|
||||
|
||||
val titlePrefix = if (item.highlighted) "▶" else ""
|
||||
val langSuffix = if (source.lang.isNotEmpty()) " (${source.lang})" else ""
|
||||
|
||||
// Set Title with country code if available.
|
||||
title.text = titlePrefix + source.name + langSuffix
|
||||
|
||||
when {
|
||||
results == null -> {
|
||||
progress.visible()
|
||||
showHolder()
|
||||
}
|
||||
results.isEmpty() -> {
|
||||
progress.gone()
|
||||
hideHolder()
|
||||
}
|
||||
else -> {
|
||||
progress.gone()
|
||||
showHolder()
|
||||
}
|
||||
}
|
||||
if (results !== lastBoundResults) {
|
||||
mangaAdapter.updateDataSet(results)
|
||||
lastBoundResults = results
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the initialized manga.
|
||||
*/
|
||||
fun setImage(manga: Manga) {
|
||||
getHolder(manga)?.setImage(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view holder for the given manga.
|
||||
*
|
||||
* @param manga the manga to find.
|
||||
* @return the holder of the manga or null if it's not bound.
|
||||
*/
|
||||
private fun getHolder(manga: Manga): GlobalSearchCardHolder? {
|
||||
mangaAdapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = mangaAdapter.getItem(holder.adapterPosition)
|
||||
if (item != null && item.manga.id!! == manga.id!!) {
|
||||
return holder as GlobalSearchCardHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun showHolder() {
|
||||
title.visible()
|
||||
source_card.visible()
|
||||
}
|
||||
|
||||
private fun hideHolder() {
|
||||
title.gone()
|
||||
source_card.gone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package eu.kanade.tachiyomi.ui.source.global_search
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
|
||||
/**
|
||||
* Item that contains search result information.
|
||||
*
|
||||
* @param source the source for the search results.
|
||||
* @param results the search results.
|
||||
* @param highlighted whether this search item should be highlighted/marked in the catalogue search view.
|
||||
*/
|
||||
class GlobalSearchItem(val source: CatalogueSource, val results: List<GlobalSearchCardItem>?, val highlighted: Boolean = false) :
|
||||
AbstractFlexibleItem<GlobalSearchHolder>() {
|
||||
|
||||
/**
|
||||
* Set view.
|
||||
*
|
||||
* @return id of view
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.global_search_controller_card
|
||||
}
|
||||
|
||||
/**
|
||||
* Create view holder (see [GlobalSearchAdapter].
|
||||
*
|
||||
* @return holder of view.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): GlobalSearchHolder {
|
||||
return GlobalSearchHolder(view, adapter as GlobalSearchAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind item to view.
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: GlobalSearchHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if two items are equal.
|
||||
*
|
||||
* @return items are equal?
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is GlobalSearchItem) {
|
||||
return source.id == other.source.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return hash code of item.
|
||||
*
|
||||
* @return hashcode
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
return source.id.toInt()
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
package eu.kanade.tachiyomi.ui.source.global_search
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourcePresenter
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Presenter of [GlobalSearchController]
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*
|
||||
* @param sourceManager manages the different sources.
|
||||
* @param db manages the database calls.
|
||||
* @param preferences manages the preference calls.
|
||||
*/
|
||||
open class GlobalSearchPresenter(
|
||||
val initialQuery: String? = "",
|
||||
val initialExtensionFilter: String? = null,
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<GlobalSearchController>() {
|
||||
|
||||
/**
|
||||
* Enabled sources.
|
||||
*/
|
||||
val sources by lazy { getSourcesToQuery() }
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = ""
|
||||
private set
|
||||
|
||||
/**
|
||||
* Fetches the different sources by user settings.
|
||||
*/
|
||||
private var fetchSourcesSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subject which fetches image of given manga.
|
||||
*/
|
||||
private val fetchImageSubject = PublishSubject.create<Pair<List<Manga>, Source>>()
|
||||
|
||||
/**
|
||||
* Subscription for fetching images of manga.
|
||||
*/
|
||||
private var fetchImageSubscription: Subscription? = null
|
||||
|
||||
private val extensionManager by injectLazy<ExtensionManager>()
|
||||
|
||||
private var extensionFilter: String? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
extensionFilter = savedState?.getString(GlobalSearchPresenter::extensionFilter.name)
|
||||
?: initialExtensionFilter
|
||||
|
||||
// Perform a search with previous or initial state
|
||||
search(savedState?.getString(BrowseSourcePresenter::query.name)
|
||||
?: initialQuery.orEmpty())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchImageSubscription?.unsubscribe()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
state.putString(BrowseSourcePresenter::query.name, query)
|
||||
state.putString(GlobalSearchPresenter::extensionFilter.name, extensionFilter)
|
||||
super.onSave(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of enabled sources ordered by language and name, with pinned catalogues
|
||||
* prioritized.
|
||||
*
|
||||
* @return list containing enabled sources.
|
||||
*/
|
||||
protected open fun getEnabledSources(): List<CatalogueSource> {
|
||||
val languages = preferences.enabledLanguages().getOrDefault()
|
||||
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
|
||||
val pinnedCatalogues = preferences.pinnedCatalogues().getOrDefault()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||
.sortedWith(compareBy({ it.id.toString() !in pinnedCatalogues }, { "(${it.lang}) ${it.name}" }))
|
||||
}
|
||||
|
||||
private fun getSourcesToQuery(): List<CatalogueSource> {
|
||||
val filter = extensionFilter
|
||||
val enabledSources = getEnabledSources()
|
||||
if (filter.isNullOrEmpty()) {
|
||||
return enabledSources
|
||||
}
|
||||
|
||||
val filterSources = extensionManager.installedExtensions
|
||||
.filter { it.pkgName == filter }
|
||||
.flatMap { it.sources }
|
||||
.filter { it in enabledSources }
|
||||
.filterIsInstance<CatalogueSource>()
|
||||
|
||||
if (filterSources.isEmpty()) {
|
||||
return enabledSources
|
||||
}
|
||||
|
||||
return filterSources
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a catalogue search item
|
||||
*/
|
||||
protected open fun createCatalogueSearchItem(source: CatalogueSource, results: List<GlobalSearchCardItem>?): GlobalSearchItem {
|
||||
return GlobalSearchItem(source, results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a search for manga per catalogue.
|
||||
*
|
||||
* @param query query on which to search.
|
||||
*/
|
||||
fun search(query: String) {
|
||||
// Return if there's nothing to do
|
||||
if (this.query == query) return
|
||||
|
||||
// Update query
|
||||
this.query = query
|
||||
|
||||
// Create image fetch subscription
|
||||
initializeFetchImageSubscription()
|
||||
|
||||
// Create items with the initial state
|
||||
val initialItems = sources.map { createCatalogueSearchItem(it, null) }
|
||||
var items = initialItems
|
||||
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchSourcesSubscription = Observable.from(sources)
|
||||
.flatMap({ source ->
|
||||
Observable.defer { source.fetchSearchManga(1, query, FilterList()) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
|
||||
.map { it.mangas.take(10) } // Get at most 10 manga from search result.
|
||||
.map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
||||
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
||||
.map { createCatalogueSearchItem(source, it.map { GlobalSearchCardItem(it) }) }
|
||||
}, 5)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Update matching source with the obtained results
|
||||
.map { result ->
|
||||
items.map { item -> if (item.source == result.source) result else item }
|
||||
}
|
||||
// Update current state
|
||||
.doOnNext { items = it }
|
||||
// Deliver initial state
|
||||
.startWith(initialItems)
|
||||
.subscribeLatestCache({ view, manga ->
|
||||
view.setItems(manga)
|
||||
}, { _, error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a list of manga.
|
||||
*
|
||||
* @param manga the list of manga to initialize.
|
||||
*/
|
||||
private fun fetchImage(manga: List<Manga>, source: Source) {
|
||||
fetchImageSubject.onNext(Pair(manga, source))
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||
*/
|
||||
private fun initializeFetchImageSubscription() {
|
||||
fetchImageSubscription?.unsubscribe()
|
||||
fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io())
|
||||
.flatMap {
|
||||
val source = it.second
|
||||
Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized }
|
||||
.map { Pair(it, source) }
|
||||
.concatMap { getMangaDetailsObservable(it.first, it.second) }
|
||||
.map { Pair(source as CatalogueSource, it) }
|
||||
}
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ (source, manga) ->
|
||||
@Suppress("DEPRECATION")
|
||||
view?.onMangaInitialized(source, manga)
|
||||
}, { error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of manga that initializes the given manga.
|
||||
*
|
||||
* @param manga the manga to initialize.
|
||||
* @return an observable of the manga to initialize
|
||||
*/
|
||||
private fun getMangaDetailsObservable(manga: Manga, source: Source): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.flatMap { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
Observable.just(manga)
|
||||
}
|
||||
.onErrorResumeNext { Observable.just(manga) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||
* if the manga is not yet in the database.
|
||||
*
|
||||
* @param sManga the manga from the source.
|
||||
* @return a manga from the database.
|
||||
*/
|
||||
protected open fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||
if (localManga == null) {
|
||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||
newManga.copyFrom(sManga)
|
||||
val result = db.insertManga(newManga).executeAsBlocking()
|
||||
newManga.id = result.insertedId()
|
||||
localManga = newManga
|
||||
}
|
||||
return localManga
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.ui.source.latest
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourcePresenter
|
||||
|
||||
/**
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
|
||||
*/
|
||||
class LatestUpdatesController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||
|
||||
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||
putLong(SOURCE_ID_KEY, source.id)
|
||||
})
|
||||
|
||||
override fun createPresenter(): BrowseSourcePresenter {
|
||||
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.action_search).isVisible = false
|
||||
}
|
||||
|
||||
override fun initFilterSheet() {
|
||||
// No-op: we don't allow filtering in latest
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.ui.source.latest
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.ui.source.browse.Pager
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
|
||||
/**
|
||||
* LatestUpdatesPager inherited from the general Pager.
|
||||
*/
|
||||
class LatestUpdatesPager(val source: CatalogueSource) : Pager() {
|
||||
|
||||
override fun requestNext(): Observable<MangasPage> {
|
||||
return source.fetchLatestUpdates(currentPage)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { onPageReceived(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package eu.kanade.tachiyomi.ui.source.latest
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.source.browse.Pager
|
||||
|
||||
/**
|
||||
* Presenter of [LatestUpdatesController]. Inherit BrowseCataloguePresenter.
|
||||
*/
|
||||
class LatestUpdatesPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): Pager {
|
||||
return LatestUpdatesPager(source)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user