Add Latest tab to show you up to 5 sources showing you their latest manga

This commit is contained in:
Jobobby04 2020-06-12 19:19:58 -04:00
parent b745a74e1f
commit e30694c12c
16 changed files with 994 additions and 9 deletions

View File

@ -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"
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}
}

View File

@ -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
}
}

View File

@ -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())
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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]
*/

View File

@ -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

View 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>

View 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>

View File

@ -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>