Cleanup unused files and strings
This commit is contained in:
@@ -11,7 +11,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.LatestControllerBinding
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
@@ -28,7 +28,7 @@ import exh.savedsearches.models.SavedSearch
|
||||
* [FeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class FeedController :
|
||||
NucleusController<LatestControllerBinding, FeedPresenter>(),
|
||||
NucleusController<GlobalSearchControllerBinding, FeedPresenter>(),
|
||||
FeedCardAdapter.OnMangaClickListener,
|
||||
FeedAdapter.OnFeedClickListener {
|
||||
|
||||
@@ -123,7 +123,7 @@ open class FeedController :
|
||||
onMangaClick(manga)
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater): LatestControllerBinding = LatestControllerBinding.inflate(inflater)
|
||||
override fun createBinding(inflater: LayoutInflater): GlobalSearchControllerBinding = GlobalSearchControllerBinding.inflate(inflater)
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.LatestControllerCardBinding
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardBinding
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
/**
|
||||
@@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
class FeedHolder(view: View, val adapter: FeedAdapter) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = LatestControllerCardBinding.bind(view)
|
||||
private val binding = GlobalSearchControllerCardBinding.bind(view)
|
||||
|
||||
/**
|
||||
* Adapter containing manga from search results.
|
||||
|
||||
+1
-1
@@ -345,7 +345,7 @@ open class BrowseSourcePresenter(
|
||||
.forEach { service ->
|
||||
launchIO {
|
||||
try {
|
||||
service.match(source, manga)?.let { track ->
|
||||
service.match(manga)?.let { track ->
|
||||
track.manga_id = manga.id!!
|
||||
(service as TrackService).bind(track)
|
||||
db.insertTrack(track).executeAsBlocking()
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.LatestControllerBinding
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@@ -40,7 +40,7 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
* [SourceFeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class SourceFeedController :
|
||||
SearchableNucleusController<LatestControllerBinding, SourceFeedPresenter>,
|
||||
SearchableNucleusController<GlobalSearchControllerBinding, SourceFeedPresenter>,
|
||||
FabController,
|
||||
SourceFeedCardAdapter.OnMangaClickListener,
|
||||
SourceFeedAdapter.OnFeedClickListener {
|
||||
@@ -131,7 +131,7 @@ open class SourceFeedController :
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater): LatestControllerBinding = LatestControllerBinding.inflate(inflater)
|
||||
override fun createBinding(inflater: LayoutInflater): GlobalSearchControllerBinding = GlobalSearchControllerBinding.inflate(inflater)
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.LatestControllerCardBinding
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardBinding
|
||||
|
||||
/**
|
||||
* Holder that binds the [SourceFeedItem] containing catalogue cards.
|
||||
@@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.databinding.LatestControllerCardBinding
|
||||
class SourceFeedHolder(view: View, val adapter: SourceFeedAdapter) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = LatestControllerCardBinding.bind(view)
|
||||
private val binding = GlobalSearchControllerCardBinding.bind(view)
|
||||
|
||||
/**
|
||||
* Adapter containing manga from search results.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
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 [IndexController].
|
||||
*/
|
||||
class IndexCardAdapter(controller: IndexController) :
|
||||
FlexibleAdapter<IndexCardItem>(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 [IndexController]
|
||||
*/
|
||||
interface OnMangaClickListener {
|
||||
fun onMangaClick(manga: Manga)
|
||||
fun onMangaLongClick(manga: Manga)
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.dispose
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding
|
||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
||||
|
||||
class IndexCardHolder(view: View, adapter: IndexCardAdapter) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = GlobalSearchControllerCardItemBinding.bind(view)
|
||||
|
||||
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) {
|
||||
binding.card.clipToOutline = true
|
||||
|
||||
// Set manga title
|
||||
binding.title.text = manga.title
|
||||
|
||||
// Set alpha of thumbnail.
|
||||
binding.cover.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||
|
||||
// For rounded corners
|
||||
binding.badges.clipToOutline = true
|
||||
|
||||
// Set favorite badge
|
||||
binding.favoriteText.isVisible = manga.favorite
|
||||
|
||||
setImage(manga)
|
||||
}
|
||||
|
||||
fun setImage(manga: Manga) {
|
||||
binding.cover.dispose()
|
||||
binding.cover.loadAutoPause(manga) {
|
||||
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
class IndexCardItem(val manga: Manga) : AbstractFlexibleItem<IndexCardHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.global_search_controller_card_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): IndexCardHolder {
|
||||
return IndexCardHolder(view, adapter as IndexCardAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: IndexCardHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is IndexCardItem) {
|
||||
return manga.id == other.manga.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return manga.id?.toInt() ?: 0
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.IndexControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
|
||||
/**
|
||||
* 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 [IndexPresenter]
|
||||
* [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class IndexController :
|
||||
SearchableNucleusController<IndexControllerBinding, IndexPresenter>,
|
||||
FabController,
|
||||
IndexCardAdapter.OnMangaClickListener {
|
||||
|
||||
constructor(source: CatalogueSource?) : super(
|
||||
bundleOf(
|
||||
SOURCE_EXTRA to (source?.id ?: 0)
|
||||
)
|
||||
) {
|
||||
this.source = source
|
||||
}
|
||||
|
||||
constructor(sourceId: Long) : this(
|
||||
Injekt.get<SourceManager>().get(sourceId) as? CatalogueSource
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : this(bundle.getLong(SOURCE_EXTRA))
|
||||
|
||||
var source: CatalogueSource? = null
|
||||
|
||||
private var latestAdapter: IndexCardAdapter? = null
|
||||
private var browseAdapter: IndexCardAdapter? = null
|
||||
|
||||
private var actionFab: ExtendedFloatingActionButton? = null
|
||||
|
||||
/**
|
||||
* Sheet containing filter items.
|
||||
*/
|
||||
private var filterSheet: SourceFilterSheet? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return source!!.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [IndexPresenter] used in controller.
|
||||
*
|
||||
* @return instance of [IndexPresenter]
|
||||
*/
|
||||
override fun createPresenter(): IndexPresenter {
|
||||
return IndexPresenter(source!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when manga in global search is clicked, opens manga.
|
||||
*
|
||||
* @param manga clicked item containing manga information.
|
||||
*/
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
// Open MangaController.
|
||||
router.pushController(MangaController(manga, true).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when manga in global search is long clicked.
|
||||
*
|
||||
* @param manga clicked item containing manga information.
|
||||
*/
|
||||
override fun onMangaLongClick(manga: Manga) {
|
||||
// Delegate to single click by default.
|
||||
onMangaClick(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the options menu.
|
||||
*
|
||||
* @param menu menu containing options.
|
||||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
createOptionsMenu(menu, inflater, R.menu.global_search, R.id.action_search)
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextSubmit(query: String?) {
|
||||
onBrowseClick(query.nullIfBlank())
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextChange(newText: String?) {
|
||||
if (router.backstack.lastOrNull()?.controller == this) {
|
||||
presenter.query = newText ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = IndexControllerBinding.inflate(inflater)
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
*
|
||||
* @param view view of controller
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Prepare filter sheet
|
||||
initFilterSheet()
|
||||
|
||||
latestAdapter = IndexCardAdapter(this)
|
||||
|
||||
binding.latestRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.latestRecycler.adapter = latestAdapter
|
||||
|
||||
browseAdapter = IndexCardAdapter(this)
|
||||
|
||||
binding.browseRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.browseRecycler.adapter = browseAdapter
|
||||
|
||||
binding.latestBarWrapper.clicks()
|
||||
.onEach {
|
||||
onLatestClick()
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
binding.browseBarWrapper.clicks()
|
||||
.onEach {
|
||||
onBrowseClick()
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
presenter.latestItems
|
||||
.onEach {
|
||||
bind(it, true)
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
presenter.browseItems
|
||||
.onEach {
|
||||
bind(it, false)
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
presenter.getLatest()
|
||||
}
|
||||
|
||||
private val filterSerializer = FilterSerializer()
|
||||
|
||||
open fun initFilterSheet() {
|
||||
if (presenter.sourceFilters.isEmpty()) {
|
||||
actionFab?.text = activity!!.getString(R.string.saved_searches)
|
||||
}
|
||||
|
||||
filterSheet = SourceFilterSheet(
|
||||
activity!!,
|
||||
// SY -->
|
||||
this,
|
||||
presenter.source,
|
||||
presenter.loadSearches(),
|
||||
// SY <--
|
||||
onFilterClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
filterSheet?.dismiss()
|
||||
if (allDefault) {
|
||||
onBrowseClick(
|
||||
presenter.query.nullIfBlank()
|
||||
)
|
||||
} else {
|
||||
onBrowseClick(
|
||||
presenter.query.nullIfBlank(),
|
||||
filters = Json.encodeToString(filterSerializer.serialize(presenter.sourceFilters))
|
||||
)
|
||||
}
|
||||
},
|
||||
onResetClicked = {},
|
||||
onSaveClicked = {},
|
||||
onSavedSearchClicked = cb@{ idOfSearch ->
|
||||
val search = presenter.loadSearch(idOfSearch)
|
||||
|
||||
if (search == null) {
|
||||
filterSheet?.context?.let {
|
||||
MaterialAlertDialogBuilder(it)
|
||||
.setTitle(R.string.save_search_failed_to_load)
|
||||
.setMessage(R.string.save_search_failed_to_load_message)
|
||||
.show()
|
||||
}
|
||||
return@cb
|
||||
}
|
||||
|
||||
if (search.filterList == null) {
|
||||
activity?.toast(R.string.save_search_invalid)
|
||||
return@cb
|
||||
}
|
||||
|
||||
presenter.sourceFilters = FilterList(search.filterList)
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
filterSheet?.dismiss()
|
||||
|
||||
if (!allDefault) {
|
||||
onBrowseClick(
|
||||
search = presenter.query.nullIfBlank(),
|
||||
savedSearch = search.id
|
||||
)
|
||||
}
|
||||
},
|
||||
onSavedSearchDeleteClicked = { _, _ -> }
|
||||
)
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
|
||||
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
|
||||
filterSheet?.setOnShowListener { actionFab?.isVisible = false }
|
||||
filterSheet?.setOnDismissListener { actionFab?.isVisible = true }
|
||||
|
||||
actionFab?.setOnClickListener { filterSheet?.show() }
|
||||
|
||||
actionFab?.isVisible = true
|
||||
}
|
||||
|
||||
override fun configureFab(fab: ExtendedFloatingActionButton) {
|
||||
actionFab = fab
|
||||
|
||||
// Controlled by initFilterSheet()
|
||||
fab.isVisible = false
|
||||
|
||||
fab.setText(R.string.action_filter)
|
||||
fab.setIconResource(R.drawable.ic_filter_list_24dp)
|
||||
}
|
||||
|
||||
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||
fab.setOnClickListener(null)
|
||||
actionFab = null
|
||||
}
|
||||
|
||||
private fun bind(results: List<IndexCardItem>?, isLatest: Boolean) {
|
||||
val progress = if (isLatest) binding.latestProgress else binding.browseProgress
|
||||
when {
|
||||
results == null -> {
|
||||
progress.isVisible = true
|
||||
showResultsHolder(isLatest)
|
||||
}
|
||||
results.isEmpty() -> {
|
||||
progress.isVisible = false
|
||||
showNoResults(isLatest)
|
||||
}
|
||||
else -> {
|
||||
progress.isVisible = false
|
||||
showResultsHolder(isLatest)
|
||||
}
|
||||
}
|
||||
|
||||
val adapter = if (isLatest) {
|
||||
latestAdapter
|
||||
} else {
|
||||
browseAdapter
|
||||
}
|
||||
adapter?.updateDataSet(results)
|
||||
}
|
||||
|
||||
fun onError(e: Exception, isLatest: Boolean) {
|
||||
e.message?.let {
|
||||
val textView = if (isLatest) {
|
||||
binding.latestNoResultsFound
|
||||
} else {
|
||||
binding.browseNoResultsFound
|
||||
}
|
||||
textView.text = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun showResultsHolder(isLatest: Boolean) {
|
||||
(if (isLatest) binding.latestNoResultsFound else binding.browseNoResultsFound).isVisible = false
|
||||
}
|
||||
|
||||
private fun showNoResults(isLatest: Boolean) {
|
||||
(if (isLatest) binding.latestNoResultsFound else binding.browseNoResultsFound).isVisible = true
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
latestAdapter = null
|
||||
browseAdapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
fun onBrowseClick(search: String? = null, savedSearch: Long? = null, filters: String? = null) {
|
||||
router.replaceTopController(BrowseSourceController(presenter.source, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction())
|
||||
}
|
||||
|
||||
private fun onLatestClick() {
|
||||
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the initialized manga.
|
||||
*/
|
||||
fun onMangaInitialized(manga: Manga, isLatest: Boolean) {
|
||||
val adapter = if (isLatest) latestAdapter else browseAdapter
|
||||
adapter ?: return
|
||||
|
||||
adapter.allBoundViewHolders.forEach {
|
||||
if (it !is IndexCardHolder) return@forEach
|
||||
if (adapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
|
||||
it.setImage(manga)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SOURCE_EXTRA = "source"
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import exh.log.xLogE
|
||||
import exh.savedsearches.EXHSavedSearch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
import java.lang.RuntimeException
|
||||
|
||||
/**
|
||||
* Presenter of [IndexController]
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*
|
||||
* @param source the source.
|
||||
* @param db manages the database calls.
|
||||
* @param preferences manages the preference calls.
|
||||
*/
|
||||
open class IndexPresenter(
|
||||
val source: CatalogueSource,
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<IndexController>() {
|
||||
|
||||
/**
|
||||
* Subject which fetches image of given manga.
|
||||
*/
|
||||
private val fetchImageFlow = MutableSharedFlow<Pair<List<Manga>, Boolean>>()
|
||||
|
||||
/**
|
||||
* Modifiable list of filters.
|
||||
*/
|
||||
var sourceFilters = FilterList()
|
||||
set(value) {
|
||||
field = value
|
||||
filterItems = value.toItems()
|
||||
}
|
||||
|
||||
var filterItems: List<IFlexible<*>> = emptyList()
|
||||
|
||||
/**
|
||||
* Subscription for fetching images of manga.
|
||||
*/
|
||||
private var fetchImageJob: Job? = null
|
||||
|
||||
val latestItems = MutableStateFlow<List<IndexCardItem>?>(null)
|
||||
|
||||
val browseItems = MutableStateFlow<List<IndexCardItem>?>(null)
|
||||
|
||||
init {
|
||||
query = ""
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
sourceFilters = source.getFilterList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates get latest per watching source.
|
||||
*/
|
||||
fun getLatest() {
|
||||
// Create image fetch subscription
|
||||
initializeFetchImageSubscription()
|
||||
|
||||
presenterScope.launch(Dispatchers.IO) {
|
||||
if (latestItems.value != null) return@launch
|
||||
val results = if (source.supportsLatest) {
|
||||
try {
|
||||
source.fetchLatestUpdates(1)
|
||||
.awaitSingle()
|
||||
.mangas
|
||||
.map { networkToLocalManga(it, source.id) }
|
||||
} catch (e: Exception) {
|
||||
withUIContext {
|
||||
view?.onError(e, true)
|
||||
}
|
||||
emptyList()
|
||||
}
|
||||
} else emptyList()
|
||||
|
||||
fetchImage(results, true)
|
||||
|
||||
latestItems.value = results.map { IndexCardItem(it) }
|
||||
}
|
||||
|
||||
presenterScope.launch(Dispatchers.IO) {
|
||||
if (browseItems.value != null) return@launch
|
||||
val results = try {
|
||||
source.fetchPopularManga(1)
|
||||
.awaitSingle()
|
||||
.mangas
|
||||
.map { networkToLocalManga(it, source.id) }
|
||||
} catch (e: Exception) {
|
||||
withUIContext {
|
||||
view?.onError(e, true)
|
||||
}
|
||||
emptyList()
|
||||
}
|
||||
|
||||
fetchImage(results, false)
|
||||
|
||||
browseItems.value = results.map { IndexCardItem(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a list of manga.
|
||||
*
|
||||
* @param manga the list of manga to initialize.
|
||||
*/
|
||||
private fun fetchImage(manga: List<Manga>, isLatest: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
fetchImageFlow.emit(manga to isLatest)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||
*/
|
||||
private fun initializeFetchImageSubscription() {
|
||||
fetchImageJob?.cancel()
|
||||
fetchImageFlow
|
||||
.flatMapConcat { (manga, isLatest) ->
|
||||
manga.asFlow()
|
||||
.filter { it.thumbnail_url == null && !it.initialized }
|
||||
.map {
|
||||
getMangaDetailsFlow(it, source, isLatest)
|
||||
}
|
||||
}
|
||||
.buffer(Channel.RENDEZVOUS)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.onEach { (manga, isLatest) ->
|
||||
withUIContext {
|
||||
view?.onMangaInitialized(manga, isLatest)
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
logcat(LogPriority.ERROR, it)
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 suspend fun getMangaDetailsFlow(manga: Manga, source: Source, isLatest: Boolean): Pair<Manga, Boolean> {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
manga.copyFrom(networkManga.toSManga())
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
return manga to isLatest
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
private val filterSerializer = FilterSerializer()
|
||||
|
||||
fun loadSearch(searchId: Long): EXHSavedSearch? {
|
||||
val search = db.getSavedSearch(searchId).executeAsBlocking() ?: return null
|
||||
return EXHSavedSearch(
|
||||
id = search.id!!,
|
||||
name = search.name,
|
||||
query = search.query.orEmpty(),
|
||||
filterList = runCatching {
|
||||
val originalFilters = source.getFilterList()
|
||||
filterSerializer.deserialize(
|
||||
filters = originalFilters,
|
||||
json = search.filtersJson
|
||||
?.let { Json.decodeFromString<JsonArray>(it) }
|
||||
?: return@runCatching null
|
||||
)
|
||||
originalFilters
|
||||
}.getOrNull()
|
||||
)
|
||||
}
|
||||
|
||||
fun loadSearches(): List<EXHSavedSearch> {
|
||||
return db.getSavedSearches(source.id).executeAsBlocking().map {
|
||||
val filtersJson = it.filtersJson ?: return@map EXHSavedSearch(
|
||||
id = it.id!!,
|
||||
name = it.name,
|
||||
query = it.query.orEmpty(),
|
||||
filterList = null
|
||||
)
|
||||
val filters = try {
|
||||
Json.decodeFromString<JsonArray>(filtersJson)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: return@map EXHSavedSearch(
|
||||
id = it.id!!,
|
||||
name = it.name,
|
||||
query = it.query.orEmpty(),
|
||||
filterList = null
|
||||
)
|
||||
|
||||
try {
|
||||
val originalFilters = source.getFilterList()
|
||||
filterSerializer.deserialize(originalFilters, filters)
|
||||
EXHSavedSearch(
|
||||
id = it.id!!,
|
||||
name = it.name,
|
||||
query = it.query.orEmpty(),
|
||||
filterList = originalFilters
|
||||
)
|
||||
} catch (t: RuntimeException) {
|
||||
// Load failed
|
||||
xLogE("Failed to load saved search!", t)
|
||||
EXHSavedSearch(
|
||||
id = it.id!!,
|
||||
name = it.name,
|
||||
query = it.query.orEmpty(),
|
||||
filterList = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user