diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index 31f52b8b8..1040316e6 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index 7a58e0eca..92e61a67d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 987453e2b..5df2642dd 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 1f232e03e..386fb4e86 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -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()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt index 9866f75dc..909dbe1e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt @@ -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 <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedAdapter.kt similarity index 79% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestAdapter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedAdapter.kt index 21011a132..1276df67e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedAdapter.kt @@ -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(null, controller, true) { +class FeedAdapter(val controller: FeedController) : + FlexibleAdapter(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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardAdapter.kt similarity index 64% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardAdapter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardAdapter.kt index 6be1069e8..af5b37ca6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardAdapter.kt @@ -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(null, controller, true) { +class FeedCardAdapter(controller: FeedController) : + FlexibleAdapter(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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardHolder.kt similarity index 93% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardHolder.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardHolder.kt index ad4de5acb..ee2e58040 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardHolder.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardItem.kt similarity index 71% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardItem.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardItem.kt index 84333738d..5a920e0d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestCardItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedCardItem.kt @@ -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() { +class FeedCardItem(val manga: Manga) : AbstractFlexibleItem() { override fun getLayoutRes(): Int { return R.layout.global_search_controller_card_item } - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LatestCardHolder { - return LatestCardHolder(view, adapter as LatestCardAdapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): FeedCardHolder { + return FeedCardHolder(view, adapter as FeedCardAdapter) } override fun bindViewHolder( adapter: FlexibleAdapter>, - holder: LatestCardHolder, + holder: FeedCardHolder, position: Int, payloads: List? ) { @@ -29,7 +28,7 @@ class LatestCardItem(val manga: Manga) : AbstractFlexibleItem( } override fun equals(other: Any?): Boolean { - if (other is LatestCardItem) { + if (other is FeedCardItem) { return manga.id == other.manga.id } return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt new file mode 100644 index 000000000..61d6c03a8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt @@ -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(), + 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) { + 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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedHolder.kt similarity index 62% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestHolder.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedHolder.kt index ec116cb53..a9d5c3786 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedHolder.kt @@ -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? = null + private var lastBoundResults: List? = 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 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedItem.kt similarity index 68% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestItem.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedItem.kt index 2d15e5fda..369441981 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedItem.kt @@ -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?, val highlighted: Boolean = false) : - AbstractFlexibleItem() { +class FeedItem( + val feed: FeedSavedSearch, + val savedSearch: SavedSearch?, + val source: CatalogueSource?, + val results: List?, + val highlighted: Boolean = false +) : AbstractFlexibleItem() { /** * Set view. @@ -29,12 +36,12 @@ class LatestItem(val source: CatalogueSource, val results: List? } /** - * Create view holder (see [LatestAdapter]. + * Create view holder (see [FeedAdapter]. * * @return holder of view. */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LatestHolder { - return LatestHolder(view, adapter as LatestAdapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): FeedHolder { + return FeedHolder(view, adapter as FeedAdapter) } /** @@ -42,7 +49,7 @@ class LatestItem(val source: CatalogueSource, val results: List? */ override fun bindViewHolder( adapter: FlexibleAdapter>, - holder: LatestHolder, + holder: FeedHolder, position: Int, payloads: List? ) { @@ -56,7 +63,7 @@ class LatestItem(val source: CatalogueSource, val results: List? */ 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? * @return hashcode */ override fun hashCode(): Int { - return source.id.toInt() + return feed.id!!.toInt() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt similarity index 56% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestPresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt index fefcf14c8..b6d9d1dac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt @@ -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() { +) : BasePresenter() { /** * 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 { + fun hasTooManyFeeds(): Boolean { + return db.getFeedSavedSearches().executeAsBlocking().size > 10 + } + + fun getEnabledSources(): List { 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 { - return getEnabledSources() + fun getSourceSavedSearches(source: CatalogueSource): List { + 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> { + 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?): LatestItem { - return LatestItem(source, results) + protected open fun createCatalogueSearchItem( + feed: FeedSavedSearch, + savedSearch: SavedSearch?, + source: CatalogueSource?, + results: List? + ): 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. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestController.kt deleted file mode 100644 index 1f2ae7bc2..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestController.kt +++ /dev/null @@ -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(), - 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) { - 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()) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index 116631335..fedc880b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt @@ -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 })) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexCardItem.kt index 6a60b6e2e..35403cfe5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexCardItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexCardItem.kt @@ -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() { @@ -29,7 +28,7 @@ class IndexCardItem(val manga: Manga) : AbstractFlexibleItem() } override fun equals(other: Any?): Boolean { - if (other is LatestCardItem) { + if (other is IndexCardItem) { return manga.id == other.manga.id } return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt index 517cc5404..09ed5cd82 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt @@ -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 <-- diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index af0d551b5..a267ce3f0 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -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") } } diff --git a/app/src/main/java/exh/debug/DebugFunctions.kt b/app/src/main/java/exh/debug/DebugFunctions.kt index 6f67f9f5b..d9b406ca1 100644 --- a/app/src/main/java/exh/debug/DebugFunctions.kt +++ b/app/src/main/java/exh/debug/DebugFunctions.kt @@ -373,8 +373,4 @@ object DebugFunctions { ) } } - - fun unwatchAllSources() { - prefs.latestTabSources().delete() - } } diff --git a/app/src/main/java/exh/savedsearches/mappers/FeedSavedSearchTypeMapping.kt b/app/src/main/java/exh/savedsearches/mappers/FeedSavedSearchTypeMapping.kt new file mode 100644 index 000000000..273922839 --- /dev/null +++ b/app/src/main/java/exh/savedsearches/mappers/FeedSavedSearchTypeMapping.kt @@ -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( + FeedSavedSearchPutResolver(), + FeedSavedSearchGetResolver(), + FeedSavedSearchDeleteResolver() +) + +class FeedSavedSearchPutResolver : DefaultPutResolver() { + + 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() { + + 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() { + + override fun mapToDeleteQuery(obj: FeedSavedSearch) = DeleteQuery.builder() + .table(TABLE) + .where("$COL_ID = ?") + .whereArgs(obj.id) + .build() +} diff --git a/app/src/main/java/exh/savedsearches/models/FeedSavedSearch.kt b/app/src/main/java/exh/savedsearches/models/FeedSavedSearch.kt new file mode 100644 index 000000000..8670aaae8 --- /dev/null +++ b/app/src/main/java/exh/savedsearches/models/FeedSavedSearch.kt @@ -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? +) diff --git a/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt b/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt new file mode 100644 index 000000000..9dd067552 --- /dev/null +++ b/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt @@ -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) = 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) = 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) { + db.inTransaction { + deleteSavedSearches(mergedMangaId).executeAsBlocking() + mergedMangases.chunked(100) { chunk -> + insertSavedSearches(chunk).executeAsBlocking() + } + } + }*/ +} diff --git a/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt b/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt index b62aae462..214f0bc8c 100644 --- a/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt +++ b/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt @@ -49,6 +49,17 @@ interface SavedSearchQueries : DbProvider { ) .prepare() + fun getSavedSearches(ids: List) = 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) = db.put().objects(savedSearches).prepare() diff --git a/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt b/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt new file mode 100644 index 000000000..453d03735 --- /dev/null +++ b/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt @@ -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)" +} diff --git a/app/src/main/res/menu/feed.xml b/app/src/main/res/menu/feed.xml new file mode 100644 index 000000000..bf8af7976 --- /dev/null +++ b/app/src/main/res/menu/feed.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index d944ca306..113c1b7d9 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -192,8 +192,8 @@ Launch category updates all the time - Latest tab position - 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 + Feed tab position + 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 Filter sources in categories Filter the sources that are in categories, making the sources not get put under the language if they are in a category Replace latest button @@ -361,11 +361,11 @@ No source categories available Invalid category name - - Watch - Unwatch - Too many watched sources, cannot add more then 5 - You don\'t have any watched sources, go to the sources tab and long press a source to watch it + + Feed + Delete feed item? + Too many sources in your feed, cannot add more then 10 + You don\'t have any sources in your feed, go to the sources tab and long press a source to watch it Tag sorting tags