Replace Latest tab with Feed
This commit is contained in:
parent
5d330c4f75
commit
6a41d96ddf
@ -36,8 +36,11 @@ import exh.metadata.sql.models.SearchTitle
|
||||
import exh.metadata.sql.queries.SearchMetadataQueries
|
||||
import exh.metadata.sql.queries.SearchTagQueries
|
||||
import exh.metadata.sql.queries.SearchTitleQueries
|
||||
import exh.savedsearches.mappers.FeedSavedSearchTypeMapping
|
||||
import exh.savedsearches.mappers.SavedSearchTypeMapping
|
||||
import exh.savedsearches.models.FeedSavedSearch
|
||||
import exh.savedsearches.models.SavedSearch
|
||||
import exh.savedsearches.queries.FeedSavedSearchQueries
|
||||
import exh.savedsearches.queries.SavedSearchQueries
|
||||
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||
|
||||
@ -45,7 +48,21 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
open class DatabaseHelper(context: Context) :
|
||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, FavoriteEntryQueries, SavedSearchQueries /* SY <-- */ {
|
||||
MangaQueries,
|
||||
ChapterQueries,
|
||||
TrackQueries,
|
||||
CategoryQueries,
|
||||
MangaCategoryQueries,
|
||||
HistoryQueries
|
||||
/* SY --> */,
|
||||
SearchMetadataQueries,
|
||||
SearchTagQueries,
|
||||
SearchTitleQueries,
|
||||
MergedQueries,
|
||||
FavoriteEntryQueries,
|
||||
SavedSearchQueries,
|
||||
FeedSavedSearchQueries
|
||||
/* SY <-- */ {
|
||||
|
||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||
.name(DbOpenCallback.DATABASE_NAME)
|
||||
@ -67,6 +84,7 @@ open class DatabaseHelper(context: Context) :
|
||||
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||
.addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping())
|
||||
.addTypeMapping(SavedSearch::class.java, SavedSearchTypeMapping())
|
||||
.addTypeMapping(FeedSavedSearch::class.java, FeedSavedSearchTypeMapping())
|
||||
// SY <--
|
||||
.build()
|
||||
|
||||
|
@ -13,6 +13,7 @@ import exh.merged.sql.tables.MergedTable
|
||||
import exh.metadata.sql.tables.SearchMetadataTable
|
||||
import exh.metadata.sql.tables.SearchTagTable
|
||||
import exh.metadata.sql.tables.SearchTitleTable
|
||||
import exh.savedsearches.tables.FeedSavedSearchTable
|
||||
import exh.savedsearches.tables.SavedSearchTable
|
||||
|
||||
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
@ -43,6 +44,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
execSQL(MergedTable.createTableQuery)
|
||||
execSQL(FavoriteEntryTable.createTableQuery)
|
||||
execSQL(SavedSearchTable.createTableQuery)
|
||||
execSQL(FeedSavedSearchTable.createTableQuery)
|
||||
// SY <--
|
||||
|
||||
// DB indexes
|
||||
@ -59,6 +61,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||
execSQL(MergedTable.createIndexQuery)
|
||||
execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@ -105,6 +108,8 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
}
|
||||
if (oldVersion < 13) {
|
||||
db.execSQL(SavedSearchTable.createTableQuery)
|
||||
db.execSQL(FeedSavedSearchTable.createTableQuery)
|
||||
db.execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||
import exh.savedsearches.tables.FeedSavedSearchTable
|
||||
import exh.savedsearches.tables.SavedSearchTable
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||
@ -74,6 +76,19 @@ fun getReadMangaNotInLibraryQuery() =
|
||||
)
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the manga merged into a merged manga
|
||||
*/
|
||||
fun getFeedSavedSearchQuery() =
|
||||
"""
|
||||
SELECT ${SavedSearchTable.TABLE}.*
|
||||
FROM (
|
||||
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE}
|
||||
) AS M
|
||||
JOIN ${SavedSearchTable.TABLE}
|
||||
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the manga from the library, with their categories, read and unread count.
|
||||
*/
|
||||
|
@ -439,9 +439,7 @@ class PreferencesHelper(val context: Context) {
|
||||
"false,false,false,false,false,false,false,false,false,false"
|
||||
)
|
||||
|
||||
fun latestTabSources() = flowPrefs.getStringSet("latest_tab_sources", mutableSetOf())
|
||||
|
||||
fun latestTabInFront() = flowPrefs.getBoolean("latest_tab_position", false)
|
||||
fun feedTabInFront() = flowPrefs.getBoolean("latest_tab_position", false)
|
||||
|
||||
fun sourcesTabCategories() = flowPrefs.getStringSet("sources_tab_categories", mutableSetOf())
|
||||
|
||||
|
@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController
|
||||
import eu.kanade.tachiyomi.ui.browse.latest.LatestController
|
||||
import eu.kanade.tachiyomi.ui.browse.feed.FeedController
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
@ -116,9 +116,9 @@ class BrowseController :
|
||||
|
||||
// SY -->
|
||||
private val tabTitles = (
|
||||
if (preferences.latestTabInFront().get()) {
|
||||
if (preferences.feedTabInFront().get()) {
|
||||
listOf(
|
||||
R.string.latest,
|
||||
R.string.feed,
|
||||
R.string.label_sources,
|
||||
R.string.label_extensions,
|
||||
R.string.label_migration
|
||||
@ -127,7 +127,7 @@ class BrowseController :
|
||||
} else {
|
||||
listOf(
|
||||
R.string.label_sources,
|
||||
R.string.latest,
|
||||
R.string.feed,
|
||||
R.string.label_extensions,
|
||||
R.string.label_migration
|
||||
)
|
||||
@ -144,8 +144,8 @@ class BrowseController :
|
||||
if (!router.hasRootController()) {
|
||||
val controller: Controller = when (position) {
|
||||
// SY -->
|
||||
SOURCES_CONTROLLER -> if (preferences.latestTabInFront().get()) LatestController() else SourceController()
|
||||
LATEST_CONTROLLER -> if (!preferences.latestTabInFront().get()) LatestController() else SourceController()
|
||||
SOURCES_CONTROLLER -> if (preferences.feedTabInFront().get()) FeedController() else SourceController()
|
||||
FEED_CONTROLLER -> if (!preferences.feedTabInFront().get()) FeedController() else SourceController()
|
||||
// SY <--
|
||||
EXTENSIONS_CONTROLLER -> ExtensionController()
|
||||
MIGRATION_CONTROLLER -> MigrationSourcesController()
|
||||
@ -166,7 +166,7 @@ class BrowseController :
|
||||
const val SOURCES_CONTROLLER = 0
|
||||
|
||||
// SY -->
|
||||
const val LATEST_CONTROLLER = 1
|
||||
const val FEED_CONTROLLER = 1
|
||||
const val EXTENSIONS_CONTROLLER = 2
|
||||
const val MIGRATION_CONTROLLER = 3
|
||||
// SY <--
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
package eu.kanade.tachiyomi.ui.browse.feed
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
@ -6,16 +6,18 @@ import android.util.SparseArray
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import exh.savedsearches.models.FeedSavedSearch
|
||||
import exh.savedsearches.models.SavedSearch
|
||||
|
||||
/**
|
||||
* Adapter that holds the search cards.
|
||||
*
|
||||
* @param controller instance of [LatestController].
|
||||
* @param controller instance of [FeedController].
|
||||
*/
|
||||
class LatestAdapter(val controller: LatestController) :
|
||||
FlexibleAdapter<LatestItem>(null, controller, true) {
|
||||
class FeedAdapter(val controller: FeedController) :
|
||||
FlexibleAdapter<FeedItem>(null, controller, true) {
|
||||
|
||||
val titleClickListener: OnTitleClickListener = controller
|
||||
val feedClickListener: OnFeedClickListener = controller
|
||||
|
||||
/**
|
||||
* Bundle where the view state of the holders is saved.
|
||||
@ -71,8 +73,10 @@ class LatestAdapter(val controller: LatestController) :
|
||||
}
|
||||
}
|
||||
|
||||
interface OnTitleClickListener {
|
||||
fun onTitleClick(source: CatalogueSource)
|
||||
interface OnFeedClickListener {
|
||||
fun onSourceClick(source: CatalogueSource)
|
||||
fun onSavedSearchClick(savedSearch: SavedSearch, source: CatalogueSource)
|
||||
fun onRemoveClick(feedSavedSearch: FeedSavedSearch)
|
||||
}
|
||||
|
||||
private companion object {
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
package eu.kanade.tachiyomi.ui.browse.feed
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@ -6,10 +6,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
/**
|
||||
* Adapter that holds the manga items from search results.
|
||||
*
|
||||
* @param controller instance of [LatestController].
|
||||
* @param controller instance of [FeedController].
|
||||
*/
|
||||
class LatestCardAdapter(controller: LatestController) :
|
||||
FlexibleAdapter<LatestCardItem>(null, controller, true) {
|
||||
class FeedCardAdapter(controller: FeedController) :
|
||||
FlexibleAdapter<FeedCardItem>(null, controller, true) {
|
||||
|
||||
/**
|
||||
* Listen for browse item clicks.
|
||||
@ -18,7 +18,7 @@ class LatestCardAdapter(controller: LatestController) :
|
||||
|
||||
/**
|
||||
* Listener which should be called when user clicks browse.
|
||||
* Note: Should only be handled by [LatestController]
|
||||
* Note: Should only be handled by [FeedController]
|
||||
*/
|
||||
interface OnMangaClickListener {
|
||||
fun onMangaClick(manga: Manga)
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
package eu.kanade.tachiyomi.ui.browse.feed
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding
|
||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
||||
|
||||
class LatestCardHolder(view: View, adapter: LatestCardAdapter) :
|
||||
class FeedCardHolder(view: View, adapter: FeedCardAdapter) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = GlobalSearchControllerCardItemBinding.bind(view)
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
package eu.kanade.tachiyomi.ui.browse.feed
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@ -7,21 +7,20 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class LatestCardItem(val manga: Manga) : AbstractFlexibleItem<LatestCardHolder>() {
|
||||
class FeedCardItem(val manga: Manga) : AbstractFlexibleItem<FeedCardHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.global_search_controller_card_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LatestCardHolder {
|
||||
return LatestCardHolder(view, adapter as LatestCardAdapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): FeedCardHolder {
|
||||
return FeedCardHolder(view, adapter as FeedCardAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: LatestCardHolder,
|
||||
holder: FeedCardHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
@ -29,7 +28,7 @@ class LatestCardItem(val manga: Manga) : AbstractFlexibleItem<LatestCardHolder>(
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LatestCardItem) {
|
||||
if (other is FeedCardItem) {
|
||||
return manga.id == other.manga.id
|
||||
}
|
||||
return false
|
@ -0,0 +1,230 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.feed
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
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
|
||||
|
||||
/**
|
||||
* 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 [FeedPresenter]
|
||||
* [FeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class FeedController :
|
||||
NucleusController<LatestControllerBinding, FeedPresenter>(),
|
||||
FeedCardAdapter.OnMangaClickListener,
|
||||
FeedAdapter.OnFeedClickListener {
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter containing search results grouped by lang.
|
||||
*/
|
||||
protected var adapter: FeedAdapter? = null
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.feed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [FeedPresenter] used in controller.
|
||||
*
|
||||
* @return instance of [FeedPresenter]
|
||||
*/
|
||||
override fun createPresenter(): FeedPresenter {
|
||||
return FeedPresenter()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.feed, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_add_feed -> addFeed()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun addFeed() {
|
||||
if (presenter.hasTooManyFeeds()) {
|
||||
activity?.toast(R.string.too_many_in_feed)
|
||||
return
|
||||
}
|
||||
val items = presenter.getEnabledSources()
|
||||
val itemsStrings = items.map { it.toString() }
|
||||
var selectedIndex = 0
|
||||
|
||||
MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.feed)
|
||||
.setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
addFeedSearch(items[selectedIndex])
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun addFeedSearch(source: CatalogueSource) {
|
||||
val items = presenter.getSourceSavedSearches(source)
|
||||
val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name }
|
||||
var selectedIndex = 0
|
||||
|
||||
MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.feed)
|
||||
.setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which ->
|
||||
selectedIndex = which
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
presenter.createFeed(source, items.getOrNull(selectedIndex - 1))
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = FeedAdapter(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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view holder for the given manga.
|
||||
*
|
||||
* @param source used to find holder containing source
|
||||
* @return the holder of the manga or null if it's not bound.
|
||||
*/
|
||||
private fun getHolder(source: CatalogueSource): FeedHolder? {
|
||||
val adapter = adapter ?: return null
|
||||
|
||||
adapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = adapter.getItem(holder.bindingAdapterPosition)
|
||||
if (item != null && source.id == item.feed.id) {
|
||||
return holder as FeedHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Add search result to adapter.
|
||||
*
|
||||
* @param feedManga the source items containing the latest manga.
|
||||
*/
|
||||
fun setItems(feedManga: List<FeedItem>) {
|
||||
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(source: CatalogueSource, manga: Manga) {
|
||||
getHolder(source)?.setImage(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a catalogue with the given search.
|
||||
*/
|
||||
override fun onSourceClick(source: CatalogueSource) {
|
||||
presenter.preferences.lastUsedSource().set(source.id)
|
||||
parentController?.router?.pushController(LatestUpdatesController(source).withFadeTransaction())
|
||||
}
|
||||
|
||||
override fun onSavedSearchClick(savedSearch: SavedSearch, source: CatalogueSource) {
|
||||
presenter.preferences.lastUsedSource().set(savedSearch.source)
|
||||
parentController?.router?.pushController(BrowseSourceController(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()
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
package eu.kanade.tachiyomi.ui.browse.feed
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@ -9,12 +10,12 @@ import eu.kanade.tachiyomi.databinding.LatestControllerCardBinding
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
/**
|
||||
* Holder that binds the [LatestItem] containing catalogue cards.
|
||||
* Holder that binds the [FeedItem] containing catalogue cards.
|
||||
*
|
||||
* @param view view of [LatestItem]
|
||||
* @param adapter instance of [LatestAdapter]
|
||||
* @param view view of [FeedItem]
|
||||
* @param adapter instance of [FeedAdapter]
|
||||
*/
|
||||
class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
class FeedHolder(view: View, val adapter: FeedAdapter) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = LatestControllerCardBinding.bind(view)
|
||||
@ -22,9 +23,9 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
/**
|
||||
* Adapter containing manga from search results.
|
||||
*/
|
||||
private val mangaAdapter = LatestCardAdapter(adapter.controller)
|
||||
private val mangaAdapter = FeedCardAdapter(adapter.controller)
|
||||
|
||||
private var lastBoundResults: List<LatestCardItem>? = null
|
||||
private var lastBoundResults: List<FeedCardItem>? = null
|
||||
|
||||
init {
|
||||
// Set layout horizontal.
|
||||
@ -33,9 +34,19 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
|
||||
binding.titleWrapper.setOnClickListener {
|
||||
adapter.getItem(bindingAdapterPosition)?.let {
|
||||
adapter.titleClickListener.onTitleClick(it.source)
|
||||
if (it.savedSearch != null) {
|
||||
adapter.feedClickListener.onSavedSearchClick(it.savedSearch, it.source ?: return@let)
|
||||
} else {
|
||||
adapter.feedClickListener.onSourceClick(it.source ?: return@let)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.titleWrapper.setOnLongClickListener {
|
||||
adapter.getItem(bindingAdapterPosition)?.let {
|
||||
adapter.feedClickListener.onRemoveClick(it.feed)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,15 +54,23 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
*
|
||||
* @param item item of card.
|
||||
*/
|
||||
fun bind(item: LatestItem) {
|
||||
val source = item.source
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(item: FeedItem) {
|
||||
val results = item.results
|
||||
|
||||
val titlePrefix = if (item.highlighted) "▶ " else ""
|
||||
|
||||
binding.title.text = titlePrefix + source.name
|
||||
binding.title.text = titlePrefix + if (item.savedSearch != null) {
|
||||
item.savedSearch.name
|
||||
} else {
|
||||
item.source?.name ?: item.feed.source.toString()
|
||||
}
|
||||
binding.subtitle.isVisible = true
|
||||
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
|
||||
binding.subtitle.text = if (item.savedSearch != null) {
|
||||
item.source?.name ?: item.feed.source.toString()
|
||||
} else {
|
||||
LocaleHelper.getDisplayName(item.source?.lang)
|
||||
}
|
||||
|
||||
when {
|
||||
results == null -> {
|
||||
@ -88,11 +107,11 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
* @param manga the manga to find.
|
||||
* @return the holder of the manga or null if it's not bound.
|
||||
*/
|
||||
private fun getHolder(manga: Manga): LatestCardHolder? {
|
||||
private fun getHolder(manga: Manga): FeedCardHolder? {
|
||||
mangaAdapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = mangaAdapter.getItem(holder.bindingAdapterPosition)
|
||||
if (item != null && item.manga.id!! == manga.id!!) {
|
||||
return holder as LatestCardHolder
|
||||
return holder as FeedCardHolder
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
package eu.kanade.tachiyomi.ui.browse.feed
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@ -8,16 +8,23 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
|
||||
import exh.savedsearches.models.FeedSavedSearch
|
||||
import exh.savedsearches.models.SavedSearch
|
||||
|
||||
/**
|
||||
* Item that contains search result information.
|
||||
*
|
||||
* @param source the source for the search results.
|
||||
* @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 LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?, val highlighted: Boolean = false) :
|
||||
AbstractFlexibleItem<LatestHolder>() {
|
||||
class FeedItem(
|
||||
val feed: FeedSavedSearch,
|
||||
val savedSearch: SavedSearch?,
|
||||
val source: CatalogueSource?,
|
||||
val results: List<FeedCardItem>?,
|
||||
val highlighted: Boolean = false
|
||||
) : AbstractFlexibleItem<FeedHolder>() {
|
||||
|
||||
/**
|
||||
* Set view.
|
||||
@ -29,12 +36,12 @@ class LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?
|
||||
}
|
||||
|
||||
/**
|
||||
* Create view holder (see [LatestAdapter].
|
||||
* Create view holder (see [FeedAdapter].
|
||||
*
|
||||
* @return holder of view.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LatestHolder {
|
||||
return LatestHolder(view, adapter as LatestAdapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): FeedHolder {
|
||||
return FeedHolder(view, adapter as FeedAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,7 +49,7 @@ class LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: LatestHolder,
|
||||
holder: FeedHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
@ -56,7 +63,7 @@ class LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is GlobalSearchItem) {
|
||||
return source.id == other.source.id
|
||||
return feed.id == other.source.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -67,6 +74,6 @@ class LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?
|
||||
* @return hashcode
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
return source.id.toInt()
|
||||
return feed.id!!.toInt()
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
package eu.kanade.tachiyomi.ui.browse.feed
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
@ -7,12 +8,18 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.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.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import exh.savedsearches.models.FeedSavedSearch
|
||||
import exh.savedsearches.models.SavedSearch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
@ -21,20 +28,21 @@ 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
|
||||
|
||||
/**
|
||||
* Presenter of [LatestController]
|
||||
* Presenter of [FeedController]
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*
|
||||
* @param sourceManager manages the different sources.
|
||||
* @param db manages the database calls.
|
||||
* @param preferences manages the preference calls.
|
||||
*/
|
||||
open class LatestPresenter(
|
||||
open class FeedPresenter(
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<LatestController>() {
|
||||
) : BasePresenter<FeedController>() {
|
||||
|
||||
/**
|
||||
* Fetches the different sources by user settings.
|
||||
@ -51,71 +59,128 @@ open class LatestPresenter(
|
||||
*/
|
||||
private var fetchImageSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
db.getFeedSavedSearches()
|
||||
.asRxObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEach {
|
||||
getFeed()
|
||||
}
|
||||
.subscribe()
|
||||
.let(::add)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchImageSubscription?.unsubscribe()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of enabled sources ordered by language and name, with pinned catalogues
|
||||
* prioritized.
|
||||
*
|
||||
* @return list containing enabled sources.
|
||||
*/
|
||||
protected open fun getEnabledSources(): List<CatalogueSource> {
|
||||
fun hasTooManyFeeds(): Boolean {
|
||||
return db.getFeedSavedSearches().executeAsBlocking().size > 10
|
||||
}
|
||||
|
||||
fun getEnabledSources(): List<CatalogueSource> {
|
||||
val languages = preferences.enabledLanguages().get()
|
||||
val watchedSources = preferences.latestTabSources().get()
|
||||
val pinnedSources = preferences.pinnedSources().get()
|
||||
|
||||
val list = sourceManager.getVisibleCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||
|
||||
return list.filter { it.id.toString() in watchedSources }
|
||||
.sortedBy { it.id.toString() !in pinnedSources }
|
||||
return list.sortedBy { it.id.toString() !in pinnedSources }
|
||||
}
|
||||
|
||||
private fun getSourcesToGetLatest(): List<CatalogueSource> {
|
||||
return getEnabledSources()
|
||||
fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> {
|
||||
return db.getSavedSearches(source.id).executeAsBlocking()
|
||||
}
|
||||
|
||||
fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) {
|
||||
launchIO {
|
||||
db.insertFeedSavedSearch(
|
||||
FeedSavedSearch(
|
||||
id = null,
|
||||
source = source.id,
|
||||
savedSearch = savedSearch?.id
|
||||
)
|
||||
).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFeed(feed: FeedSavedSearch) {
|
||||
launchIO {
|
||||
db.deleteFeedSavedSearch(feed).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> {
|
||||
val savedSearches = db.getSavedSearchesFeed().executeAsBlocking()
|
||||
.associateBy { it.id!! }
|
||||
return db.getFeedSavedSearches().executeAsBlocking()
|
||||
.map { it to savedSearches[it.savedSearch] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a catalogue search item
|
||||
*/
|
||||
protected open fun createCatalogueSearchItem(source: CatalogueSource, results: List<LatestCardItem>?): LatestItem {
|
||||
return LatestItem(source, results)
|
||||
protected open fun createCatalogueSearchItem(
|
||||
feed: FeedSavedSearch,
|
||||
savedSearch: SavedSearch?,
|
||||
source: CatalogueSource?,
|
||||
results: List<FeedCardItem>?
|
||||
): FeedItem {
|
||||
return FeedItem(feed, savedSearch, source, results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates get latest per watching source.
|
||||
* Initiates get manga per feed.
|
||||
*/
|
||||
fun getLatest() {
|
||||
fun getFeed() {
|
||||
// Create image fetch subscription
|
||||
initializeFetchImageSubscription()
|
||||
|
||||
// Create items with the initial state
|
||||
val initialItems = getSourcesToGetLatest().map { createCatalogueSearchItem(it, null) }
|
||||
val initialItems = getSourcesToGetFeed().map { (feed, savedSearch) ->
|
||||
createCatalogueSearchItem(
|
||||
feed,
|
||||
savedSearch,
|
||||
sourceManager.get(feed.source) as? CatalogueSource,
|
||||
null
|
||||
)
|
||||
}
|
||||
var items = initialItems
|
||||
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchSourcesSubscription = Observable.from(getSourcesToGetLatest())
|
||||
fetchSourcesSubscription = Observable.from(getSourcesToGetFeed())
|
||||
.flatMap(
|
||||
{ source ->
|
||||
Observable.defer { source.fetchLatestUpdates(1) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
|
||||
.map { it.mangas.take(10) } // Get at most 10 manga from search result.
|
||||
.map { list -> list.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
||||
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
||||
.map { list -> createCatalogueSearchItem(source, list.map { LatestCardItem(it) }) }
|
||||
{ (feed, savedSearch) ->
|
||||
val source = sourceManager.get(feed.source) as? CatalogueSource
|
||||
if (source != null) {
|
||||
Observable.defer {
|
||||
if (savedSearch == null) {
|
||||
source.fetchLatestUpdates(1)
|
||||
} else {
|
||||
source.fetchSearchManga(1, savedSearch.query.orEmpty(), getFilterList(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) } // Load manga covers.
|
||||
.map { list -> createCatalogueSearchItem(feed, savedSearch, source, list.map { FeedCardItem(it) }) }
|
||||
} else {
|
||||
Observable.just(createCatalogueSearchItem(feed, null, null, emptyList()))
|
||||
}
|
||||
},
|
||||
5
|
||||
)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Update matching source with the obtained results
|
||||
.map { result ->
|
||||
items.map { item -> if (item.source == result.source) result else item }
|
||||
items.map { item -> if (item.feed == result.feed) result else item }
|
||||
}
|
||||
// Update current state
|
||||
.doOnNext { items = it }
|
||||
@ -131,6 +196,20 @@ open class LatestPresenter(
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
@ -1,159 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
||||
/**
|
||||
* This controller shows and manages the different search result in global search.
|
||||
* This controller should only handle UI actions, IO actions should be done by [LatestPresenter]
|
||||
* [LatestCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class LatestController :
|
||||
NucleusController<LatestControllerBinding, LatestPresenter>(),
|
||||
LatestCardAdapter.OnMangaClickListener,
|
||||
LatestAdapter.OnTitleClickListener {
|
||||
|
||||
/**
|
||||
* Adapter containing search results grouped by lang.
|
||||
*/
|
||||
protected var adapter: LatestAdapter? = null
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.latest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [LatestPresenter] used in controller.
|
||||
*
|
||||
* @return instance of [LatestPresenter]
|
||||
*/
|
||||
override fun createPresenter(): LatestPresenter {
|
||||
return LatestPresenter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when manga in global search is clicked, opens manga.
|
||||
*
|
||||
* @param manga clicked item containing manga information.
|
||||
*/
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
// Open MangaController.
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = LatestAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
presenter.preferences.latestTabSources()
|
||||
.asImmediateFlow { presenter.getLatest() }
|
||||
.launchIn(viewScope)
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onSaveViewState(view: View, outState: Bundle) {
|
||||
super.onSaveViewState(view, outState)
|
||||
adapter?.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
|
||||
super.onRestoreViewState(view, savedViewState)
|
||||
adapter?.onRestoreInstanceState(savedViewState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view holder for the given manga.
|
||||
*
|
||||
* @param source used to find holder containing source
|
||||
* @return the holder of the manga or null if it's not bound.
|
||||
*/
|
||||
private fun getHolder(source: CatalogueSource): LatestHolder? {
|
||||
val adapter = adapter ?: return null
|
||||
|
||||
adapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = adapter.getItem(holder.bindingAdapterPosition)
|
||||
if (item != null && source.id == item.source.id) {
|
||||
return holder as LatestHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Add search result to adapter.
|
||||
*
|
||||
* @param latestManga the source items containing the latest manga.
|
||||
*/
|
||||
fun setItems(latestManga: List<LatestItem>) {
|
||||
adapter?.updateDataSet(latestManga)
|
||||
|
||||
if (latestManga.isEmpty()) {
|
||||
binding.emptyView.show(R.string.latest_tab_empty)
|
||||
} else {
|
||||
binding.emptyView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the initialized manga.
|
||||
*/
|
||||
fun onMangaInitialized(source: CatalogueSource, manga: Manga) {
|
||||
getHolder(source)?.setImage(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a catalogue with the given search.
|
||||
*/
|
||||
override fun onTitleClick(source: CatalogueSource) {
|
||||
presenter.preferences.lastUsedSource().set(source.id)
|
||||
parentController?.router?.pushController(LatestUpdatesController(source).withFadeTransaction())
|
||||
}
|
||||
}
|
@ -175,16 +175,6 @@ class SourceController(bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
// SY -->
|
||||
val isWatched = item.source.id.toString() in preferences.latestTabSources().get()
|
||||
|
||||
if (item.source.supportsLatest) {
|
||||
items.add(
|
||||
activity.getString(if (isWatched) R.string.unwatch else R.string.watch) to {
|
||||
watchCatalogue(item.source, isWatched)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
items.add(
|
||||
activity.getString(R.string.categories) to { addToCategories(item.source) }
|
||||
)
|
||||
@ -222,18 +212,6 @@ class SourceController(bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun watchCatalogue(source: Source, isWatched: Boolean) {
|
||||
if (isWatched) {
|
||||
preferences.latestTabSources() -= source.id.toString()
|
||||
} else {
|
||||
if (preferences.latestTabSources().get().size + 1 !in 0..5) {
|
||||
applicationContext?.toast(R.string.too_many_watched)
|
||||
return
|
||||
}
|
||||
preferences.latestTabSources() += source.id.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToCategories(source: Source) {
|
||||
val categories = preferences.sourcesTabCategories().get()
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it }))
|
||||
|
@ -7,7 +7,6 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.browse.latest.LatestCardItem
|
||||
|
||||
class IndexCardItem(val manga: Manga) : AbstractFlexibleItem<IndexCardHolder>() {
|
||||
|
||||
@ -29,7 +28,7 @@ class IndexCardItem(val manga: Manga) : AbstractFlexibleItem<IndexCardHolder>()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LatestCardItem) {
|
||||
if (other is IndexCardItem) {
|
||||
return manga.id == other.manga.id
|
||||
}
|
||||
return false
|
||||
|
@ -59,12 +59,12 @@ class SettingsBrowseController : SettingsController() {
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
titleRes = R.string.latest
|
||||
titleRes = R.string.feed
|
||||
|
||||
switchPreference {
|
||||
bindTo(preferences.latestTabInFront())
|
||||
titleRes = R.string.pref_latest_position
|
||||
summaryRes = R.string.pref_latest_position_summery
|
||||
bindTo(preferences.feedTabInFront())
|
||||
titleRes = R.string.pref_feed_position
|
||||
summaryRes = R.string.pref_feed_position_summery
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
@ -40,6 +40,7 @@ import exh.eh.EHentaiUpdateWorker
|
||||
import exh.log.xLogE
|
||||
import exh.log.xLogW
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.savedsearches.models.FeedSavedSearch
|
||||
import exh.savedsearches.models.SavedSearch
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.EH_SOURCE_ID
|
||||
@ -416,10 +417,21 @@ object EXHMigrations {
|
||||
}.getOrNull()
|
||||
}?.ifEmpty { null }
|
||||
if (savedSearches != null) {
|
||||
db.insertSavedSearches(savedSearches)
|
||||
db.insertSavedSearches(savedSearches).executeAsBlocking()
|
||||
}
|
||||
val feed = prefs.getStringSet("latest_tab_sources", emptySet())?.map {
|
||||
FeedSavedSearch(
|
||||
id = null,
|
||||
source = it.toLong(),
|
||||
savedSearch = null
|
||||
)
|
||||
}?.ifEmpty { null }
|
||||
if (feed != null) {
|
||||
db.insertFeedSavedSearches(feed).executeAsBlocking()
|
||||
}
|
||||
prefs.edit(commit = true) {
|
||||
remove("eh_saved_searches")
|
||||
remove("latest_tab_sources")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -373,8 +373,4 @@ object DebugFunctions {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun unwatchAllSources() {
|
||||
prefs.latestTabSources().delete()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
package exh.savedsearches.mappers
|
||||
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.core.database.getLongOrNull
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import exh.savedsearches.models.FeedSavedSearch
|
||||
import exh.savedsearches.tables.FeedSavedSearchTable.COL_ID
|
||||
import exh.savedsearches.tables.FeedSavedSearchTable.COL_SAVED_SEARCH_ID
|
||||
import exh.savedsearches.tables.FeedSavedSearchTable.COL_SOURCE
|
||||
import exh.savedsearches.tables.FeedSavedSearchTable.TABLE
|
||||
|
||||
class FeedSavedSearchTypeMapping : SQLiteTypeMapping<FeedSavedSearch>(
|
||||
FeedSavedSearchPutResolver(),
|
||||
FeedSavedSearchGetResolver(),
|
||||
FeedSavedSearchDeleteResolver()
|
||||
)
|
||||
|
||||
class FeedSavedSearchPutResolver : DefaultPutResolver<FeedSavedSearch>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: FeedSavedSearch) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: FeedSavedSearch) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: FeedSavedSearch) = contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_SOURCE to obj.source,
|
||||
COL_SAVED_SEARCH_ID to obj.savedSearch
|
||||
)
|
||||
}
|
||||
|
||||
class FeedSavedSearchGetResolver : DefaultGetResolver<FeedSavedSearch>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): FeedSavedSearch = FeedSavedSearch(
|
||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID)),
|
||||
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE)),
|
||||
savedSearch = cursor.getLongOrNull(cursor.getColumnIndexOrThrow(COL_SAVED_SEARCH_ID))
|
||||
)
|
||||
}
|
||||
|
||||
class FeedSavedSearchDeleteResolver : DefaultDeleteResolver<FeedSavedSearch>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: FeedSavedSearch) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package exh.savedsearches.models
|
||||
|
||||
data class FeedSavedSearch(
|
||||
// Tag identifier, unique
|
||||
var id: Long?,
|
||||
|
||||
// Source for the saved search
|
||||
var source: Long,
|
||||
|
||||
// If -1 then get latest, if set get the saved search
|
||||
var savedSearch: Long?
|
||||
)
|
@ -0,0 +1,84 @@
|
||||
package exh.savedsearches.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.queries.getFeedSavedSearchQuery
|
||||
import exh.savedsearches.models.FeedSavedSearch
|
||||
import exh.savedsearches.models.SavedSearch
|
||||
import exh.savedsearches.tables.FeedSavedSearchTable
|
||||
|
||||
interface FeedSavedSearchQueries : DbProvider {
|
||||
fun getFeedSavedSearches() = db.get()
|
||||
.listOfObjects(FeedSavedSearch::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(FeedSavedSearchTable.TABLE)
|
||||
.orderBy(FeedSavedSearchTable.COL_ID)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getFeedSavedSearch(id: 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)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(FeedSavedSearchTable.TABLE)
|
||||
.where("${FeedSavedSearchTable.COL_ID} IN (?)")
|
||||
.whereArgs(ids.joinToString())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertFeedSavedSearch(savedSearch: FeedSavedSearch) = db.put().`object`(savedSearch).prepare()
|
||||
|
||||
fun insertFeedSavedSearches(savedSearches: List<FeedSavedSearch>) = db.put().objects(savedSearches).prepare()
|
||||
|
||||
fun deleteFeedSavedSearch(savedSearch: FeedSavedSearch) = db.delete().`object`(savedSearch).prepare()
|
||||
|
||||
fun deleteFeedSavedSearch(id: Long) = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(FeedSavedSearchTable.TABLE)
|
||||
.where("${FeedSavedSearchTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build()
|
||||
).prepare()
|
||||
|
||||
fun deleteAllFeedSavedSearches() = db.delete().byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(FeedSavedSearchTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getSavedSearchesFeed() = db.get()
|
||||
.listOfObjects(SavedSearch::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getFeedSavedSearchQuery())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
/*fun setMangasForMergedManga(mergedMangaId: Long, mergedMangases: List<SavedSearch>) {
|
||||
db.inTransaction {
|
||||
deleteSavedSearches(mergedMangaId).executeAsBlocking()
|
||||
mergedMangases.chunked(100) { chunk ->
|
||||
insertSavedSearches(chunk).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
@ -49,6 +49,17 @@ interface SavedSearchQueries : DbProvider {
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getSavedSearches(ids: List<Long>) = db.get()
|
||||
.listOfObjects(SavedSearch::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(SavedSearchTable.TABLE)
|
||||
.where("${SavedSearchTable.COL_ID} IN (?)")
|
||||
.whereArgs(ids.joinToString())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertSavedSearch(savedSearch: SavedSearch) = db.put().`object`(savedSearch).prepare()
|
||||
|
||||
fun insertSavedSearches(savedSearches: List<SavedSearch>) = db.put().objects(savedSearches).prepare()
|
||||
|
@ -0,0 +1,25 @@
|
||||
package exh.savedsearches.tables
|
||||
|
||||
object FeedSavedSearchTable {
|
||||
|
||||
const val TABLE = "feed_saved_search"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_SOURCE = "source"
|
||||
|
||||
const val COL_SAVED_SEARCH_ID = "saved_search"
|
||||
|
||||
val createTableQuery: String
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_SOURCE INTEGER NOT NULL,
|
||||
$COL_SAVED_SEARCH_ID INTEGER,
|
||||
FOREIGN KEY($COL_SAVED_SEARCH_ID) REFERENCES ${SavedSearchTable.TABLE} (${SavedSearchTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
)"""
|
||||
|
||||
val createSavedSearchIdIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_${COL_SAVED_SEARCH_ID}_index ON $TABLE($COL_SAVED_SEARCH_ID)"
|
||||
}
|
11
app/src/main/res/menu/feed.xml
Normal file
11
app/src/main/res/menu/feed.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_add_feed"
|
||||
android:icon="@drawable/ic_add_24dp"
|
||||
android:title="@string/action_add"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
@ -192,8 +192,8 @@
|
||||
<string name="library_group_updates_all">Launch category updates all the time</string>
|
||||
|
||||
<!-- Browse settings -->
|
||||
<string name="pref_latest_position">Latest tab position</string>
|
||||
<string name="pref_latest_position_summery">Do you want the latest tab to be the first tab in browse? This will make it the default tab when opening browse, not recommended if you\'re on data or a metered network</string>
|
||||
<string name="pref_feed_position">Feed tab position</string>
|
||||
<string name="pref_feed_position_summery">Do you want the feed tab to be the first tab in browse? This will make it the default tab when opening browse, not recommended if you\'re on data or a metered network</string>
|
||||
<string name="pref_source_source_filtering">Filter sources in categories</string>
|
||||
<string name="pref_source_source_filtering_summery">Filter the sources that are in categories, making the sources not get put under the language if they are in a category</string>
|
||||
<string name="pref_source_navigation">Replace latest button</string>
|
||||
@ -361,11 +361,11 @@
|
||||
<string name="no_source_categories">No source categories available</string>
|
||||
<string name="invalid_category_name">Invalid category name</string>
|
||||
|
||||
<!-- Latest Tab -->
|
||||
<string name="watch">Watch</string>
|
||||
<string name="unwatch">Unwatch</string>
|
||||
<string name="too_many_watched">Too many watched sources, cannot add more then 5</string>
|
||||
<string name="latest_tab_empty">You don\'t have any watched sources, go to the sources tab and long press a source to watch it</string>
|
||||
<!-- Feed Tab -->
|
||||
<string name="feed">Feed</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="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>
|
||||
|
||||
<!-- Sort by tags -->
|
||||
<string name="pref_tag_sorting">Tag sorting tags</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user