Replace Latest tab with Feed

This commit is contained in:
Jobobby04 2022-03-27 18:45:14 -04:00
parent 5d330c4f75
commit 6a41d96ddf
26 changed files with 689 additions and 286 deletions

View File

@ -36,8 +36,11 @@ import exh.metadata.sql.models.SearchTitle
import exh.metadata.sql.queries.SearchMetadataQueries import exh.metadata.sql.queries.SearchMetadataQueries
import exh.metadata.sql.queries.SearchTagQueries import exh.metadata.sql.queries.SearchTagQueries
import exh.metadata.sql.queries.SearchTitleQueries import exh.metadata.sql.queries.SearchTitleQueries
import exh.savedsearches.mappers.FeedSavedSearchTypeMapping
import exh.savedsearches.mappers.SavedSearchTypeMapping import exh.savedsearches.mappers.SavedSearchTypeMapping
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
import exh.savedsearches.queries.FeedSavedSearchQueries
import exh.savedsearches.queries.SavedSearchQueries import exh.savedsearches.queries.SavedSearchQueries
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory 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. * This class provides operations to manage the database through its interfaces.
*/ */
open class DatabaseHelper(context: Context) : 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) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
@ -67,6 +84,7 @@ open class DatabaseHelper(context: Context) :
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping()) .addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
.addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping()) .addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping())
.addTypeMapping(SavedSearch::class.java, SavedSearchTypeMapping()) .addTypeMapping(SavedSearch::class.java, SavedSearchTypeMapping())
.addTypeMapping(FeedSavedSearch::class.java, FeedSavedSearchTypeMapping())
// SY <-- // SY <--
.build() .build()

View File

@ -13,6 +13,7 @@ import exh.merged.sql.tables.MergedTable
import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable import exh.metadata.sql.tables.SearchTagTable
import exh.metadata.sql.tables.SearchTitleTable import exh.metadata.sql.tables.SearchTitleTable
import exh.savedsearches.tables.FeedSavedSearchTable
import exh.savedsearches.tables.SavedSearchTable import exh.savedsearches.tables.SavedSearchTable
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
@ -43,6 +44,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(MergedTable.createTableQuery) execSQL(MergedTable.createTableQuery)
execSQL(FavoriteEntryTable.createTableQuery) execSQL(FavoriteEntryTable.createTableQuery)
execSQL(SavedSearchTable.createTableQuery) execSQL(SavedSearchTable.createTableQuery)
execSQL(FeedSavedSearchTable.createTableQuery)
// SY <-- // SY <--
// DB indexes // DB indexes
@ -59,6 +61,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTitleTable.createMangaIdIndexQuery) execSQL(SearchTitleTable.createMangaIdIndexQuery)
execSQL(SearchTitleTable.createTitleIndexQuery) execSQL(SearchTitleTable.createTitleIndexQuery)
execSQL(MergedTable.createIndexQuery) execSQL(MergedTable.createIndexQuery)
execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
// SY <-- // SY <--
} }
@ -105,6 +108,8 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
} }
if (oldVersion < 13) { if (oldVersion < 13) {
db.execSQL(SavedSearchTable.createTableQuery) db.execSQL(SavedSearchTable.createTableQuery)
db.execSQL(FeedSavedSearchTable.createTableQuery)
db.execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
} }
} }

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.database.queries package eu.kanade.tachiyomi.data.database.queries
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver 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 exh.source.MERGED_SOURCE_ID
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter 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. * Query to get the manga from the library, with their categories, read and unread count.
*/ */

View File

@ -439,9 +439,7 @@ class PreferencesHelper(val context: Context) {
"false,false,false,false,false,false,false,false,false,false" "false,false,false,false,false,false,false,false,false,false"
) )
fun latestTabSources() = flowPrefs.getStringSet("latest_tab_sources", mutableSetOf()) fun feedTabInFront() = flowPrefs.getBoolean("latest_tab_position", false)
fun latestTabInFront() = flowPrefs.getBoolean("latest_tab_position", false)
fun sourcesTabCategories() = flowPrefs.getStringSet("sources_tab_categories", mutableSetOf()) fun sourcesTabCategories() = flowPrefs.getStringSet("sources_tab_categories", mutableSetOf())

View File

@ -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.RxController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController
import eu.kanade.tachiyomi.ui.browse.latest.LatestController import eu.kanade.tachiyomi.ui.browse.feed.FeedController
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
import eu.kanade.tachiyomi.ui.browse.source.SourceController import eu.kanade.tachiyomi.ui.browse.source.SourceController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
@ -116,9 +116,9 @@ class BrowseController :
// SY --> // SY -->
private val tabTitles = ( private val tabTitles = (
if (preferences.latestTabInFront().get()) { if (preferences.feedTabInFront().get()) {
listOf( listOf(
R.string.latest, R.string.feed,
R.string.label_sources, R.string.label_sources,
R.string.label_extensions, R.string.label_extensions,
R.string.label_migration R.string.label_migration
@ -127,7 +127,7 @@ class BrowseController :
} else { } else {
listOf( listOf(
R.string.label_sources, R.string.label_sources,
R.string.latest, R.string.feed,
R.string.label_extensions, R.string.label_extensions,
R.string.label_migration R.string.label_migration
) )
@ -144,8 +144,8 @@ class BrowseController :
if (!router.hasRootController()) { if (!router.hasRootController()) {
val controller: Controller = when (position) { val controller: Controller = when (position) {
// SY --> // SY -->
SOURCES_CONTROLLER -> if (preferences.latestTabInFront().get()) LatestController() else SourceController() SOURCES_CONTROLLER -> if (preferences.feedTabInFront().get()) FeedController() else SourceController()
LATEST_CONTROLLER -> if (!preferences.latestTabInFront().get()) LatestController() else SourceController() FEED_CONTROLLER -> if (!preferences.feedTabInFront().get()) FeedController() else SourceController()
// SY <-- // SY <--
EXTENSIONS_CONTROLLER -> ExtensionController() EXTENSIONS_CONTROLLER -> ExtensionController()
MIGRATION_CONTROLLER -> MigrationSourcesController() MIGRATION_CONTROLLER -> MigrationSourcesController()
@ -166,7 +166,7 @@ class BrowseController :
const val SOURCES_CONTROLLER = 0 const val SOURCES_CONTROLLER = 0
// SY --> // SY -->
const val LATEST_CONTROLLER = 1 const val FEED_CONTROLLER = 1
const val EXTENSIONS_CONTROLLER = 2 const val EXTENSIONS_CONTROLLER = 2
const val MIGRATION_CONTROLLER = 3 const val MIGRATION_CONTROLLER = 3
// SY <-- // SY <--

View File

@ -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.Bundle
import android.os.Parcelable import android.os.Parcelable
@ -6,16 +6,18 @@ import android.util.SparseArray
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
/** /**
* Adapter that holds the search cards. * Adapter that holds the search cards.
* *
* @param controller instance of [LatestController]. * @param controller instance of [FeedController].
*/ */
class LatestAdapter(val controller: LatestController) : class FeedAdapter(val controller: FeedController) :
FlexibleAdapter<LatestItem>(null, controller, true) { FlexibleAdapter<FeedItem>(null, controller, true) {
val titleClickListener: OnTitleClickListener = controller val feedClickListener: OnFeedClickListener = controller
/** /**
* Bundle where the view state of the holders is saved. * Bundle where the view state of the holders is saved.
@ -71,8 +73,10 @@ class LatestAdapter(val controller: LatestController) :
} }
} }
interface OnTitleClickListener { interface OnFeedClickListener {
fun onTitleClick(source: CatalogueSource) fun onSourceClick(source: CatalogueSource)
fun onSavedSearchClick(savedSearch: SavedSearch, source: CatalogueSource)
fun onRemoveClick(feedSavedSearch: FeedSavedSearch)
} }
private companion object { private companion object {

View File

@ -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.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga 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. * Adapter that holds the manga items from search results.
* *
* @param controller instance of [LatestController]. * @param controller instance of [FeedController].
*/ */
class LatestCardAdapter(controller: LatestController) : class FeedCardAdapter(controller: FeedController) :
FlexibleAdapter<LatestCardItem>(null, controller, true) { FlexibleAdapter<FeedCardItem>(null, controller, true) {
/** /**
* Listen for browse item clicks. * Listen for browse item clicks.
@ -18,7 +18,7 @@ class LatestCardAdapter(controller: LatestController) :
/** /**
* Listener which should be called when user clicks browse. * 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 { interface OnMangaClickListener {
fun onMangaClick(manga: Manga) fun onMangaClick(manga: Manga)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.browse.latest package eu.kanade.tachiyomi.ui.browse.feed
import android.view.View import android.view.View
import androidx.core.view.isVisible 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.databinding.GlobalSearchControllerCardItemBinding
import eu.kanade.tachiyomi.util.view.loadAutoPause import eu.kanade.tachiyomi.util.view.loadAutoPause
class LatestCardHolder(view: View, adapter: LatestCardAdapter) : class FeedCardHolder(view: View, adapter: FeedCardAdapter) :
FlexibleViewHolder(view, adapter) { FlexibleViewHolder(view, adapter) {
private val binding = GlobalSearchControllerCardItemBinding.bind(view) private val binding = GlobalSearchControllerCardItemBinding.bind(view)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.browse.latest package eu.kanade.tachiyomi.ui.browse.feed
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -7,21 +7,20 @@ 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.data.database.models.Manga 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 { override fun getLayoutRes(): Int {
return R.layout.global_search_controller_card_item return R.layout.global_search_controller_card_item
} }
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LatestCardHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): FeedCardHolder {
return LatestCardHolder(view, adapter as LatestCardAdapter) return FeedCardHolder(view, adapter as FeedCardAdapter)
} }
override fun bindViewHolder( override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LatestCardHolder, holder: FeedCardHolder,
position: Int, position: Int,
payloads: List<Any?>? payloads: List<Any?>?
) { ) {
@ -29,7 +28,7 @@ class LatestCardItem(val manga: Manga) : AbstractFlexibleItem<LatestCardHolder>(
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other is LatestCardItem) { if (other is FeedCardItem) {
return manga.id == other.manga.id return manga.id == other.manga.id
} }
return false return false

View File

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

View File

@ -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 android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -9,12 +10,12 @@ import eu.kanade.tachiyomi.databinding.LatestControllerCardBinding
import eu.kanade.tachiyomi.util.system.LocaleHelper 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 view view of [FeedItem]
* @param adapter instance of [LatestAdapter] * @param adapter instance of [FeedAdapter]
*/ */
class LatestHolder(view: View, val adapter: LatestAdapter) : class FeedHolder(view: View, val adapter: FeedAdapter) :
FlexibleViewHolder(view, adapter) { FlexibleViewHolder(view, adapter) {
private val binding = LatestControllerCardBinding.bind(view) private val binding = LatestControllerCardBinding.bind(view)
@ -22,9 +23,9 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
/** /**
* Adapter containing manga from search results. * 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 { init {
// Set layout horizontal. // Set layout horizontal.
@ -33,25 +34,43 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
binding.titleWrapper.setOnClickListener { binding.titleWrapper.setOnClickListener {
adapter.getItem(bindingAdapterPosition)?.let { 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
}
}
/** /**
* Show the loading of source search result. * Show the loading of source search result.
* *
* @param item item of card. * @param item item of card.
*/ */
fun bind(item: LatestItem) { @SuppressLint("SetTextI18n")
val source = item.source fun bind(item: FeedItem) {
val results = item.results val results = item.results
val titlePrefix = if (item.highlighted) "" else "" 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.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 { when {
results == null -> { results == null -> {
@ -88,11 +107,11 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
* @param manga the manga to find. * @param manga the manga to find.
* @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(manga: Manga): LatestCardHolder? { private fun getHolder(manga: Manga): FeedCardHolder? {
mangaAdapter.allBoundViewHolders.forEach { holder -> mangaAdapter.allBoundViewHolders.forEach { holder ->
val item = mangaAdapter.getItem(holder.bindingAdapterPosition) val item = mangaAdapter.getItem(holder.bindingAdapterPosition)
if (item != null && item.manga.id!! == manga.id!!) { if (item != null && item.manga.id!! == manga.id!!) {
return holder as LatestCardHolder return holder as FeedCardHolder
} }
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.browse.latest package eu.kanade.tachiyomi.ui.browse.feed
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -8,16 +8,23 @@ 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 eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
/** /**
* Item that contains search result information. * 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 results the search results.
* @param highlighted whether this search item should be highlighted/marked in the catalogue search view. * @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) : class FeedItem(
AbstractFlexibleItem<LatestHolder>() { val feed: FeedSavedSearch,
val savedSearch: SavedSearch?,
val source: CatalogueSource?,
val results: List<FeedCardItem>?,
val highlighted: Boolean = false
) : AbstractFlexibleItem<FeedHolder>() {
/** /**
* Set view. * 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. * @return holder of view.
*/ */
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LatestHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): FeedHolder {
return LatestHolder(view, adapter as LatestAdapter) return FeedHolder(view, adapter as FeedAdapter)
} }
/** /**
@ -42,7 +49,7 @@ class LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?
*/ */
override fun bindViewHolder( override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LatestHolder, holder: FeedHolder,
position: Int, position: Int,
payloads: List<Any?>? payloads: List<Any?>?
) { ) {
@ -56,7 +63,7 @@ class LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?
*/ */
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other is GlobalSearchItem) { if (other is GlobalSearchItem) {
return source.id == other.source.id return feed.id == other.source.id
} }
return false return false
} }
@ -67,6 +74,6 @@ class LatestItem(val source: CatalogueSource, val results: List<LatestCardItem>?
* @return hashcode * @return hashcode
*/ */
override fun hashCode(): Int { override fun hashCode(): Int {
return source.id.toInt() return feed.id!!.toInt()
} }
} }

View File

@ -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.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo 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.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager 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.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter 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.lang.runAsObservable
import eu.kanade.tachiyomi.util.system.logcat 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 logcat.LogPriority
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
@ -21,20 +28,21 @@ import rx.schedulers.Schedulers
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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. * Function calls should be done from here. UI calls should be done from the controller.
* *
* @param sourceManager manages the different sources. * @param sourceManager manages the different sources.
* @param db manages the database calls. * @param db manages the database calls.
* @param preferences manages the preference calls. * @param preferences manages the preference calls.
*/ */
open class LatestPresenter( open class FeedPresenter(
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get() val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<LatestController>() { ) : BasePresenter<FeedController>() {
/** /**
* Fetches the different sources by user settings. * Fetches the different sources by user settings.
@ -51,71 +59,128 @@ open class LatestPresenter(
*/ */
private var fetchImageSubscription: Subscription? = null 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() { override fun onDestroy() {
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchImageSubscription?.unsubscribe() fetchImageSubscription?.unsubscribe()
super.onDestroy() super.onDestroy()
} }
/** fun hasTooManyFeeds(): Boolean {
* Returns a list of enabled sources ordered by language and name, with pinned catalogues return db.getFeedSavedSearches().executeAsBlocking().size > 10
* prioritized. }
*
* @return list containing enabled sources. fun getEnabledSources(): List<CatalogueSource> {
*/
protected open fun getEnabledSources(): List<CatalogueSource> {
val languages = preferences.enabledLanguages().get() val languages = preferences.enabledLanguages().get()
val watchedSources = preferences.latestTabSources().get()
val pinnedSources = preferences.pinnedSources().get() val pinnedSources = preferences.pinnedSources().get()
val list = sourceManager.getVisibleCatalogueSources() val list = sourceManager.getVisibleCatalogueSources()
.filter { it.lang in languages } .filter { it.lang in languages }
.sortedBy { "(${it.lang}) ${it.name}" } .sortedBy { "(${it.lang}) ${it.name}" }
return list.filter { it.id.toString() in watchedSources } return list.sortedBy { it.id.toString() !in pinnedSources }
.sortedBy { it.id.toString() !in pinnedSources }
} }
private fun getSourcesToGetLatest(): List<CatalogueSource> { fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> {
return getEnabledSources() 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 * Creates a catalogue search item
*/ */
protected open fun createCatalogueSearchItem(source: CatalogueSource, results: List<LatestCardItem>?): LatestItem { protected open fun createCatalogueSearchItem(
return LatestItem(source, results) 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 // Create image fetch subscription
initializeFetchImageSubscription() initializeFetchImageSubscription()
// Create items with the initial state // 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 var items = initialItems
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchSourcesSubscription = Observable.from(getSourcesToGetLatest()) fetchSourcesSubscription = Observable.from(getSourcesToGetFeed())
.flatMap( .flatMap(
{ source -> { (feed, savedSearch) ->
Observable.defer { source.fetchLatestUpdates(1) } 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()) .subscribeOn(Schedulers.io())
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions .onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
.map { it.mangas.take(10) } // Get at most 10 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) } // Load manga covers.
.map { list -> createCatalogueSearchItem(source, list.map { LatestCardItem(it) }) } .map { list -> createCatalogueSearchItem(feed, savedSearch, source, list.map { FeedCardItem(it) }) }
} else {
Observable.just(createCatalogueSearchItem(feed, null, null, emptyList()))
}
}, },
5 5
) )
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
// Update matching source with the obtained results // Update matching source with the obtained results
.map { result -> .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 // Update current state
.doOnNext { items = it } .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. * Initialize a list of manga.
* *

View File

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

View File

@ -175,16 +175,6 @@ class SourceController(bundle: Bundle? = null) :
} }
// SY --> // 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( items.add(
activity.getString(R.string.categories) to { addToCategories(item.source) } activity.getString(R.string.categories) to { addToCategories(item.source) }
) )
@ -222,18 +212,6 @@ class SourceController(bundle: Bundle? = null) :
} }
// SY --> // 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) { private fun addToCategories(source: Source) {
val categories = preferences.sourcesTabCategories().get() val categories = preferences.sourcesTabCategories().get()
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it })) .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it }))

View File

@ -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.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.browse.latest.LatestCardItem
class IndexCardItem(val manga: Manga) : AbstractFlexibleItem<IndexCardHolder>() { class IndexCardItem(val manga: Manga) : AbstractFlexibleItem<IndexCardHolder>() {
@ -29,7 +28,7 @@ class IndexCardItem(val manga: Manga) : AbstractFlexibleItem<IndexCardHolder>()
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other is LatestCardItem) { if (other is IndexCardItem) {
return manga.id == other.manga.id return manga.id == other.manga.id
} }
return false return false

View File

@ -59,12 +59,12 @@ class SettingsBrowseController : SettingsController() {
} }
preferenceCategory { preferenceCategory {
titleRes = R.string.latest titleRes = R.string.feed
switchPreference { switchPreference {
bindTo(preferences.latestTabInFront()) bindTo(preferences.feedTabInFront())
titleRes = R.string.pref_latest_position titleRes = R.string.pref_feed_position
summaryRes = R.string.pref_latest_position_summery summaryRes = R.string.pref_feed_position_summery
} }
} }
// SY <-- // SY <--

View File

@ -40,6 +40,7 @@ import exh.eh.EHentaiUpdateWorker
import exh.log.xLogE import exh.log.xLogE
import exh.log.xLogW import exh.log.xLogW
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
@ -416,10 +417,21 @@ object EXHMigrations {
}.getOrNull() }.getOrNull()
}?.ifEmpty { null } }?.ifEmpty { null }
if (savedSearches != 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) { prefs.edit(commit = true) {
remove("eh_saved_searches") remove("eh_saved_searches")
remove("latest_tab_sources")
} }
} }

View File

@ -373,8 +373,4 @@ object DebugFunctions {
) )
} }
} }
fun unwatchAllSources() {
prefs.latestTabSources().delete()
}
} }

View File

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

View File

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

View File

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

View File

@ -49,6 +49,17 @@ interface SavedSearchQueries : DbProvider {
) )
.prepare() .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 insertSavedSearch(savedSearch: SavedSearch) = db.put().`object`(savedSearch).prepare()
fun insertSavedSearches(savedSearches: List<SavedSearch>) = db.put().objects(savedSearches).prepare() fun insertSavedSearches(savedSearches: List<SavedSearch>) = db.put().objects(savedSearches).prepare()

View File

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

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

View File

@ -192,8 +192,8 @@
<string name="library_group_updates_all">Launch category updates all the time</string> <string name="library_group_updates_all">Launch category updates all the time</string>
<!-- Browse settings --> <!-- Browse settings -->
<string name="pref_latest_position">Latest tab position</string> <string name="pref_feed_position">Feed 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_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">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_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> <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="no_source_categories">No source categories available</string>
<string name="invalid_category_name">Invalid category name</string> <string name="invalid_category_name">Invalid category name</string>
<!-- Latest Tab --> <!-- Feed Tab -->
<string name="watch">Watch</string> <string name="feed">Feed</string>
<string name="unwatch">Unwatch</string> <string name="feed_delete">Delete feed item?</string>
<string name="too_many_watched">Too many watched sources, cannot add more then 5</string> <string name="too_many_in_feed">Too many sources in your feed, cannot add more then 10</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> <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 --> <!-- Sort by tags -->
<string name="pref_tag_sorting">Tag sorting tags</string> <string name="pref_tag_sorting">Tag sorting tags</string>