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_ehentai_quality = "ehentai_quality"
|
||||||
|
|
||||||
const val eh_enable_hah = "eh_enable_hah"
|
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_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 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.RxController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController
|
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.migration.sources.MigrationSourcesController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
import kotlinx.android.synthetic.main.main_activity.tabs
|
||||||
@ -100,7 +101,7 @@ class BrowseController :
|
|||||||
activity?.tabs?.apply {
|
activity?.tabs?.apply {
|
||||||
val updates = preferences.extensionUpdatesCount().get()
|
val updates = preferences.extensionUpdatesCount().get()
|
||||||
if (updates > 0) {
|
if (updates > 0) {
|
||||||
val badge: BadgeDrawable? = getTabAt(1)?.orCreateBadge
|
val badge: BadgeDrawable? = getTabAt(EXTENSIONS_CONTROLLER)?.orCreateBadge
|
||||||
badge?.isVisible = true
|
badge?.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
|
getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
|
||||||
@ -110,10 +111,23 @@ class BrowseController :
|
|||||||
|
|
||||||
private inner class BrowseAdapter : RouterPagerAdapter(this@BrowseController) {
|
private inner class BrowseAdapter : RouterPagerAdapter(this@BrowseController) {
|
||||||
|
|
||||||
private val tabTitles = listOf(
|
private val tabTitles = (
|
||||||
|
if (preferences.latestTabInFront().get()) {
|
||||||
|
listOf(
|
||||||
|
R.string.latest,
|
||||||
R.string.label_sources,
|
R.string.label_sources,
|
||||||
R.string.label_extensions,
|
R.string.label_extensions,
|
||||||
R.string.label_migration
|
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) }
|
.map { resources!!.getString(it) }
|
||||||
|
|
||||||
@ -124,7 +138,8 @@ class BrowseController :
|
|||||||
override fun configureRouter(router: Router, position: Int) {
|
override fun configureRouter(router: Router, position: Int) {
|
||||||
if (!router.hasRootController()) {
|
if (!router.hasRootController()) {
|
||||||
val controller: Controller = when (position) {
|
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()
|
EXTENSIONS_CONTROLLER -> ExtensionController()
|
||||||
MIGRATION_CONTROLLER -> MigrationSourcesController()
|
MIGRATION_CONTROLLER -> MigrationSourcesController()
|
||||||
else -> error("Wrong position $position")
|
else -> error("Wrong position $position")
|
||||||
@ -142,7 +157,8 @@ class BrowseController :
|
|||||||
const val TO_EXTENSIONS_EXTRA = "to_extensions"
|
const val TO_EXTENSIONS_EXTRA = "to_extensions"
|
||||||
|
|
||||||
const val SOURCES_CONTROLLER = 0
|
const val SOURCES_CONTROLLER = 0
|
||||||
const val EXTENSIONS_CONTROLLER = 1
|
const val LATEST_CONTROLLER = 1
|
||||||
const val MIGRATION_CONTROLLER = 2
|
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.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.ui.smartsearch.SmartSearchController
|
import exh.ui.smartsearch.SmartSearchController
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
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) }))
|
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)
|
MaterialDialog(activity)
|
||||||
.title(text = item.source.name)
|
.title(text = item.source.name)
|
||||||
.listItems(
|
.listItems(
|
||||||
@ -197,6 +209,19 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
presenter.updateSources()
|
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]
|
* 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.defaultValue
|
||||||
import eu.kanade.tachiyomi.util.preference.onChange
|
import eu.kanade.tachiyomi.util.preference.onChange
|
||||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
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.switchPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||||
|
|
||||||
@ -15,6 +16,17 @@ class SettingsBrowseController : SettingsController() {
|
|||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||||
titleRes = R.string.browse
|
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 {
|
preferenceCategory {
|
||||||
titleRes = R.string.label_extensions
|
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_mode">Auto Webtoon Mode Detection</string>
|
||||||
<string name="eh_auto_webtoon_snack">Reading webtoon style</string>
|
<string name="eh_auto_webtoon_snack">Reading webtoon style</string>
|
||||||
<string name="loading_gallery">Loading gallery…</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 -->
|
<!-- AZ -->
|
||||||
<string name="az_recommends">See Recommendations</string>
|
<string name="az_recommends">See Recommendations</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user