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.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()

View File

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

View File

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

View File

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

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

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.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 {

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.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)

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

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

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

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

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.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.
*

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

View File

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

View File

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

View File

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

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

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