Add feed to the combined sources menus
This commit is contained in:
parent
6a41d96ddf
commit
d0e9d24f6f
@ -77,13 +77,26 @@ fun getReadMangaNotInLibraryQuery() =
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the manga merged into a merged manga
|
* Query to get the global feed saved searches
|
||||||
*/
|
*/
|
||||||
fun getFeedSavedSearchQuery() =
|
fun getGlobalFeedSavedSearchQuery() =
|
||||||
"""
|
"""
|
||||||
SELECT ${SavedSearchTable.TABLE}.*
|
SELECT ${SavedSearchTable.TABLE}.*
|
||||||
FROM (
|
FROM (
|
||||||
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE}
|
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 1
|
||||||
|
) AS M
|
||||||
|
JOIN ${SavedSearchTable.TABLE}
|
||||||
|
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the source feed saved searches
|
||||||
|
*/
|
||||||
|
fun getSourceFeedSavedSearchQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${SavedSearchTable.TABLE}.*
|
||||||
|
FROM (
|
||||||
|
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 0 AND ${FeedSavedSearchTable.COL_SOURCE} = ?
|
||||||
) AS M
|
) AS M
|
||||||
JOIN ${SavedSearchTable.TABLE}
|
JOIN ${SavedSearchTable.TABLE}
|
||||||
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
|
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
|
||||||
|
@ -167,12 +167,12 @@ open class FeedController :
|
|||||||
* @param source used to find holder containing source
|
* @param source used to find holder containing source
|
||||||
* @return the holder of the manga or null if it's not bound.
|
* @return the holder of the manga or null if it's not bound.
|
||||||
*/
|
*/
|
||||||
private fun getHolder(source: CatalogueSource): FeedHolder? {
|
private fun getHolder(feed: FeedSavedSearch): FeedHolder? {
|
||||||
val adapter = adapter ?: return null
|
val adapter = adapter ?: return null
|
||||||
|
|
||||||
adapter.allBoundViewHolders.forEach { holder ->
|
adapter.allBoundViewHolders.forEach { holder ->
|
||||||
val item = adapter.getItem(holder.bindingAdapterPosition)
|
val item = adapter.getItem(holder.bindingAdapterPosition)
|
||||||
if (item != null && source.id == item.feed.id) {
|
if (item != null && feed.id == item.feed.id) {
|
||||||
return holder as FeedHolder
|
return holder as FeedHolder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,8 +200,8 @@ open class FeedController :
|
|||||||
*
|
*
|
||||||
* @param manga the initialized manga.
|
* @param manga the initialized manga.
|
||||||
*/
|
*/
|
||||||
fun onMangaInitialized(source: CatalogueSource, manga: Manga) {
|
fun onMangaInitialized(feed: FeedSavedSearch, manga: Manga) {
|
||||||
getHolder(source)?.setImage(manga)
|
getHolder(feed)?.setImage(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,7 +7,6 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
|||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
|
|
||||||
import exh.savedsearches.models.FeedSavedSearch
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
import exh.savedsearches.models.SavedSearch
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
|
||||||
@ -62,8 +61,8 @@ class FeedItem(
|
|||||||
* @return items are equal?
|
* @return items are equal?
|
||||||
*/
|
*/
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other is GlobalSearchItem) {
|
if (other is FeedItem) {
|
||||||
return feed.id == other.source.id
|
return feed.id == other.feed.id
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ open class FeedPresenter(
|
|||||||
/**
|
/**
|
||||||
* Subject which fetches image of given manga.
|
* Subject which fetches image of given manga.
|
||||||
*/
|
*/
|
||||||
private val fetchImageSubject = PublishSubject.create<Pair<List<Manga>, Source>>()
|
private val fetchImageSubject = PublishSubject.create<Triple<List<Manga>, Source, FeedSavedSearch>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for fetching images of manga.
|
* Subscription for fetching images of manga.
|
||||||
@ -62,7 +62,7 @@ open class FeedPresenter(
|
|||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
db.getFeedSavedSearches()
|
db.getGlobalFeedSavedSearches()
|
||||||
.asRxObservable()
|
.asRxObservable()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnEach {
|
.doOnEach {
|
||||||
@ -79,7 +79,7 @@ open class FeedPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun hasTooManyFeeds(): Boolean {
|
fun hasTooManyFeeds(): Boolean {
|
||||||
return db.getFeedSavedSearches().executeAsBlocking().size > 10
|
return db.getGlobalFeedSavedSearches().executeAsBlocking().size > 10
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEnabledSources(): List<CatalogueSource> {
|
fun getEnabledSources(): List<CatalogueSource> {
|
||||||
@ -103,7 +103,8 @@ open class FeedPresenter(
|
|||||||
FeedSavedSearch(
|
FeedSavedSearch(
|
||||||
id = null,
|
id = null,
|
||||||
source = source.id,
|
source = source.id,
|
||||||
savedSearch = savedSearch?.id
|
savedSearch = savedSearch?.id,
|
||||||
|
global = true
|
||||||
)
|
)
|
||||||
).executeAsBlocking()
|
).executeAsBlocking()
|
||||||
}
|
}
|
||||||
@ -116,9 +117,9 @@ open class FeedPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> {
|
private fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> {
|
||||||
val savedSearches = db.getSavedSearchesFeed().executeAsBlocking()
|
val savedSearches = db.getGlobalSavedSearchesFeed().executeAsBlocking()
|
||||||
.associateBy { it.id!! }
|
.associateBy { it.id!! }
|
||||||
return db.getFeedSavedSearches().executeAsBlocking()
|
return db.getGlobalFeedSavedSearches().executeAsBlocking()
|
||||||
.map { it to savedSearches[it.savedSearch] }
|
.map { it to savedSearches[it.savedSearch] }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +170,7 @@ open class FeedPresenter(
|
|||||||
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
|
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
|
||||||
.map { it.mangas } // Get manga from search result.
|
.map { it.mangas } // Get manga from search result.
|
||||||
.map { list -> list.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
.map { list -> list.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
||||||
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
.doOnNext { fetchImage(it, source, feed) } // Load manga covers.
|
||||||
.map { list -> createCatalogueSearchItem(feed, savedSearch, source, list.map { FeedCardItem(it) }) }
|
.map { list -> createCatalogueSearchItem(feed, savedSearch, source, list.map { FeedCardItem(it) }) }
|
||||||
} else {
|
} else {
|
||||||
Observable.just(createCatalogueSearchItem(feed, null, null, emptyList()))
|
Observable.just(createCatalogueSearchItem(feed, null, null, emptyList()))
|
||||||
@ -215,8 +216,8 @@ open class FeedPresenter(
|
|||||||
*
|
*
|
||||||
* @param manga the list of manga to initialize.
|
* @param manga the list of manga to initialize.
|
||||||
*/
|
*/
|
||||||
private fun fetchImage(manga: List<Manga>, source: Source) {
|
private fun fetchImage(manga: List<Manga>, source: CatalogueSource, feed: FeedSavedSearch) {
|
||||||
fetchImageSubject.onNext(Pair(manga, source))
|
fetchImageSubject.onNext(Triple(manga, source, feed))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -230,14 +231,14 @@ open class FeedPresenter(
|
|||||||
Observable.from(pair.first).filter { it.thumbnail_url == null && !it.initialized }
|
Observable.from(pair.first).filter { it.thumbnail_url == null && !it.initialized }
|
||||||
.map { Pair(it, source) }
|
.map { Pair(it, source) }
|
||||||
.concatMap { getMangaDetailsObservable(it.first, it.second) }
|
.concatMap { getMangaDetailsObservable(it.first, it.second) }
|
||||||
.map { Pair(source as CatalogueSource, it) }
|
.map { Pair(pair.third, it) }
|
||||||
}
|
}
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ (source, manga) ->
|
{ (feed, manga) ->
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
view?.onMangaInitialized(source, manga)
|
view?.onMangaInitialized(feed, manga)
|
||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
logcat(LogPriority.ERROR, error)
|
logcat(LogPriority.ERROR, error)
|
||||||
|
@ -29,8 +29,8 @@ import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
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.feed.SourceFeedController
|
||||||
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.index.IndexController
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
|
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
@ -145,7 +145,7 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
// Open the catalogue view.
|
// Open the catalogue view.
|
||||||
// SY -->
|
// SY -->
|
||||||
if (source.supportsLatest && preferences.useNewSourceNavigation().get()) {
|
if (source.supportsLatest && preferences.useNewSourceNavigation().get()) {
|
||||||
openIndexSource(source)
|
openSourceFeed(source)
|
||||||
} else openSource(source, BrowseSourceController(source))
|
} else openSource(source, BrowseSourceController(source))
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
@ -307,11 +307,11 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
/**
|
/**
|
||||||
* Opens a catalogue with the index controller.
|
* Opens a catalogue with the source feed controller.
|
||||||
*/
|
*/
|
||||||
private fun openIndexSource(source: CatalogueSource) {
|
private fun openSourceFeed(source: CatalogueSource) {
|
||||||
preferences.lastUsedSource().set(source.id)
|
preferences.lastUsedSource().set(source.id)
|
||||||
parentController!!.router.pushController(IndexController(source).withFadeTransaction())
|
parentController!!.router.pushController(SourceFeedController(source).withFadeTransaction())
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.SparseArray
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter that holds the search cards.
|
||||||
|
*
|
||||||
|
* @param controller instance of [SourceFeedController].
|
||||||
|
*/
|
||||||
|
class SourceFeedAdapter(val controller: SourceFeedController) :
|
||||||
|
FlexibleAdapter<SourceFeedItem>(null, controller, true) {
|
||||||
|
|
||||||
|
val feedClickListener: OnFeedClickListener = 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 OnFeedClickListener {
|
||||||
|
fun onLatestClick()
|
||||||
|
fun onBrowseClick()
|
||||||
|
fun onSavedSearchClick(savedSearch: SavedSearch)
|
||||||
|
fun onRemoveClick(feedSavedSearch: FeedSavedSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val HOLDER_BUNDLE_KEY = "holder_bundle"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
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 [SourceFeedController].
|
||||||
|
*/
|
||||||
|
class SourceFeedCardAdapter(controller: SourceFeedController) :
|
||||||
|
FlexibleAdapter<SourceFeedCardItem>(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 [SourceFeedController]
|
||||||
|
*/
|
||||||
|
interface OnMangaClickListener {
|
||||||
|
fun onMangaClick(manga: Manga)
|
||||||
|
fun onMangaLongClick(manga: Manga)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
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 SourceFeedCardHolder(view: View, adapter: SourceFeedCardAdapter) :
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
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 SourceFeedCardItem(val manga: Manga) : AbstractFlexibleItem<SourceFeedCardHolder>() {
|
||||||
|
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.global_search_controller_card_item
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceFeedCardHolder {
|
||||||
|
return SourceFeedCardHolder(view, adapter as SourceFeedCardAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindViewHolder(
|
||||||
|
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||||
|
holder: SourceFeedCardHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: List<Any?>?
|
||||||
|
) {
|
||||||
|
holder.bind(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other is SourceFeedCardItem) {
|
||||||
|
return manga.id == other.manga.id
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return manga.id?.toInt() ?: 0
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,345 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
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 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.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.savedsearches.models.FeedSavedSearch
|
||||||
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
import exh.util.nullIfBlank
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
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 [SourceFeedPresenter]
|
||||||
|
* [SourceFeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||||
|
*/
|
||||||
|
open class SourceFeedController :
|
||||||
|
SearchableNucleusController<LatestControllerBinding, SourceFeedPresenter>,
|
||||||
|
FabController,
|
||||||
|
SourceFeedCardAdapter.OnMangaClickListener,
|
||||||
|
SourceFeedAdapter.OnFeedClickListener {
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter containing search results grouped by lang.
|
||||||
|
*/
|
||||||
|
protected var adapter: SourceFeedAdapter? = null
|
||||||
|
|
||||||
|
var source: CatalogueSource? = 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 [SourceFeedPresenter] used in controller.
|
||||||
|
*
|
||||||
|
* @return instance of [SourceFeedPresenter]
|
||||||
|
*/
|
||||||
|
override fun createPresenter(): SourceFeedPresenter {
|
||||||
|
return SourceFeedPresenter(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.
|
||||||
|
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) {
|
||||||
|
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): LatestControllerBinding = LatestControllerBinding.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()
|
||||||
|
|
||||||
|
binding.recycler.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = SourceFeedAdapter(this)
|
||||||
|
|
||||||
|
// Create recycler and set adapter.
|
||||||
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
binding.recycler.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
adapter = null
|
||||||
|
super.onDestroyView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveViewState(view: View, outState: Bundle) {
|
||||||
|
super.onSaveViewState(view, outState)
|
||||||
|
adapter?.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
|
||||||
|
super.onRestoreViewState(view, savedViewState)
|
||||||
|
adapter?.onRestoreInstanceState(savedViewState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val filterSerializer = FilterSerializer()
|
||||||
|
|
||||||
|
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 = { idOfSearch, name ->
|
||||||
|
MaterialAlertDialogBuilder(activity!!)
|
||||||
|
.setTitle(R.string.feed)
|
||||||
|
.setMessage(activity!!.getString(R.string.feed_add, name))
|
||||||
|
.setPositiveButton(R.string.action_add) { _, _ ->
|
||||||
|
presenter.createFeed(idOfSearch)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(sourceFeed: SourceFeed): SourceFeedHolder? {
|
||||||
|
val adapter = adapter ?: return null
|
||||||
|
|
||||||
|
adapter.allBoundViewHolders.forEach { holder ->
|
||||||
|
val item = adapter.getItem(holder.bindingAdapterPosition)
|
||||||
|
if (item != null && sourceFeed == item.sourceFeed) {
|
||||||
|
return holder as SourceFeedHolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add search result to adapter.
|
||||||
|
*
|
||||||
|
* @param feedManga the source items containing the latest manga.
|
||||||
|
*/
|
||||||
|
fun setItems(feedManga: List<SourceFeedItem>) {
|
||||||
|
adapter?.updateDataSet(feedManga)
|
||||||
|
|
||||||
|
if (feedManga.isEmpty()) {
|
||||||
|
binding.emptyView.show(R.string.feed_tab_empty)
|
||||||
|
} else {
|
||||||
|
binding.emptyView.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from the presenter when a manga is initialized.
|
||||||
|
*
|
||||||
|
* @param manga the initialized manga.
|
||||||
|
*/
|
||||||
|
fun onMangaInitialized(sourceFeed: SourceFeed, manga: Manga) {
|
||||||
|
getHolder(sourceFeed)?.setImage(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBrowseClick(search: String? = null, savedSearch: Long? = null, filters: String? = null) {
|
||||||
|
router.replaceTopController(BrowseSourceController(presenter.source, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLatestClick() {
|
||||||
|
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBrowseClick() {
|
||||||
|
router.replaceTopController(BrowseSourceController(presenter.source).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSavedSearchClick(savedSearch: SavedSearch) {
|
||||||
|
router.replaceTopController(BrowseSourceController(presenter.source, savedSearch = savedSearch.id).withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoveClick(feedSavedSearch: FeedSavedSearch) {
|
||||||
|
MaterialAlertDialogBuilder(activity!!)
|
||||||
|
.setTitle(R.string.feed)
|
||||||
|
.setMessage(R.string.feed_delete)
|
||||||
|
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||||
|
presenter.deleteFeed(feedSavedSearch)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SOURCE_EXTRA = "source"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holder that binds the [SourceFeedItem] containing catalogue cards.
|
||||||
|
*
|
||||||
|
* @param view view of [SourceFeedItem]
|
||||||
|
* @param adapter instance of [SourceFeedAdapter]
|
||||||
|
*/
|
||||||
|
class SourceFeedHolder(view: View, val adapter: SourceFeedAdapter) :
|
||||||
|
FlexibleViewHolder(view, adapter) {
|
||||||
|
|
||||||
|
private val binding = LatestControllerCardBinding.bind(view)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter containing manga from search results.
|
||||||
|
*/
|
||||||
|
private val mangaAdapter = SourceFeedCardAdapter(adapter.controller)
|
||||||
|
|
||||||
|
private var lastBoundResults: List<SourceFeedCardItem>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Set layout horizontal.
|
||||||
|
binding.recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
binding.recycler.adapter = mangaAdapter
|
||||||
|
|
||||||
|
binding.titleWrapper.setOnClickListener {
|
||||||
|
adapter.getItem(bindingAdapterPosition)?.let {
|
||||||
|
when (it.sourceFeed) {
|
||||||
|
SourceFeed.Browse -> adapter.feedClickListener.onBrowseClick()
|
||||||
|
SourceFeed.Latest -> adapter.feedClickListener.onLatestClick()
|
||||||
|
is SourceFeed.SourceSavedSearch -> adapter.feedClickListener.onSavedSearchClick(it.sourceFeed.savedSearch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.titleWrapper.setOnLongClickListener {
|
||||||
|
adapter.getItem(bindingAdapterPosition)?.let {
|
||||||
|
when (it.sourceFeed) {
|
||||||
|
SourceFeed.Browse -> adapter.feedClickListener.onBrowseClick()
|
||||||
|
SourceFeed.Latest -> adapter.feedClickListener.onLatestClick()
|
||||||
|
is SourceFeed.SourceSavedSearch -> adapter.feedClickListener.onRemoveClick(it.sourceFeed.feed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the loading of source search result.
|
||||||
|
*
|
||||||
|
* @param item item of card.
|
||||||
|
*/
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
fun bind(item: SourceFeedItem) {
|
||||||
|
val results = item.results
|
||||||
|
|
||||||
|
when (item.sourceFeed) {
|
||||||
|
SourceFeed.Browse -> binding.title.setText(R.string.browse)
|
||||||
|
SourceFeed.Latest -> binding.title.setText(R.string.latest)
|
||||||
|
is SourceFeed.SourceSavedSearch -> binding.title.text = item.sourceFeed.savedSearch.name
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
results == null -> {
|
||||||
|
binding.progress.isVisible = true
|
||||||
|
showResultsHolder()
|
||||||
|
}
|
||||||
|
results.isEmpty() -> {
|
||||||
|
binding.progress.isVisible = false
|
||||||
|
showNoResults()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
binding.progress.isVisible = false
|
||||||
|
showResultsHolder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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): SourceFeedCardHolder? {
|
||||||
|
mangaAdapter.allBoundViewHolders.forEach { holder ->
|
||||||
|
val item = mangaAdapter.getItem(holder.bindingAdapterPosition)
|
||||||
|
if (item != null && item.manga.id!! == manga.id!!) {
|
||||||
|
return holder as SourceFeedCardHolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showResultsHolder() {
|
||||||
|
binding.noResultsFound.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNoResults() {
|
||||||
|
binding.noResultsFound.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item that contains search result information.
|
||||||
|
*
|
||||||
|
* @param feed 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 SourceFeedItem(
|
||||||
|
val sourceFeed: SourceFeed,
|
||||||
|
val results: List<SourceFeedCardItem>?,
|
||||||
|
val highlighted: Boolean = false
|
||||||
|
) : AbstractFlexibleItem<SourceFeedHolder>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set view.
|
||||||
|
*
|
||||||
|
* @return id of view
|
||||||
|
*/
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.global_search_controller_card
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create view holder (see [SourceFeedAdapter].
|
||||||
|
*
|
||||||
|
* @return holder of view.
|
||||||
|
*/
|
||||||
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceFeedHolder {
|
||||||
|
return SourceFeedHolder(view, adapter as SourceFeedAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind item to view.
|
||||||
|
*/
|
||||||
|
override fun bindViewHolder(
|
||||||
|
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||||
|
holder: SourceFeedHolder,
|
||||||
|
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 SourceFeedItem) {
|
||||||
|
return sourceFeed == other.sourceFeed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return hash code of item.
|
||||||
|
*
|
||||||
|
* @return hashcode
|
||||||
|
*/
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return sourceFeed.hashCode()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,357 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||||
|
|
||||||
|
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.MangasPage
|
||||||
|
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.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import exh.log.xLogE
|
||||||
|
import exh.savedsearches.EXHSavedSearch
|
||||||
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import logcat.LogPriority
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
|
||||||
|
sealed class SourceFeed {
|
||||||
|
object Latest : SourceFeed()
|
||||||
|
object Browse : SourceFeed()
|
||||||
|
data class SourceSavedSearch(val feed: FeedSavedSearch, val savedSearch: SavedSearch) : SourceFeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presenter of [SourceFeedController]
|
||||||
|
* 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 SourceFeedPresenter(
|
||||||
|
val source: CatalogueSource,
|
||||||
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
) : BasePresenter<SourceFeedController>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the different sources by user settings.
|
||||||
|
*/
|
||||||
|
private var fetchSourcesSubscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject which fetches image of given manga.
|
||||||
|
*/
|
||||||
|
private val fetchImageSubject = PublishSubject.create<Triple<List<Manga>, Source, SourceFeed>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription for fetching images of manga.
|
||||||
|
*/
|
||||||
|
private var fetchImageSubscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifiable list of filters.
|
||||||
|
*/
|
||||||
|
var sourceFilters = FilterList()
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
filterItems = value.toItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterItems: List<IFlexible<*>> = emptyList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
query = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
sourceFilters = source.getFilterList()
|
||||||
|
|
||||||
|
db.getSourceFeedSavedSearches(source.id)
|
||||||
|
.asRxObservable()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnEach {
|
||||||
|
getFeed()
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
.let(::add)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
fetchSourcesSubscription?.unsubscribe()
|
||||||
|
fetchImageSubscription?.unsubscribe()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasTooManyFeeds(): Boolean {
|
||||||
|
return db.getSourceFeedSavedSearches(source.id).executeAsBlocking().size > 10
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSourceSavedSearches(): List<SavedSearch> {
|
||||||
|
return db.getSavedSearches(source.id).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createFeed(savedSearchId: Long) {
|
||||||
|
launchIO {
|
||||||
|
db.insertFeedSavedSearch(
|
||||||
|
FeedSavedSearch(
|
||||||
|
id = null,
|
||||||
|
source = source.id,
|
||||||
|
savedSearch = savedSearchId,
|
||||||
|
global = false
|
||||||
|
)
|
||||||
|
).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteFeed(feed: FeedSavedSearch) {
|
||||||
|
launchIO {
|
||||||
|
db.deleteFeedSavedSearch(feed).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSourcesToGetFeed(): List<SourceFeed> {
|
||||||
|
val savedSearches = db.getSourceSavedSearchesFeed(source.id).executeAsBlocking()
|
||||||
|
.associateBy { it.id!! }
|
||||||
|
|
||||||
|
return listOf(SourceFeed.Latest, SourceFeed.Browse) + db.getSourceFeedSavedSearches(source.id).executeAsBlocking()
|
||||||
|
.map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a catalogue search item
|
||||||
|
*/
|
||||||
|
protected open fun createCatalogueSearchItem(
|
||||||
|
sourceFeed: SourceFeed,
|
||||||
|
results: List<SourceFeedCardItem>?
|
||||||
|
): SourceFeedItem {
|
||||||
|
return SourceFeedItem(sourceFeed, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates get manga per feed.
|
||||||
|
*/
|
||||||
|
fun getFeed() {
|
||||||
|
// Create image fetch subscription
|
||||||
|
initializeFetchImageSubscription()
|
||||||
|
|
||||||
|
// Create items with the initial state
|
||||||
|
val initialItems = getSourcesToGetFeed().map {
|
||||||
|
createCatalogueSearchItem(
|
||||||
|
it,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var items = initialItems
|
||||||
|
|
||||||
|
fetchSourcesSubscription?.unsubscribe()
|
||||||
|
fetchSourcesSubscription = Observable.from(getSourcesToGetFeed())
|
||||||
|
.flatMap(
|
||||||
|
{ sourceFeed ->
|
||||||
|
Observable.defer {
|
||||||
|
when (sourceFeed) {
|
||||||
|
SourceFeed.Browse -> source.fetchPopularManga(1)
|
||||||
|
SourceFeed.Latest -> source.fetchLatestUpdates(1)
|
||||||
|
is SourceFeed.SourceSavedSearch -> source.fetchSearchManga(
|
||||||
|
page = 1,
|
||||||
|
query = sourceFeed.savedSearch.query.orEmpty(),
|
||||||
|
filters = getFilterList(sourceFeed.savedSearch, source)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
|
||||||
|
.map { it.mangas } // Get manga from search result.
|
||||||
|
.map { list -> list.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
||||||
|
.doOnNext { fetchImage(it, source, sourceFeed) } // Load manga covers.
|
||||||
|
.map { list -> createCatalogueSearchItem(sourceFeed, list.map { SourceFeedCardItem(it) }) }
|
||||||
|
},
|
||||||
|
5
|
||||||
|
)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
// Update matching source with the obtained results
|
||||||
|
.map { result ->
|
||||||
|
items.map { item -> if (item.sourceFeed == result.sourceFeed) result else item }
|
||||||
|
}
|
||||||
|
// Update current state
|
||||||
|
.doOnNext { items = it }
|
||||||
|
// Deliver initial state
|
||||||
|
.startWith(initialItems)
|
||||||
|
.subscribeLatestCache(
|
||||||
|
{ view, manga ->
|
||||||
|
view.setItems(manga)
|
||||||
|
},
|
||||||
|
{ _, error ->
|
||||||
|
logcat(LogPriority.ERROR, error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val filterSerializer = FilterSerializer()
|
||||||
|
|
||||||
|
private fun getFilterList(savedSearch: SavedSearch, source: CatalogueSource): FilterList {
|
||||||
|
val filters = savedSearch.filtersJson ?: return FilterList()
|
||||||
|
return runCatching {
|
||||||
|
val originalFilters = source.getFilterList()
|
||||||
|
filterSerializer.deserialize(
|
||||||
|
filters = originalFilters,
|
||||||
|
json = Json.decodeFromString(filters)
|
||||||
|
)
|
||||||
|
originalFilters
|
||||||
|
}.getOrElse { FilterList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a list of manga.
|
||||||
|
*
|
||||||
|
* @param manga the list of manga to initialize.
|
||||||
|
*/
|
||||||
|
private fun fetchImage(manga: List<Manga>, source: Source, sourceFeed: SourceFeed) {
|
||||||
|
fetchImageSubject.onNext(Triple(manga, source, sourceFeed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(pair.third, it) }
|
||||||
|
}
|
||||||
|
.onBackpressureBuffer()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ (sourceFeed, manga) ->
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
view?.onMangaInitialized(sourceFeed, manga)
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
|
logcat(LogPriority.ERROR, 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 runAsObservable {
|
||||||
|
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||||
|
manga.copyFrom(networkManga.toSManga())
|
||||||
|
manga.initialized = true
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -66,8 +66,8 @@ import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationContr
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController.Companion.SMART_SEARCH_SOURCE_TAG
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController.Companion.SMART_SEARCH_SOURCE_TAG
|
||||||
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.feed.SourceFeedController
|
||||||
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.index.IndexController
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCoverDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCoverDialog
|
||||||
@ -921,7 +921,7 @@ class MangaController :
|
|||||||
previousController.searchWithQuery(query)
|
previousController.searchWithQuery(query)
|
||||||
}
|
}
|
||||||
// SY -->
|
// SY -->
|
||||||
is IndexController -> {
|
is SourceFeedController -> {
|
||||||
router.handleBack()
|
router.handleBack()
|
||||||
previousController.onBrowseClick(query)
|
previousController.onBrowseClick(query)
|
||||||
}
|
}
|
||||||
|
@ -423,7 +423,8 @@ object EXHMigrations {
|
|||||||
FeedSavedSearch(
|
FeedSavedSearch(
|
||||||
id = null,
|
id = null,
|
||||||
source = it.toLong(),
|
source = it.toLong(),
|
||||||
savedSearch = null
|
savedSearch = null,
|
||||||
|
global = true
|
||||||
)
|
)
|
||||||
}?.ifEmpty { null }
|
}?.ifEmpty { null }
|
||||||
if (feed != null) {
|
if (feed != null) {
|
||||||
|
@ -11,6 +11,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
import exh.savedsearches.models.FeedSavedSearch
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
|
import exh.savedsearches.tables.FeedSavedSearchTable.COL_GLOBAL
|
||||||
import exh.savedsearches.tables.FeedSavedSearchTable.COL_ID
|
import exh.savedsearches.tables.FeedSavedSearchTable.COL_ID
|
||||||
import exh.savedsearches.tables.FeedSavedSearchTable.COL_SAVED_SEARCH_ID
|
import exh.savedsearches.tables.FeedSavedSearchTable.COL_SAVED_SEARCH_ID
|
||||||
import exh.savedsearches.tables.FeedSavedSearchTable.COL_SOURCE
|
import exh.savedsearches.tables.FeedSavedSearchTable.COL_SOURCE
|
||||||
@ -37,7 +38,8 @@ class FeedSavedSearchPutResolver : DefaultPutResolver<FeedSavedSearch>() {
|
|||||||
override fun mapToContentValues(obj: FeedSavedSearch) = contentValuesOf(
|
override fun mapToContentValues(obj: FeedSavedSearch) = contentValuesOf(
|
||||||
COL_ID to obj.id,
|
COL_ID to obj.id,
|
||||||
COL_SOURCE to obj.source,
|
COL_SOURCE to obj.source,
|
||||||
COL_SAVED_SEARCH_ID to obj.savedSearch
|
COL_SAVED_SEARCH_ID to obj.savedSearch,
|
||||||
|
COL_GLOBAL to obj.global
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +48,8 @@ class FeedSavedSearchGetResolver : DefaultGetResolver<FeedSavedSearch>() {
|
|||||||
override fun mapFromCursor(cursor: Cursor): FeedSavedSearch = FeedSavedSearch(
|
override fun mapFromCursor(cursor: Cursor): FeedSavedSearch = FeedSavedSearch(
|
||||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID)),
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID)),
|
||||||
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE)),
|
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE)),
|
||||||
savedSearch = cursor.getLongOrNull(cursor.getColumnIndexOrThrow(COL_SAVED_SEARCH_ID))
|
savedSearch = cursor.getLongOrNull(cursor.getColumnIndexOrThrow(COL_SAVED_SEARCH_ID)),
|
||||||
|
global = cursor.getInt(cursor.getColumnIndexOrThrow(COL_GLOBAL)) == 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,5 +8,8 @@ data class FeedSavedSearch(
|
|||||||
var source: Long,
|
var source: Long,
|
||||||
|
|
||||||
// If -1 then get latest, if set get the saved search
|
// If -1 then get latest, if set get the saved search
|
||||||
var savedSearch: Long?
|
var savedSearch: Long?,
|
||||||
|
|
||||||
|
// If the feed is a global or source specific feed
|
||||||
|
var global: Boolean
|
||||||
)
|
)
|
||||||
|
@ -4,40 +4,32 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.queries.getFeedSavedSearchQuery
|
import eu.kanade.tachiyomi.data.database.queries.getGlobalFeedSavedSearchQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.getSourceFeedSavedSearchQuery
|
||||||
import exh.savedsearches.models.FeedSavedSearch
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
import exh.savedsearches.models.SavedSearch
|
import exh.savedsearches.models.SavedSearch
|
||||||
import exh.savedsearches.tables.FeedSavedSearchTable
|
import exh.savedsearches.tables.FeedSavedSearchTable
|
||||||
|
|
||||||
interface FeedSavedSearchQueries : DbProvider {
|
interface FeedSavedSearchQueries : DbProvider {
|
||||||
fun getFeedSavedSearches() = db.get()
|
fun getGlobalFeedSavedSearches() = db.get()
|
||||||
.listOfObjects(FeedSavedSearch::class.java)
|
.listOfObjects(FeedSavedSearch::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
Query.builder()
|
Query.builder()
|
||||||
.table(FeedSavedSearchTable.TABLE)
|
.table(FeedSavedSearchTable.TABLE)
|
||||||
|
.where("${FeedSavedSearchTable.COL_GLOBAL} = 1")
|
||||||
.orderBy(FeedSavedSearchTable.COL_ID)
|
.orderBy(FeedSavedSearchTable.COL_ID)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getFeedSavedSearch(id: Long) = db.get()
|
fun getSourceFeedSavedSearches(sourceId: Long) = db.get()
|
||||||
.`object`(FeedSavedSearch::class.java)
|
|
||||||
.withQuery(
|
|
||||||
Query.builder()
|
|
||||||
.table(FeedSavedSearchTable.TABLE)
|
|
||||||
.where("${FeedSavedSearchTable.COL_ID} = ?")
|
|
||||||
.whereArgs(id)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getFeedSavedSearches(ids: List<Long>) = db.get()
|
|
||||||
.listOfObjects(FeedSavedSearch::class.java)
|
.listOfObjects(FeedSavedSearch::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
Query.builder()
|
Query.builder()
|
||||||
.table(FeedSavedSearchTable.TABLE)
|
.table(FeedSavedSearchTable.TABLE)
|
||||||
.where("${FeedSavedSearchTable.COL_ID} IN (?)")
|
.where("${FeedSavedSearchTable.COL_SOURCE} = ? AND ${FeedSavedSearchTable.COL_GLOBAL} = 0")
|
||||||
.whereArgs(ids.joinToString())
|
.whereArgs(sourceId)
|
||||||
|
.orderBy(FeedSavedSearchTable.COL_ID)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
@ -64,11 +56,21 @@ interface FeedSavedSearchQueries : DbProvider {
|
|||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getSavedSearchesFeed() = db.get()
|
fun getGlobalSavedSearchesFeed() = db.get()
|
||||||
.listOfObjects(SavedSearch::class.java)
|
.listOfObjects(SavedSearch::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getFeedSavedSearchQuery())
|
.query(getGlobalFeedSavedSearchQuery())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getSourceSavedSearchesFeed(sourceId: Long) = db.get()
|
||||||
|
.listOfObjects(SavedSearch::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getSourceFeedSavedSearchQuery())
|
||||||
|
.args(sourceId)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
@ -10,12 +10,15 @@ object FeedSavedSearchTable {
|
|||||||
|
|
||||||
const val COL_SAVED_SEARCH_ID = "saved_search"
|
const val COL_SAVED_SEARCH_ID = "saved_search"
|
||||||
|
|
||||||
|
const val COL_GLOBAL = "global"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() =
|
get() =
|
||||||
"""CREATE TABLE $TABLE(
|
"""CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_SOURCE INTEGER NOT NULL,
|
$COL_SOURCE INTEGER NOT NULL,
|
||||||
$COL_SAVED_SEARCH_ID INTEGER,
|
$COL_SAVED_SEARCH_ID INTEGER,
|
||||||
|
$COL_GLOBAL BOOLEAN NOT NULL,
|
||||||
FOREIGN KEY($COL_SAVED_SEARCH_ID) REFERENCES ${SavedSearchTable.TABLE} (${SavedSearchTable.COL_ID})
|
FOREIGN KEY($COL_SAVED_SEARCH_ID) REFERENCES ${SavedSearchTable.TABLE} (${SavedSearchTable.COL_ID})
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)"""
|
)"""
|
||||||
|
@ -366,6 +366,7 @@
|
|||||||
<string name="feed_delete">Delete feed item?</string>
|
<string name="feed_delete">Delete feed item?</string>
|
||||||
<string name="too_many_in_feed">Too many sources in your feed, cannot add more then 10</string>
|
<string name="too_many_in_feed">Too many sources in your feed, cannot add more then 10</string>
|
||||||
<string name="feed_tab_empty">You don\'t have any sources in your feed, go to the sources tab and long press a source to watch it</string>
|
<string name="feed_tab_empty">You don\'t have any sources in your feed, go to the sources tab and long press a source to watch it</string>
|
||||||
|
<string name="feed_add">Add %1$s to feed?</string>
|
||||||
|
|
||||||
<!-- Sort by tags -->
|
<!-- Sort by tags -->
|
||||||
<string name="pref_tag_sorting">Tag sorting tags</string>
|
<string name="pref_tag_sorting">Tag sorting tags</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user