Add Latest tab to show you up to 5 sources showing you their latest manga
This commit is contained in:
parent
b745a74e1f
commit
e30694c12c
@ -256,4 +256,8 @@ object PreferenceKeys {
|
||||
const val eh_ehentai_quality = "ehentai_quality"
|
||||
|
||||
const val eh_enable_hah = "eh_enable_hah"
|
||||
|
||||
const val latest_tab_sources = "latest_tab_sources"
|
||||
|
||||
const val latest_tab_position = "latest_tab_position"
|
||||
}
|
||||
|
@ -356,4 +356,8 @@ class PreferencesHelper(val context: Context) {
|
||||
fun eh_settingsLanguages() = flowPrefs.getString(Keys.eh_settings_languages, "false*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false")
|
||||
|
||||
fun eh_EnabledCategories() = flowPrefs.getString(Keys.eh_enabled_categories, "false,false,false,false,false,false,false,false,false,false")
|
||||
|
||||
fun latestTabSources() = flowPrefs.getStringSet(Keys.latest_tab_sources, mutableSetOf())
|
||||
|
||||
fun latestTabInFront() = flowPrefs.getBoolean(Keys.latest_tab_position, false)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController
|
||||
import eu.kanade.tachiyomi.ui.browse.latest.LatestController
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
||||
@ -100,7 +101,7 @@ class BrowseController :
|
||||
activity?.tabs?.apply {
|
||||
val updates = preferences.extensionUpdatesCount().get()
|
||||
if (updates > 0) {
|
||||
val badge: BadgeDrawable? = getTabAt(1)?.orCreateBadge
|
||||
val badge: BadgeDrawable? = getTabAt(EXTENSIONS_CONTROLLER)?.orCreateBadge
|
||||
badge?.isVisible = true
|
||||
} else {
|
||||
getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
|
||||
@ -110,11 +111,24 @@ class BrowseController :
|
||||
|
||||
private inner class BrowseAdapter : RouterPagerAdapter(this@BrowseController) {
|
||||
|
||||
private val tabTitles = listOf(
|
||||
R.string.label_sources,
|
||||
R.string.label_extensions,
|
||||
R.string.label_migration
|
||||
)
|
||||
private val tabTitles = (
|
||||
if (preferences.latestTabInFront().get()) {
|
||||
listOf(
|
||||
R.string.latest,
|
||||
R.string.label_sources,
|
||||
R.string.label_extensions,
|
||||
R.string.label_migration
|
||||
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
R.string.label_sources,
|
||||
R.string.latest,
|
||||
R.string.label_extensions,
|
||||
R.string.label_migration
|
||||
)
|
||||
}
|
||||
)
|
||||
.map { resources!!.getString(it) }
|
||||
|
||||
override fun getCount(): Int {
|
||||
@ -124,7 +138,8 @@ class BrowseController :
|
||||
override fun configureRouter(router: Router, position: Int) {
|
||||
if (!router.hasRootController()) {
|
||||
val controller: Controller = when (position) {
|
||||
SOURCES_CONTROLLER -> SourceController()
|
||||
SOURCES_CONTROLLER -> if (preferences.latestTabInFront().get()) LatestController() else SourceController()
|
||||
LATEST_CONTROLLER -> if (!preferences.latestTabInFront().get()) LatestController() else SourceController()
|
||||
EXTENSIONS_CONTROLLER -> ExtensionController()
|
||||
MIGRATION_CONTROLLER -> MigrationSourcesController()
|
||||
else -> error("Wrong position $position")
|
||||
@ -142,7 +157,8 @@ class BrowseController :
|
||||
const val TO_EXTENSIONS_EXTRA = "to_extensions"
|
||||
|
||||
const val SOURCES_CONTROLLER = 0
|
||||
const val EXTENSIONS_CONTROLLER = 1
|
||||
const val MIGRATION_CONTROLLER = 2
|
||||
const val LATEST_CONTROLLER = 1
|
||||
const val EXTENSIONS_CONTROLLER = 2
|
||||
const val MIGRATION_CONTROLLER = 3
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,83 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseArray
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.latest.LatestController
|
||||
import eu.kanade.tachiyomi.ui.browse.latest.LatestItem
|
||||
|
||||
/**
|
||||
* Adapter that holds the search cards.
|
||||
*
|
||||
* @param controller instance of [LatestController].
|
||||
*/
|
||||
class LatestAdapter(val controller: LatestController) :
|
||||
FlexibleAdapter<LatestItem>(null, controller, true) {
|
||||
|
||||
val titleClickListener: OnTitleClickListener = controller
|
||||
|
||||
/**
|
||||
* 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.bindingAdapterPosition}"
|
||||
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.bindingAdapterPosition}"
|
||||
val holderState = bundle.getSparseParcelableArray<Parcelable>(key)
|
||||
if (holderState != null) {
|
||||
holder.itemView.restoreHierarchyState(holderState)
|
||||
bundle.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
interface OnTitleClickListener {
|
||||
fun onTitleClick(source: CatalogueSource)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val HOLDER_BUNDLE_KEY = "holder_bundle"
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
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 [LatestController].
|
||||
*/
|
||||
class LatestCardAdapter(controller: LatestController) :
|
||||
FlexibleAdapter<LatestCardItem>(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 [LatestController]
|
||||
*/
|
||||
interface OnMangaClickListener {
|
||||
fun onMangaClick(manga: Manga)
|
||||
fun onMangaLongClick(manga: Manga)
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
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.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
import kotlinx.android.synthetic.main.global_search_controller_comfortable_card_item.itemImage
|
||||
import kotlinx.android.synthetic.main.global_search_controller_comfortable_card_item.progress
|
||||
import kotlinx.android.synthetic.main.global_search_controller_comfortable_card_item.tvTitle
|
||||
|
||||
class LatestCardHolder(view: View, adapter: LatestCardAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
init {
|
||||
// Call onMangaClickListener when item is pressed.
|
||||
itemView.setOnClickListener {
|
||||
val item = adapter.getItem(bindingAdapterPosition)
|
||||
if (item != null) {
|
||||
adapter.mangaClickListener.onMangaClick(item.manga)
|
||||
}
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
val item = adapter.getItem(bindingAdapterPosition)
|
||||
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.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(itemImage, progress))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
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
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class LatestCardItem(val manga: Manga) : AbstractFlexibleItem<LatestCardHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return when (Injekt.get<PreferencesHelper>().catalogueDisplayMode().get()) {
|
||||
PreferenceValues.DisplayMode.COMPACT_GRID -> R.layout.global_search_controller_compact_card_item
|
||||
else -> R.layout.global_search_controller_comfortable_card_item
|
||||
}
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LatestCardHolder {
|
||||
return LatestCardHolder(view, adapter as LatestCardAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: LatestCardHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LatestCardItem) {
|
||||
return manga.id == other.manga.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return manga.id?.toInt() ?: 0
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.databinding.LatestControllerBinding
|
||||
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.browse.source.globalsearch.LatestAdapter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
||||
/**
|
||||
* 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 [LatestPresenter]
|
||||
* [LatestCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class LatestController :
|
||||
NucleusController<LatestControllerBinding, LatestPresenter>(),
|
||||
LatestCardAdapter.OnMangaClickListener,
|
||||
LatestAdapter.OnTitleClickListener {
|
||||
|
||||
/**
|
||||
* Adapter containing search results grouped by lang.
|
||||
*/
|
||||
protected var adapter: LatestAdapter? = null
|
||||
|
||||
/*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 = LatestControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.latest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [LatestPresenter] used in controller.
|
||||
*
|
||||
* @return instance of [LatestPresenter]
|
||||
*/
|
||||
override fun createPresenter(): LatestPresenter {
|
||||
return LatestPresenter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when manga in global search is clicked, opens manga.
|
||||
*
|
||||
* @param manga clicked item containing manga information.
|
||||
*/
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
// Open MangaController.
|
||||
if (presenter.preferences.eh_useNewMangaInterface().get()) {
|
||||
parentController?.router?.pushController(MangaAllInOneController(manga, true).withFadeTransaction())
|
||||
} else {
|
||||
parentController?.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
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
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
|
||||
}
|
||||
})*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
*
|
||||
* @param view view of controller
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = LatestAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
presenter.preferences.latestTabSources()
|
||||
.asImmediateFlow { presenter.getLatest() }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
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): LatestHolder? {
|
||||
val adapter = adapter ?: return null
|
||||
|
||||
adapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = adapter.getItem(holder.bindingAdapterPosition)
|
||||
if (item != null && source.id == item.source.id) {
|
||||
return holder as LatestHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Add search result to adapter.
|
||||
*
|
||||
* @param latestManga the source items containing the latest manga.
|
||||
*/
|
||||
fun setItems(latestManga: List<LatestItem>) {
|
||||
adapter?.updateDataSet(latestManga)
|
||||
|
||||
if (latestManga.isEmpty()) {
|
||||
binding.emptyView.show(R.string.latest_tab_empty)
|
||||
} else {
|
||||
binding.emptyView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the initialized manga.
|
||||
*/
|
||||
fun onMangaInitialized(source: CatalogueSource, manga: Manga) {
|
||||
getHolder(source)?.setImage(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a catalogue with the given search.
|
||||
*/
|
||||
override fun onTitleClick(source: CatalogueSource) {
|
||||
presenter.preferences.lastUsedCatalogueSource().set(source.id)
|
||||
parentController?.router?.pushController(LatestUpdatesController(source).withFadeTransaction())
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
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.ui.browse.source.globalsearch.LatestAdapter
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.progress
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.recycler
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.source_card
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.title
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.title_wrapper
|
||||
|
||||
/**
|
||||
* Holder that binds the [LatestItem] containing catalogue cards.
|
||||
*
|
||||
* @param view view of [LatestItem]
|
||||
* @param adapter instance of [LatestAdapter]
|
||||
*/
|
||||
class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Adapter containing manga from search results.
|
||||
*/
|
||||
private val mangaAdapter = LatestCardAdapter(adapter.controller)
|
||||
|
||||
private var lastBoundResults: List<LatestCardItem>? = null
|
||||
|
||||
init {
|
||||
// Set layout horizontal.
|
||||
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
recycler.adapter = mangaAdapter
|
||||
|
||||
title_wrapper.setOnClickListener {
|
||||
adapter.getItem(bindingAdapterPosition)?.let {
|
||||
adapter.titleClickListener.onTitleClick(it.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the loading of source search result.
|
||||
*
|
||||
* @param item item of card.
|
||||
*/
|
||||
fun bind(item: LatestItem) {
|
||||
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): LatestCardHolder? {
|
||||
mangaAdapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = mangaAdapter.getItem(holder.bindingAdapterPosition)
|
||||
if (item != null && item.manga.id!! == manga.id!!) {
|
||||
return holder as LatestCardHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun showHolder() {
|
||||
title_wrapper.visible()
|
||||
source_card.visible()
|
||||
}
|
||||
|
||||
private fun hideHolder() {
|
||||
title_wrapper.gone()
|
||||
source_card.gone()
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
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
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
|
||||
|
||||
/**
|
||||
* 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 LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?, val highlighted: Boolean = false) :
|
||||
AbstractFlexibleItem<LatestHolder>() {
|
||||
|
||||
/**
|
||||
* Set view.
|
||||
*
|
||||
* @return id of view
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.global_search_controller_card
|
||||
}
|
||||
|
||||
/**
|
||||
* Create view holder (see [LatestAdapter].
|
||||
*
|
||||
* @return holder of view.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LatestHolder {
|
||||
return LatestHolder(view, adapter as LatestAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind item to view.
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: LatestHolder,
|
||||
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()
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
||||
|
||||
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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
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.browse.latest.LatestCardItem
|
||||
import eu.kanade.tachiyomi.ui.browse.latest.LatestController
|
||||
import eu.kanade.tachiyomi.ui.browse.latest.LatestItem
|
||||
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 [LatestController]
|
||||
* 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 LatestPresenter(
|
||||
private val sourcesToUse: List<CatalogueSource>? = null,
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<LatestController>() {
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
override fun onDestroy() {
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchImageSubscription?.unsubscribe()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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().get()
|
||||
val watchedSources = preferences.latestTabSources().get()
|
||||
|
||||
val list = sourceManager.getVisibleCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||
|
||||
return list.filter { it.id.toString() in watchedSources }
|
||||
}
|
||||
|
||||
private fun getSourcesToGetLatest(): List<CatalogueSource> {
|
||||
return getEnabledSources()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a catalogue search item
|
||||
*/
|
||||
protected open fun createCatalogueSearchItem(source: CatalogueSource, results: List<LatestCardItem>?): LatestItem {
|
||||
return LatestItem(source, results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates get latest per watching source.
|
||||
*/
|
||||
fun getLatest() {
|
||||
// Create image fetch subscription
|
||||
initializeFetchImageSubscription()
|
||||
|
||||
// Create items with the initial state
|
||||
val initialItems = getSourcesToGetLatest().map { createCatalogueSearchItem(it, null) }
|
||||
var items = initialItems
|
||||
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchSourcesSubscription = Observable.from(getSourcesToGetLatest())
|
||||
.flatMap(
|
||||
{ source ->
|
||||
Observable.defer { source.fetchLatestUpdates(1) }
|
||||
.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 { list -> list.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
||||
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
||||
.map { list -> createCatalogueSearchItem(source, list.map { LatestCardItem(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 { pair ->
|
||||
val source = pair.second
|
||||
Observable.from(pair.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
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.ui.smartsearch.SmartSearchController
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
@ -167,6 +168,17 @@ class SourceController(bundle: Bundle? = null) :
|
||||
items.add(Pair(activity.getString(R.string.action_hide), { hideCatalogue(item.source) }))
|
||||
}
|
||||
|
||||
val isWatched = preferences.latestTabSources().get().contains(item.source.id.toString())
|
||||
|
||||
if (item.source.supportsLatest) {
|
||||
items.add(
|
||||
Pair(
|
||||
activity.getString(if (isWatched) R.string.unwatch else R.string.watch),
|
||||
{ watchCatalogue(item.source, isWatched) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
MaterialDialog(activity)
|
||||
.title(text = item.source.name)
|
||||
.listItems(
|
||||
@ -197,6 +209,19 @@ class SourceController(bundle: Bundle? = null) :
|
||||
presenter.updateSources()
|
||||
}
|
||||
|
||||
private fun watchCatalogue(source: Source, isWatched: Boolean) {
|
||||
val current = preferences.latestTabSources().get()
|
||||
|
||||
if (isWatched) {
|
||||
preferences.latestTabSources().set(current - source.id.toString())
|
||||
} else {
|
||||
if (current.size + 1 !in 0..5) {
|
||||
applicationContext?.toast(R.string.too_many_watched)
|
||||
return
|
||||
}
|
||||
preferences.latestTabSources().set(current + source.id.toString())
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called when browse is clicked in [SourceAdapter]
|
||||
*/
|
||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||
import eu.kanade.tachiyomi.util.preference.onChange
|
||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
|
||||
@ -15,6 +16,17 @@ class SettingsBrowseController : SettingsController() {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||
titleRes = R.string.browse
|
||||
|
||||
preferenceCategory {
|
||||
titleRes = R.string.pref_category_general
|
||||
|
||||
switchPreference {
|
||||
key = Keys.latest_tab_position
|
||||
titleRes = R.string.pref_latest_position
|
||||
summaryRes = R.string.pref_latest_position_summery
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
titleRes = R.string.label_extensions
|
||||
|
||||
|
43
app/src/main/res/layout/latest_controller.xml
Normal file
43
app/src/main/res/layout/latest_controller.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="@dimen/action_toolbar_list_padding"
|
||||
tools:listitem="@layout/latest_controller_card" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:alpha="0.75" />
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<eu.kanade.tachiyomi.widget.EmptyView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
71
app/src/main/res/layout/latest_controller_card.xml
Normal file
71
app/src/main/res/layout/latest_controller_card.xml
Normal file
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/title_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
app:srcCompat="@drawable/ic_add_24dp"
|
||||
app:tint="?android:attr/textColorPrimary"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.Regular.SubHeading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@+id/image"
|
||||
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
tools:text="Title" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
app:srcCompat="@drawable/ic_arrow_forward_24dp"
|
||||
app:tint="?android:attr/textColorPrimary"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/source_card"
|
||||
style="@style/Theme.Widget.CardView.Item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="144dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
tools:listitem="@layout/global_search_controller_comfortable_card_item" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
@ -117,6 +117,12 @@
|
||||
<string name="eh_auto_webtoon_mode">Auto Webtoon Mode Detection</string>
|
||||
<string name="eh_auto_webtoon_snack">Reading webtoon style</string>
|
||||
<string name="loading_gallery">Loading gallery…</string>
|
||||
<string name="watch">Watch</string>
|
||||
<string name="unwatch">Unwatch</string>
|
||||
<string name="pref_latest_position">Latest tab position</string>
|
||||
<string name="pref_latest_position_summery">Do you want the latest tab to be the first tab in browse? This will make it the default tab when opening browse, not recommended if your on data or a metered network</string>
|
||||
<string name="too_many_watched">Too many watched sources, cannot add more then 5</string>
|
||||
<string name="latest_tab_empty">You don\'t have any watched sources, go to the sources tab and long press a source to watch it</string>
|
||||
|
||||
<!-- AZ -->
|
||||
<string name="az_recommends">See Recommendations</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user