From d0e9d24f6f41ed9dcbec8ec0ca4f0c9d68db8ffc Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sun, 27 Mar 2022 20:09:39 -0400 Subject: [PATCH] Add feed to the combined sources menus --- .../data/database/queries/RawQueries.kt | 19 +- .../ui/browse/feed/FeedController.kt | 8 +- .../tachiyomi/ui/browse/feed/FeedItem.kt | 5 +- .../tachiyomi/ui/browse/feed/FeedPresenter.kt | 25 +- .../ui/browse/source/SourceController.kt | 10 +- .../browse/source/feed/SourceFeedAdapter.kt | 85 +++++ .../source/feed/SourceFeedCardAdapter.kt | 27 ++ .../source/feed/SourceFeedCardHolder.kt | 58 +++ .../browse/source/feed/SourceFeedCardItem.kt | 40 ++ .../source/feed/SourceFeedController.kt | 345 +++++++++++++++++ .../ui/browse/source/feed/SourceFeedHolder.kt | 125 ++++++ .../ui/browse/source/feed/SourceFeedItem.kt | 73 ++++ .../browse/source/feed/SourceFeedPresenter.kt | 357 ++++++++++++++++++ .../tachiyomi/ui/manga/MangaController.kt | 4 +- app/src/main/java/exh/EXHMigrations.kt | 3 +- .../mappers/FeedSavedSearchTypeMapping.kt | 7 +- .../savedsearches/models/FeedSavedSearch.kt | 5 +- .../queries/FeedSavedSearchQueries.kt | 38 +- .../tables/FeedSavedSearchTable.kt | 3 + app/src/main/res/values/strings_sy.xml | 1 + 20 files changed, 1187 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt 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 5df2642dd..8d3e69f3e 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 @@ -77,13 +77,26 @@ fun getReadMangaNotInLibraryQuery() = """ /** - * Query to get the manga merged into a merged manga + * Query to get the global feed saved searches */ -fun getFeedSavedSearchQuery() = +fun getGlobalFeedSavedSearchQuery() = """ SELECT ${SavedSearchTable.TABLE}.* FROM ( - SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} + SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 1 + ) AS M + JOIN ${SavedSearchTable.TABLE} + ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} +""" + +/** + * Query to get the source feed saved searches + */ +fun getSourceFeedSavedSearchQuery() = + """ + SELECT ${SavedSearchTable.TABLE}.* + FROM ( + SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 0 AND ${FeedSavedSearchTable.COL_SOURCE} = ? ) AS M JOIN ${SavedSearchTable.TABLE} ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} 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 index 61d6c03a8..9c92a7d18 100644 --- 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 @@ -167,12 +167,12 @@ open class FeedController : * @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? { + private fun getHolder(feed: FeedSavedSearch): FeedHolder? { val adapter = adapter ?: return null adapter.allBoundViewHolders.forEach { holder -> val item = adapter.getItem(holder.bindingAdapterPosition) - if (item != null && source.id == item.feed.id) { + if (item != null && feed.id == item.feed.id) { return holder as FeedHolder } } @@ -200,8 +200,8 @@ open class FeedController : * * @param manga the initialized manga. */ - fun onMangaInitialized(source: CatalogueSource, manga: Manga) { - getHolder(source)?.setImage(manga) + fun onMangaInitialized(feed: FeedSavedSearch, manga: Manga) { + getHolder(feed)?.setImage(manga) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedItem.kt index 369441981..cffac85ff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedItem.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.source.CatalogueSource -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.SavedSearch @@ -62,8 +61,8 @@ class FeedItem( * @return items are equal? */ override fun equals(other: Any?): Boolean { - if (other is GlobalSearchItem) { - return feed.id == other.source.id + if (other is FeedItem) { + return feed.id == other.feed.id } return false } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt index b6d9d1dac..e85ddc5b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt @@ -52,7 +52,7 @@ open class FeedPresenter( /** * Subject which fetches image of given manga. */ - private val fetchImageSubject = PublishSubject.create, Source>>() + private val fetchImageSubject = PublishSubject.create, Source, FeedSavedSearch>>() /** * Subscription for fetching images of manga. @@ -62,7 +62,7 @@ open class FeedPresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - db.getFeedSavedSearches() + db.getGlobalFeedSavedSearches() .asRxObservable() .observeOn(AndroidSchedulers.mainThread()) .doOnEach { @@ -79,7 +79,7 @@ open class FeedPresenter( } fun hasTooManyFeeds(): Boolean { - return db.getFeedSavedSearches().executeAsBlocking().size > 10 + return db.getGlobalFeedSavedSearches().executeAsBlocking().size > 10 } fun getEnabledSources(): List { @@ -103,7 +103,8 @@ open class FeedPresenter( FeedSavedSearch( id = null, source = source.id, - savedSearch = savedSearch?.id + savedSearch = savedSearch?.id, + global = true ) ).executeAsBlocking() } @@ -116,9 +117,9 @@ open class FeedPresenter( } private fun getSourcesToGetFeed(): List> { - val savedSearches = db.getSavedSearchesFeed().executeAsBlocking() + val savedSearches = db.getGlobalSavedSearchesFeed().executeAsBlocking() .associateBy { it.id!! } - return db.getFeedSavedSearches().executeAsBlocking() + return db.getGlobalFeedSavedSearches().executeAsBlocking() .map { it to savedSearches[it.savedSearch] } } @@ -169,7 +170,7 @@ open class FeedPresenter( .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. + .doOnNext { fetchImage(it, source, feed) } // Load manga covers. .map { list -> createCatalogueSearchItem(feed, savedSearch, source, list.map { FeedCardItem(it) }) } } else { Observable.just(createCatalogueSearchItem(feed, null, null, emptyList())) @@ -215,8 +216,8 @@ open class FeedPresenter( * * @param manga the list of manga to initialize. */ - private fun fetchImage(manga: List, source: Source) { - fetchImageSubject.onNext(Pair(manga, source)) + private fun fetchImage(manga: List, source: CatalogueSource, feed: FeedSavedSearch) { + fetchImageSubject.onNext(Triple(manga, source, feed)) } /** @@ -230,14 +231,14 @@ open class FeedPresenter( Observable.from(pair.first).filter { it.thumbnail_url == null && !it.initialized } .map { Pair(it, source) } .concatMap { getMangaDetailsObservable(it.first, it.second) } - .map { Pair(source as CatalogueSource, it) } + .map { Pair(pair.third, it) } } .onBackpressureBuffer() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { (source, manga) -> + { (feed, manga) -> @Suppress("DEPRECATION") - view?.onMangaInitialized(source, manga) + view?.onMangaInitialized(feed, manga) }, { error -> logcat(LogPriority.ERROR, error) 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 fedc880b9..902bef2df 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 @@ -29,8 +29,8 @@ import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController +import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController -import eu.kanade.tachiyomi.ui.browse.source.index.IndexController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog import eu.kanade.tachiyomi.ui.main.MainActivity @@ -145,7 +145,7 @@ class SourceController(bundle: Bundle? = null) : // Open the catalogue view. // SY --> if (source.supportsLatest && preferences.useNewSourceNavigation().get()) { - openIndexSource(source) + openSourceFeed(source) } else openSource(source, BrowseSourceController(source)) // SY <-- } @@ -307,11 +307,11 @@ class SourceController(bundle: Bundle? = null) : // SY --> /** - * Opens a catalogue with the index controller. + * Opens a catalogue with the source feed controller. */ - private fun openIndexSource(source: CatalogueSource) { + private fun openSourceFeed(source: CatalogueSource) { preferences.lastUsedSource().set(source.id) - parentController!!.router.pushController(IndexController(source).withFadeTransaction()) + parentController!!.router.pushController(SourceFeedController(source).withFadeTransaction()) } // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedAdapter.kt new file mode 100644 index 000000000..cdca13abb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedAdapter.kt @@ -0,0 +1,85 @@ +package eu.kanade.tachiyomi.ui.browse.source.feed + +import android.os.Bundle +import android.os.Parcelable +import android.util.SparseArray +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import exh.savedsearches.models.FeedSavedSearch +import exh.savedsearches.models.SavedSearch + +/** + * Adapter that holds the search cards. + * + * @param controller instance of [SourceFeedController]. + */ +class SourceFeedAdapter(val controller: SourceFeedController) : + FlexibleAdapter(null, controller, true) { + + val feedClickListener: OnFeedClickListener = controller + + /** + * Bundle where the view state of the holders is saved. + */ + private var bundle = Bundle() + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { + super.onBindViewHolder(holder, position, payloads) + restoreHolderState(holder) + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + super.onViewRecycled(holder) + saveHolderState(holder, bundle) + } + + override fun onSaveInstanceState(outState: Bundle) { + val holdersBundle = Bundle() + allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) } + outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)!! + } + + /** + * Saves the view state of the given holder. + * + * @param holder The holder to save. + * @param outState The bundle where the state is saved. + */ + private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) { + val key = "holder_${holder.bindingAdapterPosition}" + val holderState = SparseArray() + holder.itemView.saveHierarchyState(holderState) + outState.putSparseParcelableArray(key, holderState) + } + + /** + * Restores the view state of the given holder. + * + * @param holder The holder to restore. + */ + private fun restoreHolderState(holder: RecyclerView.ViewHolder) { + val key = "holder_${holder.bindingAdapterPosition}" + val holderState = bundle.getSparseParcelableArray(key) + if (holderState != null) { + holder.itemView.restoreHierarchyState(holderState) + bundle.remove(key) + } + } + + interface OnFeedClickListener { + fun onLatestClick() + fun onBrowseClick() + fun onSavedSearchClick(savedSearch: SavedSearch) + fun onRemoveClick(feedSavedSearch: FeedSavedSearch) + } + + private companion object { + const val HOLDER_BUNDLE_KEY = "holder_bundle" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardAdapter.kt new file mode 100644 index 000000000..d0421877b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardAdapter.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.ui.browse.source.feed + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.data.database.models.Manga + +/** + * Adapter that holds the manga items from search results. + * + * @param controller instance of [SourceFeedController]. + */ +class SourceFeedCardAdapter(controller: SourceFeedController) : + FlexibleAdapter(null, controller, true) { + + /** + * Listen for browse item clicks. + */ + val mangaClickListener: OnMangaClickListener = controller + + /** + * Listener which should be called when user clicks browse. + * Note: Should only be handled by [SourceFeedController] + */ + interface OnMangaClickListener { + fun onMangaClick(manga: Manga) + fun onMangaLongClick(manga: Manga) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardHolder.kt new file mode 100644 index 000000000..1c4f94101 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardHolder.kt @@ -0,0 +1,58 @@ +package eu.kanade.tachiyomi.ui.browse.source.feed + +import android.view.View +import androidx.core.view.isVisible +import coil.dispose +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding +import eu.kanade.tachiyomi.util.view.loadAutoPause + +class SourceFeedCardHolder(view: View, adapter: SourceFeedCardAdapter) : + FlexibleViewHolder(view, adapter) { + + private val binding = GlobalSearchControllerCardItemBinding.bind(view) + + init { + // Call onMangaClickListener when item is pressed. + itemView.setOnClickListener { + val item = adapter.getItem(bindingAdapterPosition) + if (item != null) { + adapter.mangaClickListener.onMangaClick(item.manga) + } + } + itemView.setOnLongClickListener { + val item = adapter.getItem(bindingAdapterPosition) + if (item != null) { + adapter.mangaClickListener.onMangaLongClick(item.manga) + } + true + } + } + + fun bind(manga: Manga) { + binding.card.clipToOutline = true + + // Set manga title + binding.title.text = manga.title + + // Set alpha of thumbnail. + binding.cover.alpha = if (manga.favorite) 0.3f else 1.0f + + // For rounded corners + binding.badges.clipToOutline = true + + // Set favorite badge + binding.favoriteText.isVisible = manga.favorite + + setImage(manga) + } + + fun setImage(manga: Manga) { + binding.cover.dispose() + binding.cover.loadAutoPause(manga) { + setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardItem.kt new file mode 100644 index 000000000..d1cba299f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedCardItem.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.ui.browse.source.feed + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga + +class SourceFeedCardItem(val manga: Manga) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.global_search_controller_card_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): SourceFeedCardHolder { + return SourceFeedCardHolder(view, adapter as SourceFeedCardAdapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: SourceFeedCardHolder, + position: Int, + payloads: List? + ) { + holder.bind(manga) + } + + override fun equals(other: Any?): Boolean { + if (other is SourceFeedCardItem) { + return manga.id == other.manga.id + } + return false + } + + override fun hashCode(): Int { + return manga.id?.toInt() ?: 0 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedController.kt new file mode 100644 index 000000000..dc676368e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedController.kt @@ -0,0 +1,345 @@ +package eu.kanade.tachiyomi.ui.browse.source.feed + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import dev.chrisbanes.insetter.applyInsetter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.databinding.LatestControllerBinding +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.ui.base.controller.FabController +import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController +import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet +import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.system.toast +import exh.savedsearches.models.FeedSavedSearch +import exh.savedsearches.models.SavedSearch +import exh.util.nullIfBlank +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import xyz.nulldev.ts.api.http.serializer.FilterSerializer + +/** + * This controller shows and manages the different search result in global search. + * This controller should only handle UI actions, IO actions should be done by [SourceFeedPresenter] + * [SourceFeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search + */ +open class SourceFeedController : + SearchableNucleusController, + FabController, + SourceFeedCardAdapter.OnMangaClickListener, + SourceFeedAdapter.OnFeedClickListener { + + constructor(source: CatalogueSource?) : super( + bundleOf( + SOURCE_EXTRA to (source?.id ?: 0) + ) + ) { + this.source = source + } + + constructor(sourceId: Long) : this( + Injekt.get().get(sourceId) as? CatalogueSource + ) + + @Suppress("unused") + constructor(bundle: Bundle) : this(bundle.getLong(SOURCE_EXTRA)) + + /** + * Adapter containing search results grouped by lang. + */ + protected var adapter: SourceFeedAdapter? = null + + var source: CatalogueSource? = null + + private var actionFab: ExtendedFloatingActionButton? = null + + /** + * Sheet containing filter items. + */ + private var filterSheet: SourceFilterSheet? = null + + init { + setHasOptionsMenu(true) + } + + override fun getTitle(): String? { + return source!!.name + } + + /** + * Create the [SourceFeedPresenter] used in controller. + * + * @return instance of [SourceFeedPresenter] + */ + override fun createPresenter(): SourceFeedPresenter { + return SourceFeedPresenter(source!!) + } + + /** + * Called when manga in global search is clicked, opens manga. + * + * @param manga clicked item containing manga information. + */ + override fun onMangaClick(manga: Manga) { + // Open MangaController. + parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction()) + } + + /** + * Called when manga in global search is long clicked. + * + * @param manga clicked item containing manga information. + */ + override fun onMangaLongClick(manga: Manga) { + // Delegate to single click by default. + onMangaClick(manga) + } + + /** + * Adds items to the options menu. + * + * @param menu menu containing options. + * @param inflater used to load the menu xml. + */ + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + createOptionsMenu(menu, inflater, R.menu.global_search, R.id.action_search) + } + + override fun onSearchViewQueryTextSubmit(query: String?) { + onBrowseClick(query.nullIfBlank()) + } + + override fun onSearchViewQueryTextChange(newText: String?) { + if (router.backstack.lastOrNull()?.controller == this) { + presenter.query = newText ?: "" + } + } + + override fun createBinding(inflater: LayoutInflater): LatestControllerBinding = LatestControllerBinding.inflate(inflater) + + /** + * Called when the view is created + * + * @param view view of controller + */ + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + // Prepare filter sheet + initFilterSheet() + + binding.recycler.applyInsetter { + type(navigationBars = true) { + padding() + } + } + + adapter = SourceFeedAdapter(this) + + // Create recycler and set adapter. + binding.recycler.layoutManager = LinearLayoutManager(view.context) + binding.recycler.adapter = adapter + } + + override fun onDestroyView(view: View) { + adapter = null + super.onDestroyView(view) + } + + override fun onSaveViewState(view: View, outState: Bundle) { + super.onSaveViewState(view, outState) + adapter?.onSaveInstanceState(outState) + } + + override fun onRestoreViewState(view: View, savedViewState: Bundle) { + super.onRestoreViewState(view, savedViewState) + adapter?.onRestoreInstanceState(savedViewState) + } + + private val filterSerializer = FilterSerializer() + + fun initFilterSheet() { + if (presenter.sourceFilters.isEmpty()) { + actionFab?.text = activity!!.getString(R.string.saved_searches) + } + + filterSheet = SourceFilterSheet( + activity!!, + // SY --> + this, + presenter.source, + presenter.loadSearches(), + // SY <-- + onFilterClicked = { + val allDefault = presenter.sourceFilters == presenter.source.getFilterList() + filterSheet?.dismiss() + if (allDefault) { + onBrowseClick( + presenter.query.nullIfBlank() + ) + } else { + onBrowseClick( + presenter.query.nullIfBlank(), + filters = Json.encodeToString(filterSerializer.serialize(presenter.sourceFilters)) + ) + } + }, + onResetClicked = {}, + onSaveClicked = {}, + onSavedSearchClicked = cb@{ idOfSearch -> + val search = presenter.loadSearch(idOfSearch) + + if (search == null) { + filterSheet?.context?.let { + MaterialAlertDialogBuilder(it) + .setTitle(R.string.save_search_failed_to_load) + .setMessage(R.string.save_search_failed_to_load_message) + .show() + } + return@cb + } + + if (search.filterList == null) { + activity?.toast(R.string.save_search_invalid) + return@cb + } + + presenter.sourceFilters = FilterList(search.filterList) + filterSheet?.setFilters(presenter.filterItems) + val allDefault = presenter.sourceFilters == presenter.source.getFilterList() + filterSheet?.dismiss() + + if (!allDefault) { + onBrowseClick( + search = presenter.query.nullIfBlank(), + savedSearch = search.id + ) + } + }, + onSavedSearchDeleteClicked = { idOfSearch, name -> + MaterialAlertDialogBuilder(activity!!) + .setTitle(R.string.feed) + .setMessage(activity!!.getString(R.string.feed_add, name)) + .setPositiveButton(R.string.action_add) { _, _ -> + presenter.createFeed(idOfSearch) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + ) + filterSheet?.setFilters(presenter.filterItems) + + // TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly + filterSheet?.setOnShowListener { actionFab?.isVisible = false } + filterSheet?.setOnDismissListener { actionFab?.isVisible = true } + + actionFab?.setOnClickListener { filterSheet?.show() } + + actionFab?.isVisible = true + } + + override fun configureFab(fab: ExtendedFloatingActionButton) { + actionFab = fab + + // Controlled by initFilterSheet() + fab.isVisible = false + + fab.setText(R.string.action_filter) + fab.setIconResource(R.drawable.ic_filter_list_24dp) + } + + override fun cleanupFab(fab: ExtendedFloatingActionButton) { + fab.setOnClickListener(null) + actionFab = null + } + + /** + * Returns the view holder for the given manga. + * + * @param source used to find holder containing source + * @return the holder of the manga or null if it's not bound. + */ + private fun getHolder(sourceFeed: SourceFeed): SourceFeedHolder? { + val adapter = adapter ?: return null + + adapter.allBoundViewHolders.forEach { holder -> + val item = adapter.getItem(holder.bindingAdapterPosition) + if (item != null && sourceFeed == item.sourceFeed) { + return holder as SourceFeedHolder + } + } + + return null + } + + /** + * Add search result to adapter. + * + * @param feedManga the source items containing the latest manga. + */ + fun setItems(feedManga: List) { + adapter?.updateDataSet(feedManga) + + if (feedManga.isEmpty()) { + binding.emptyView.show(R.string.feed_tab_empty) + } else { + binding.emptyView.hide() + } + } + + /** + * Called from the presenter when a manga is initialized. + * + * @param manga the initialized manga. + */ + fun onMangaInitialized(sourceFeed: SourceFeed, manga: Manga) { + getHolder(sourceFeed)?.setImage(manga) + } + + fun onBrowseClick(search: String? = null, savedSearch: Long? = null, filters: String? = null) { + router.replaceTopController(BrowseSourceController(presenter.source, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction()) + } + + override fun onLatestClick() { + router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction()) + } + + override fun onBrowseClick() { + router.replaceTopController(BrowseSourceController(presenter.source).withFadeTransaction()) + } + + override fun onSavedSearchClick(savedSearch: SavedSearch) { + router.replaceTopController(BrowseSourceController(presenter.source, savedSearch = savedSearch.id).withFadeTransaction()) + } + + override fun onRemoveClick(feedSavedSearch: FeedSavedSearch) { + MaterialAlertDialogBuilder(activity!!) + .setTitle(R.string.feed) + .setMessage(R.string.feed_delete) + .setPositiveButton(R.string.action_delete) { _, _ -> + presenter.deleteFeed(feedSavedSearch) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + companion object { + const val SOURCE_EXTRA = "source" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedHolder.kt new file mode 100644 index 000000000..2ab83940b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedHolder.kt @@ -0,0 +1,125 @@ +package eu.kanade.tachiyomi.ui.browse.source.feed + +import android.annotation.SuppressLint +import android.view.View +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.databinding.LatestControllerCardBinding + +/** + * Holder that binds the [SourceFeedItem] containing catalogue cards. + * + * @param view view of [SourceFeedItem] + * @param adapter instance of [SourceFeedAdapter] + */ +class SourceFeedHolder(view: View, val adapter: SourceFeedAdapter) : + FlexibleViewHolder(view, adapter) { + + private val binding = LatestControllerCardBinding.bind(view) + + /** + * Adapter containing manga from search results. + */ + private val mangaAdapter = SourceFeedCardAdapter(adapter.controller) + + private var lastBoundResults: List? = null + + init { + // Set layout horizontal. + binding.recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false) + binding.recycler.adapter = mangaAdapter + + binding.titleWrapper.setOnClickListener { + adapter.getItem(bindingAdapterPosition)?.let { + when (it.sourceFeed) { + SourceFeed.Browse -> adapter.feedClickListener.onBrowseClick() + SourceFeed.Latest -> adapter.feedClickListener.onLatestClick() + is SourceFeed.SourceSavedSearch -> adapter.feedClickListener.onSavedSearchClick(it.sourceFeed.savedSearch) + } + } + } + + binding.titleWrapper.setOnLongClickListener { + adapter.getItem(bindingAdapterPosition)?.let { + when (it.sourceFeed) { + SourceFeed.Browse -> adapter.feedClickListener.onBrowseClick() + SourceFeed.Latest -> adapter.feedClickListener.onLatestClick() + is SourceFeed.SourceSavedSearch -> adapter.feedClickListener.onRemoveClick(it.sourceFeed.feed) + } + } + true + } + } + + /** + * Show the loading of source search result. + * + * @param item item of card. + */ + @SuppressLint("SetTextI18n") + fun bind(item: SourceFeedItem) { + val results = item.results + + when (item.sourceFeed) { + SourceFeed.Browse -> binding.title.setText(R.string.browse) + SourceFeed.Latest -> binding.title.setText(R.string.latest) + is SourceFeed.SourceSavedSearch -> binding.title.text = item.sourceFeed.savedSearch.name + } + + when { + results == null -> { + binding.progress.isVisible = true + showResultsHolder() + } + results.isEmpty() -> { + binding.progress.isVisible = false + showNoResults() + } + else -> { + binding.progress.isVisible = false + showResultsHolder() + } + } + if (results !== lastBoundResults) { + mangaAdapter.updateDataSet(results) + lastBoundResults = results + } + } + + /** + * Called from the presenter when a manga is initialized. + * + * @param manga the initialized manga. + */ + fun setImage(manga: Manga) { + getHolder(manga)?.setImage(manga) + } + + /** + * Returns the view holder for the given manga. + * + * @param manga the manga to find. + * @return the holder of the manga or null if it's not bound. + */ + private fun getHolder(manga: Manga): SourceFeedCardHolder? { + mangaAdapter.allBoundViewHolders.forEach { holder -> + val item = mangaAdapter.getItem(holder.bindingAdapterPosition) + if (item != null && item.manga.id!! == manga.id!!) { + return holder as SourceFeedCardHolder + } + } + + return null + } + + private fun showResultsHolder() { + binding.noResultsFound.isVisible = false + } + + private fun showNoResults() { + binding.noResultsFound.isVisible = true + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedItem.kt new file mode 100644 index 000000000..e23406c21 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedItem.kt @@ -0,0 +1,73 @@ +package eu.kanade.tachiyomi.ui.browse.source.feed + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R + +/** + * Item that contains search result information. + * + * @param feed the source for the search results. + * @param results the search results. + * @param highlighted whether this search item should be highlighted/marked in the catalogue search view. + */ +class SourceFeedItem( + val sourceFeed: SourceFeed, + val results: List?, + val highlighted: Boolean = false +) : AbstractFlexibleItem() { + + /** + * Set view. + * + * @return id of view + */ + override fun getLayoutRes(): Int { + return R.layout.global_search_controller_card + } + + /** + * Create view holder (see [SourceFeedAdapter]. + * + * @return holder of view. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): SourceFeedHolder { + return SourceFeedHolder(view, adapter as SourceFeedAdapter) + } + + /** + * Bind item to view. + */ + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: SourceFeedHolder, + position: Int, + payloads: List? + ) { + holder.bind(this) + } + + /** + * Used to check if two items are equal. + * + * @return items are equal? + */ + override fun equals(other: Any?): Boolean { + if (other is SourceFeedItem) { + return sourceFeed == other.sourceFeed + } + return false + } + + /** + * Return hash code of item. + * + * @return hashcode + */ + override fun hashCode(): Int { + return sourceFeed.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt new file mode 100644 index 000000000..c8a092a81 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt @@ -0,0 +1,357 @@ +package eu.kanade.tachiyomi.ui.browse.source.feed + +import android.os.Bundle +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.toMangaInfo +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.toSManga +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems +import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.runAsObservable +import eu.kanade.tachiyomi.util.system.logcat +import exh.log.xLogE +import exh.savedsearches.EXHSavedSearch +import exh.savedsearches.models.FeedSavedSearch +import exh.savedsearches.models.SavedSearch +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import logcat.LogPriority +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import rx.subjects.PublishSubject +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import xyz.nulldev.ts.api.http.serializer.FilterSerializer +import java.lang.RuntimeException + +sealed class SourceFeed { + object Latest : SourceFeed() + object Browse : SourceFeed() + data class SourceSavedSearch(val feed: FeedSavedSearch, val savedSearch: SavedSearch) : SourceFeed() +} + +/** + * Presenter of [SourceFeedController] + * Function calls should be done from here. UI calls should be done from the controller. + * + * @param source the source. + * @param db manages the database calls. + * @param preferences manages the preference calls. + */ +open class SourceFeedPresenter( + val source: CatalogueSource, + val db: DatabaseHelper = Injekt.get(), + val preferences: PreferencesHelper = Injekt.get() +) : BasePresenter() { + + /** + * Fetches the different sources by user settings. + */ + private var fetchSourcesSubscription: Subscription? = null + + /** + * Subject which fetches image of given manga. + */ + private val fetchImageSubject = PublishSubject.create, Source, SourceFeed>>() + + /** + * Subscription for fetching images of manga. + */ + private var fetchImageSubscription: Subscription? = null + + /** + * Modifiable list of filters. + */ + var sourceFilters = FilterList() + set(value) { + field = value + filterItems = value.toItems() + } + + var filterItems: List> = emptyList() + + init { + query = "" + } + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + sourceFilters = source.getFilterList() + + db.getSourceFeedSavedSearches(source.id) + .asRxObservable() + .observeOn(AndroidSchedulers.mainThread()) + .doOnEach { + getFeed() + } + .subscribe() + .let(::add) + } + + override fun onDestroy() { + fetchSourcesSubscription?.unsubscribe() + fetchImageSubscription?.unsubscribe() + super.onDestroy() + } + + fun hasTooManyFeeds(): Boolean { + return db.getSourceFeedSavedSearches(source.id).executeAsBlocking().size > 10 + } + + fun getSourceSavedSearches(): List { + return db.getSavedSearches(source.id).executeAsBlocking() + } + + fun createFeed(savedSearchId: Long) { + launchIO { + db.insertFeedSavedSearch( + FeedSavedSearch( + id = null, + source = source.id, + savedSearch = savedSearchId, + global = false + ) + ).executeAsBlocking() + } + } + + fun deleteFeed(feed: FeedSavedSearch) { + launchIO { + db.deleteFeedSavedSearch(feed).executeAsBlocking() + } + } + + private fun getSourcesToGetFeed(): List { + val savedSearches = db.getSourceSavedSearchesFeed(source.id).executeAsBlocking() + .associateBy { it.id!! } + + return listOf(SourceFeed.Latest, SourceFeed.Browse) + db.getSourceFeedSavedSearches(source.id).executeAsBlocking() + .map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) } + } + + /** + * Creates a catalogue search item + */ + protected open fun createCatalogueSearchItem( + sourceFeed: SourceFeed, + results: List? + ): SourceFeedItem { + return SourceFeedItem(sourceFeed, results) + } + + /** + * Initiates get manga per feed. + */ + fun getFeed() { + // Create image fetch subscription + initializeFetchImageSubscription() + + // Create items with the initial state + val initialItems = getSourcesToGetFeed().map { + createCatalogueSearchItem( + it, + null + ) + } + var items = initialItems + + fetchSourcesSubscription?.unsubscribe() + fetchSourcesSubscription = Observable.from(getSourcesToGetFeed()) + .flatMap( + { sourceFeed -> + Observable.defer { + when (sourceFeed) { + SourceFeed.Browse -> source.fetchPopularManga(1) + SourceFeed.Latest -> source.fetchLatestUpdates(1) + is SourceFeed.SourceSavedSearch -> source.fetchSearchManga( + page = 1, + query = sourceFeed.savedSearch.query.orEmpty(), + filters = getFilterList(sourceFeed.savedSearch, source) + ) + } + } + .subscribeOn(Schedulers.io()) + .onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions + .map { it.mangas } // Get manga from search result. + .map { list -> list.map { networkToLocalManga(it, source.id) } } // Convert to local manga. + .doOnNext { fetchImage(it, source, sourceFeed) } // Load manga covers. + .map { list -> createCatalogueSearchItem(sourceFeed, list.map { SourceFeedCardItem(it) }) } + }, + 5 + ) + .observeOn(AndroidSchedulers.mainThread()) + // Update matching source with the obtained results + .map { result -> + items.map { item -> if (item.sourceFeed == result.sourceFeed) result else item } + } + // Update current state + .doOnNext { items = it } + // Deliver initial state + .startWith(initialItems) + .subscribeLatestCache( + { view, manga -> + view.setItems(manga) + }, + { _, error -> + logcat(LogPriority.ERROR, error) + } + ) + } + + private val filterSerializer = FilterSerializer() + + private fun getFilterList(savedSearch: SavedSearch, source: CatalogueSource): FilterList { + val filters = savedSearch.filtersJson ?: return FilterList() + return runCatching { + val originalFilters = source.getFilterList() + filterSerializer.deserialize( + filters = originalFilters, + json = Json.decodeFromString(filters) + ) + originalFilters + }.getOrElse { FilterList() } + } + + /** + * Initialize a list of manga. + * + * @param manga the list of manga to initialize. + */ + private fun fetchImage(manga: List, source: Source, sourceFeed: SourceFeed) { + fetchImageSubject.onNext(Triple(manga, source, sourceFeed)) + } + + /** + * Subscribes to the initializer of manga details and updates the view if needed. + */ + private fun initializeFetchImageSubscription() { + fetchImageSubscription?.unsubscribe() + fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io()) + .flatMap { pair -> + val source = pair.second + Observable.from(pair.first).filter { it.thumbnail_url == null && !it.initialized } + .map { Pair(it, source) } + .concatMap { getMangaDetailsObservable(it.first, it.second) } + .map { Pair(pair.third, it) } + } + .onBackpressureBuffer() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { (sourceFeed, manga) -> + @Suppress("DEPRECATION") + view?.onMangaInitialized(sourceFeed, manga) + }, + { error -> + logcat(LogPriority.ERROR, error) + } + ) + } + + /** + * Returns an observable of manga that initializes the given manga. + * + * @param manga the manga to initialize. + * @return an observable of the manga to initialize + */ + private fun getMangaDetailsObservable(manga: Manga, source: Source): Observable { + return runAsObservable { + val networkManga = source.getMangaDetails(manga.toMangaInfo()) + manga.copyFrom(networkManga.toSManga()) + manga.initialized = true + db.insertManga(manga).executeAsBlocking() + manga + } + .onErrorResumeNext { Observable.just(manga) } + } + + /** + * Returns a manga from the database for the given manga from network. It creates a new entry + * if the manga is not yet in the database. + * + * @param sManga the manga from the source. + * @return a manga from the database. + */ + protected open fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga { + var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking() + if (localManga == null) { + val newManga = Manga.create(sManga.url, sManga.title, sourceId) + newManga.copyFrom(sManga) + val result = db.insertManga(newManga).executeAsBlocking() + newManga.id = result.insertedId() + localManga = newManga + } + return localManga + } + + fun loadSearch(searchId: Long): EXHSavedSearch? { + val search = db.getSavedSearch(searchId).executeAsBlocking() ?: return null + return EXHSavedSearch( + id = search.id!!, + name = search.name, + query = search.query.orEmpty(), + filterList = runCatching { + val originalFilters = source.getFilterList() + filterSerializer.deserialize( + filters = originalFilters, + json = search.filtersJson + ?.let { Json.decodeFromString(it) } + ?: return@runCatching null + ) + originalFilters + }.getOrNull() + ) + } + + fun loadSearches(): List { + return db.getSavedSearches(source.id).executeAsBlocking().map { + val filtersJson = it.filtersJson ?: return@map EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null + ) + val filters = try { + Json.decodeFromString(filtersJson) + } catch (e: Exception) { + null + } ?: return@map EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null + ) + + try { + val originalFilters = source.getFilterList() + filterSerializer.deserialize(originalFilters, filters) + EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = originalFilters + ) + } catch (t: RuntimeException) { + // Load failed + xLogE("Failed to load saved search!", t) + EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 2300718e4..3f4a2bec3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -66,8 +66,8 @@ import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationContr import eu.kanade.tachiyomi.ui.browse.source.SourceController import eu.kanade.tachiyomi.ui.browse.source.SourceController.Companion.SMART_SEARCH_SOURCE_TAG import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController +import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController -import eu.kanade.tachiyomi.ui.browse.source.index.IndexController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.ChangeMangaCoverDialog @@ -921,7 +921,7 @@ class MangaController : previousController.searchWithQuery(query) } // SY --> - is IndexController -> { + is SourceFeedController -> { router.handleBack() previousController.onBrowseClick(query) } diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index a267ce3f0..b90d9533d 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -423,7 +423,8 @@ object EXHMigrations { FeedSavedSearch( id = null, source = it.toLong(), - savedSearch = null + savedSearch = null, + global = true ) }?.ifEmpty { null } if (feed != null) { diff --git a/app/src/main/java/exh/savedsearches/mappers/FeedSavedSearchTypeMapping.kt b/app/src/main/java/exh/savedsearches/mappers/FeedSavedSearchTypeMapping.kt index 273922839..e56c7cf1c 100644 --- a/app/src/main/java/exh/savedsearches/mappers/FeedSavedSearchTypeMapping.kt +++ b/app/src/main/java/exh/savedsearches/mappers/FeedSavedSearchTypeMapping.kt @@ -11,6 +11,7 @@ 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_GLOBAL import exh.savedsearches.tables.FeedSavedSearchTable.COL_ID import exh.savedsearches.tables.FeedSavedSearchTable.COL_SAVED_SEARCH_ID import exh.savedsearches.tables.FeedSavedSearchTable.COL_SOURCE @@ -37,7 +38,8 @@ class FeedSavedSearchPutResolver : DefaultPutResolver() { override fun mapToContentValues(obj: FeedSavedSearch) = contentValuesOf( COL_ID to obj.id, COL_SOURCE to obj.source, - COL_SAVED_SEARCH_ID to obj.savedSearch + COL_SAVED_SEARCH_ID to obj.savedSearch, + COL_GLOBAL to obj.global ) } @@ -46,7 +48,8 @@ class FeedSavedSearchGetResolver : DefaultGetResolver() { 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)) + savedSearch = cursor.getLongOrNull(cursor.getColumnIndexOrThrow(COL_SAVED_SEARCH_ID)), + global = cursor.getInt(cursor.getColumnIndexOrThrow(COL_GLOBAL)) == 1 ) } diff --git a/app/src/main/java/exh/savedsearches/models/FeedSavedSearch.kt b/app/src/main/java/exh/savedsearches/models/FeedSavedSearch.kt index 8670aaae8..547b12d96 100644 --- a/app/src/main/java/exh/savedsearches/models/FeedSavedSearch.kt +++ b/app/src/main/java/exh/savedsearches/models/FeedSavedSearch.kt @@ -8,5 +8,8 @@ data class FeedSavedSearch( var source: Long, // If -1 then get latest, if set get the saved search - var savedSearch: Long? + var savedSearch: Long?, + + // If the feed is a global or source specific feed + var global: Boolean ) diff --git a/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt b/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt index 9dd067552..29c646b57 100644 --- a/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt +++ b/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt @@ -4,40 +4,32 @@ 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 eu.kanade.tachiyomi.data.database.queries.getGlobalFeedSavedSearchQuery +import eu.kanade.tachiyomi.data.database.queries.getSourceFeedSavedSearchQuery import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.SavedSearch import exh.savedsearches.tables.FeedSavedSearchTable interface FeedSavedSearchQueries : DbProvider { - fun getFeedSavedSearches() = db.get() + fun getGlobalFeedSavedSearches() = db.get() .listOfObjects(FeedSavedSearch::class.java) .withQuery( Query.builder() .table(FeedSavedSearchTable.TABLE) + .where("${FeedSavedSearchTable.COL_GLOBAL} = 1") .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() + fun getSourceFeedSavedSearches(sourceId: Long) = db.get() .listOfObjects(FeedSavedSearch::class.java) .withQuery( Query.builder() .table(FeedSavedSearchTable.TABLE) - .where("${FeedSavedSearchTable.COL_ID} IN (?)") - .whereArgs(ids.joinToString()) + .where("${FeedSavedSearchTable.COL_SOURCE} = ? AND ${FeedSavedSearchTable.COL_GLOBAL} = 0") + .whereArgs(sourceId) + .orderBy(FeedSavedSearchTable.COL_ID) .build() ) .prepare() @@ -64,11 +56,21 @@ interface FeedSavedSearchQueries : DbProvider { ) .prepare() - fun getSavedSearchesFeed() = db.get() + fun getGlobalSavedSearchesFeed() = db.get() .listOfObjects(SavedSearch::class.java) .withQuery( RawQuery.builder() - .query(getFeedSavedSearchQuery()) + .query(getGlobalFeedSavedSearchQuery()) + .build() + ) + .prepare() + + fun getSourceSavedSearchesFeed(sourceId: Long) = db.get() + .listOfObjects(SavedSearch::class.java) + .withQuery( + RawQuery.builder() + .query(getSourceFeedSavedSearchQuery()) + .args(sourceId) .build() ) .prepare() diff --git a/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt b/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt index 453d03735..6b6f5b631 100644 --- a/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt +++ b/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt @@ -10,12 +10,15 @@ object FeedSavedSearchTable { const val COL_SAVED_SEARCH_ID = "saved_search" + const val COL_GLOBAL = "global" + val createTableQuery: String get() = """CREATE TABLE $TABLE( $COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_SOURCE INTEGER NOT NULL, $COL_SAVED_SEARCH_ID INTEGER, + $COL_GLOBAL BOOLEAN NOT NULL, FOREIGN KEY($COL_SAVED_SEARCH_ID) REFERENCES ${SavedSearchTable.TABLE} (${SavedSearchTable.COL_ID}) ON DELETE CASCADE )""" diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index 113c1b7d9..9f31ce39e 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -366,6 +366,7 @@ 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 + Add %1$s to feed? Tag sorting tags