Migrate saved search and feed saved search to SQLDelight

This commit is contained in:
Jobobby04
2022-04-22 19:19:50 -04:00
parent 4a115785eb
commit 26b30adf4a
18 changed files with 469 additions and 558 deletions
@@ -0,0 +1,13 @@
package eu.kanade.data.exh
import exh.savedsearches.models.FeedSavedSearch
val feedSavedSearchMapper: (Long, Long, Long?, Boolean) -> FeedSavedSearch =
{ id, source, savedSearch, global ->
FeedSavedSearch(
id = id,
source = source,
savedSearch = savedSearch,
global = global
)
}
@@ -0,0 +1,14 @@
package eu.kanade.data.exh
import exh.savedsearches.models.SavedSearch
val savedSearchMapper: (Long, Long, String, String?, String?) -> SavedSearch =
{ id, source, name, query, filtersJson ->
SavedSearch(
id = id,
source = source,
name = name,
query = query,
filtersJson = filtersJson
)
}
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.data.DatabaseHandler
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
@@ -20,6 +21,7 @@ import uy.kohesive.injekt.injectLazy
abstract class AbstractBackupManager(protected val context: Context) {
internal val databaseHelper: DatabaseHelper by injectLazy()
internal val databaseHandler: DatabaseHandler by injectLazy()
internal val sourceManager: SourceManager by injectLazy()
internal val trackManager: TrackManager by injectLazy()
protected val preferences: PreferencesHelper by injectLazy()
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.full
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
@@ -39,11 +40,11 @@ import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.savedsearches.models.SavedSearch
import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource
import exh.util.executeOnIO
import exh.util.nullIfBlank
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority
import okio.buffer
@@ -167,13 +168,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @return list of [BackupSavedSearch] to be backed up
*/
private fun backupSavedSearches(): List<BackupSavedSearch> {
return databaseHelper.getSavedSearches().executeAsBlocking().map {
BackupSavedSearch(
it.name,
it.query.orEmpty(),
it.filtersJson ?: "[]",
it.source,
)
return runBlocking {
databaseHandler.awaitList { saved_searchQueries.selectAll(savedSearchMapper) }.map {
BackupSavedSearch(
it.name,
it.query.orEmpty(),
it.filtersJson ?: "[]",
it.source,
)
}
}
}
// SY <--
@@ -431,25 +434,24 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
// SY -->
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
val currentSavedSearches = databaseHelper.getSavedSearches()
.executeAsBlocking()
internal suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
val currentSavedSearches = databaseHandler.awaitList {
saved_searchQueries.selectAll(savedSearchMapper)
}
val newSavedSearches = backupSavedSearches.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.map {
SavedSearch(
id = null,
it.source,
it.name,
it.query.nullIfBlank(),
filtersJson = it.filterList.nullIfBlank()
?.takeUnless { it == "[]" },
)
}.ifEmpty { null }
if (newSavedSearches != null) {
databaseHelper.insertSavedSearches(newSavedSearches)
databaseHandler.await(true) {
backupSavedSearches.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.forEach {
saved_searchQueries.insertSavedSearch(
_id = null,
source = it.source,
name = it.name,
query = it.query.nullIfBlank(),
filters_json = it.filterList.nullIfBlank()
?.takeUnless { it == "[]" },
)
}
}
}
@@ -76,7 +76,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
}
// SY -->
private fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
private suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
backupManager.restoreSavedSearches(backupSavedSearches)
restoreProgress += 1
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context
import android.net.Uri
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
@@ -289,28 +290,37 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
}
// SY -->
internal fun restoreSavedSearches(jsonSavedSearches: String) {
internal suspend fun restoreSavedSearches(jsonSavedSearches: String) {
val backupSavedSearches = jsonSavedSearches.split("***").toSet()
val currentSavedSearches = databaseHelper.getSavedSearches().executeAsBlocking()
val currentSavedSearches = databaseHandler.awaitList {
saved_searchQueries.selectAll(savedSearchMapper)
}
val newSavedSearches = backupSavedSearches.mapNotNull {
runCatching {
val content = parser.decodeFromString<JsonObject>(it.substringAfter(':'))
SavedSearch(
id = null,
source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null,
content["name"]!!.jsonPrimitive.content,
content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
Json.encodeToString(content["filters"]!!.jsonArray),
databaseHandler.await(true) {
backupSavedSearches.mapNotNull {
runCatching {
val content = parser.decodeFromString<JsonObject>(it.substringAfter(':'))
SavedSearch(
id = null,
source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null,
content["name"]!!.jsonPrimitive.content,
content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
Json.encodeToString(content["filters"]!!.jsonArray),
)
}.getOrNull()
}.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.forEach {
saved_searchQueries.insertSavedSearch(
_id = null,
source = it.source,
name = it.name,
query = it.query.nullIfBlank(),
filters_json = it.filtersJson.nullIfBlank()
?.takeUnless { it == "[]" },
)
}.getOrNull()
}.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.ifEmpty { null }
if (newSavedSearches != null) {
databaseHelper.insertSavedSearches(newSavedSearches)
}
}
}
@@ -73,7 +73,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
}
// SY -->
private fun restoreSavedSearches(savedSearches: String) {
private suspend fun restoreSavedSearches(savedSearches: String) {
backupManager.restoreSavedSearches(savedSearches)
restoreProgress += 1
@@ -1,8 +1,6 @@
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
@@ -76,32 +74,6 @@ fun getReadMangaNotInLibraryQuery() =
)
"""
/**
* Query to get the global feed saved searches
*/
fun getGlobalFeedSavedSearchQuery() =
"""
SELECT ${SavedSearchTable.TABLE}.*
FROM (
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}
"""
/**
* Query to get the manga from the library, with their categories, read and unread count.
*/
@@ -18,6 +18,7 @@ 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.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
@@ -66,41 +67,45 @@ open class FeedController :
}
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
viewScope.launchUI {
if (presenter.hasTooManyFeeds()) {
activity?.toast(R.string.too_many_in_feed)
return@launchUI
}
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()
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
viewScope.launchUI {
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()
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()
}
}
/**
@@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.ui.browse.feed
import android.os.Bundle
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.feedSavedSearchMapper
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
@@ -15,9 +18,12 @@ 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.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import logcat.LogPriority
@@ -35,11 +41,12 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
* 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 database manages the database calls.
* @param preferences manages the preference calls.
*/
open class FeedPresenter(
val sourceManager: SourceManager = Injekt.get(),
val database: DatabaseHandler = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
) : BasePresenter<FeedController>() {
@@ -62,14 +69,11 @@ open class FeedPresenter(
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
db.getGlobalFeedSavedSearches()
.asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnEach {
database.subscribeToList { feed_saved_searchQueries.selectAllGlobal() }
.onEach {
getFeed()
}
.subscribe()
.let(::add)
.launchIn(presenterScope)
}
override fun onDestroy() {
@@ -78,8 +82,10 @@ open class FeedPresenter(
super.onDestroy()
}
fun hasTooManyFeeds(): Boolean {
return db.getGlobalFeedSavedSearches().executeAsBlocking().size > 10
suspend fun hasTooManyFeeds(): Boolean {
return withIOContext {
database.awaitList { feed_saved_searchQueries.selectAllGlobal() }.size > 10
}
}
fun getEnabledSources(): List<CatalogueSource> {
@@ -93,33 +99,38 @@ open class FeedPresenter(
return list.sortedBy { it.id.toString() !in pinnedSources }
}
fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> {
return db.getSavedSearches(source.id).executeAsBlocking()
suspend fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> {
return withIOContext {
database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }
}
}
fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) {
launchIO {
db.insertFeedSavedSearch(
FeedSavedSearch(
id = null,
database.await {
feed_saved_searchQueries.insertFeedSavedSearch(
_id = null,
source = source.id,
savedSearch = savedSearch?.id,
saved_search = savedSearch?.id,
global = true,
),
).executeAsBlocking()
)
}
}
}
fun deleteFeed(feed: FeedSavedSearch) {
launchIO {
db.deleteFeedSavedSearch(feed).executeAsBlocking()
database.await {
feed_saved_searchQueries.deleteById(feed.id ?: return@await)
}
}
}
private fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> {
val savedSearches = db.getGlobalSavedSearchesFeed().executeAsBlocking()
.associateBy { it.id!! }
return db.getGlobalFeedSavedSearches().executeAsBlocking()
private suspend fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> {
val savedSearches = database.awaitList {
feed_saved_searchQueries.selectGlobalFeedSavedSearch(savedSearchMapper)
}.associateBy { it.id }
return database.awaitList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) }
.map { it to savedSearches[it.savedSearch] }
}
@@ -138,7 +149,7 @@ open class FeedPresenter(
/**
* Initiates get manga per feed.
*/
fun getFeed() {
suspend fun getFeed() {
// Create image fetch subscription
initializeFetchImageSubscription()
@@ -44,6 +44,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.openInBrowser
@@ -201,7 +202,7 @@ open class BrowseSourceController(bundle: Bundle) :
// SY -->
this,
presenter.source,
presenter.loadSearches(),
emptyList(),
// SY <--
onFilterClicked = {
showProgressBar()
@@ -216,54 +217,58 @@ open class BrowseSourceController(bundle: Bundle) :
},
// EXH -->
onSaveClicked = {
filterSheet?.context?.let {
val names = presenter.loadSearches().map { it.name }
var searchName = ""
MaterialAlertDialogBuilder(it)
.setTitle(R.string.save_search)
.setTextInput(hint = it.getString(R.string.save_search_hint)) { input ->
searchName = input
}
.setPositiveButton(R.string.action_save) { _, _ ->
if (searchName.isNotBlank() && searchName !in names) {
presenter.saveSearch(searchName.trim(), presenter.query, presenter.sourceFilters)
} else {
it.toast(R.string.save_search_invalid_name)
}
}
.setNegativeButton(R.string.action_cancel, null)
.show()
}
},
onSavedSearchClicked = cb@{ idOfSearch ->
val search = presenter.loadSearch(idOfSearch)
if (search == null) {
viewScope.launchUI {
filterSheet?.context?.let {
val names = presenter.loadSearches().map { it.name }
var searchName = ""
MaterialAlertDialogBuilder(it)
.setTitle(R.string.save_search_failed_to_load)
.setMessage(R.string.save_search_failed_to_load_message)
.setTitle(R.string.save_search)
.setTextInput(hint = it.getString(R.string.save_search_hint)) { input ->
searchName = input
}
.setPositiveButton(R.string.action_save) { _, _ ->
if (searchName.isNotBlank() && searchName !in names) {
presenter.saveSearch(searchName.trim(), presenter.query, presenter.sourceFilters)
} else {
it.toast(R.string.save_search_invalid_name)
}
}
.setNegativeButton(R.string.action_cancel, null)
.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()
showProgressBar()
adapter?.clear()
filterSheet?.dismiss()
presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters)
activity?.invalidateOptionsMenu()
},
onSavedSearchDeleteClicked = cb@{ idToDelete, name ->
onSavedSearchClicked = { idOfSearch ->
viewScope.launchUI {
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@launchUI
}
if (search.filterList == null) {
activity?.toast(R.string.save_search_invalid)
return@launchUI
}
presenter.sourceFilters = FilterList(search.filterList)
filterSheet?.setFilters(presenter.filterItems)
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar()
adapter?.clear()
filterSheet?.dismiss()
presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters)
activity?.invalidateOptionsMenu()
}
},
onSavedSearchDeleteClicked = { idToDelete, name ->
filterSheet?.context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(R.string.save_search_delete)
@@ -277,6 +282,9 @@ open class BrowseSourceController(bundle: Bundle) :
},
// EXH <--
)
launchUI {
filterSheet?.setSavedSearches(presenter.loadSearches())
}
filterSheet?.setFilters(presenter.filterItems)
filterSheet?.setOnShowListener { actionFab?.hide() }
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@@ -37,6 +39,7 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.logcat
@@ -50,8 +53,10 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -78,6 +83,7 @@ open class BrowseSourcePresenter(
// SY <--
private val sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val database: DatabaseHandler = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
) : BasePresenter<BrowseSourceController>() {
@@ -140,7 +146,11 @@ open class BrowseSourcePresenter(
val jsonFilters = filters
if (savedSearchFilters != null) {
runCatching {
val savedSearch = db.getSavedSearch(savedSearchFilters).executeAsBlocking() ?: return@runCatching
val savedSearch = runBlocking {
database.awaitOneOrNull {
saved_searchQueries.selectById(savedSearchFilters, savedSearchMapper)
}
} ?: return@runCatching
query = savedSearch.query.orEmpty()
val filtersJson = savedSearch.filtersJson
?: return@runCatching
@@ -156,18 +166,14 @@ open class BrowseSourcePresenter(
}
}
db.getSavedSearches(source.id)
.asRxObservable()
.map {
loadSearches(it)
database.subscribeToList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }
.map { loadSearches(it) }
.onEach {
withUIContext {
view?.setSavedSearches(it)
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(
{ controller, savedSearches ->
controller.setSavedSearches(savedSearches)
},
)
.launchIn(presenterScope)
// SY <--
if (savedState != null) {
@@ -485,83 +491,90 @@ open class BrowseSourcePresenter(
fun saveSearch(name: String, query: String, filterList: FilterList) {
launchIO {
kotlin.runCatching {
val savedSearch = SavedSearch(
id = null,
source = source.id,
name = name.trim(),
query = query.nullIfBlank(),
filtersJson = filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) },
)
db.insertSavedSearch(savedSearch).executeAsBlocking()
database.await {
saved_searchQueries.insertSavedSearch(
_id = null,
source = source.id,
name = name.trim(),
query = query.nullIfBlank(),
filters_json = filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) },
)
}
}
}
}
fun deleteSearch(searchId: Long) {
launchIO {
db.deleteSavedSearch(searchId).executeAsBlocking()
database.await { saved_searchQueries.deleteById(searchId) }
}
}
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<JsonArray>(it) }
?: return@runCatching null,
)
originalFilters
}.getOrNull(),
)
suspend fun loadSearch(searchId: Long): EXHSavedSearch? {
return withIOContext {
val search = database.awaitOneOrNull {
saved_searchQueries.selectById(searchId, savedSearchMapper)
} ?: return@withIOContext null
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<JsonArray>(it) }
?: return@runCatching null,
)
originalFilters
}.getOrNull(),
)
}
}
fun loadSearches(searches: List<SavedSearch> = db.getSavedSearches(source.id).executeAsBlocking()): List<EXHSavedSearch> {
return searches.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<JsonArray>(filtersJson)
} catch (e: Exception) {
xLogE("Failed to load saved search!", e)
null
} ?: return@map EXHSavedSearch(
id = it.id!!,
name = it.name,
query = it.query.orEmpty(),
filterList = null,
)
suspend fun loadSearches(searches: List<SavedSearch>? = null): List<EXHSavedSearch> {
return withIOContext {
(searches ?: (database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }))
.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<JsonArray>(filtersJson)
} catch (e: Exception) {
xLogE("Failed to load saved search!", e)
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,
)
}
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,
)
}
}
}
}
// EXH <--
@@ -24,6 +24,8 @@ 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.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
@@ -184,7 +186,7 @@ open class SourceFeedController :
// SY -->
this,
presenter.source,
presenter.loadSearches(),
emptyList(),
// SY <--
onFilterClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
@@ -202,51 +204,60 @@ open class SourceFeedController :
},
onResetClicked = {},
onSaveClicked = {},
onSavedSearchClicked = cb@{ idOfSearch ->
val search = presenter.loadSearch(idOfSearch)
onSavedSearchClicked = { idOfSearch ->
viewScope.launchUI {
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)
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@launchUI
}
if (search.filterList == null) {
activity?.toast(R.string.save_search_invalid)
return@launchUI
}
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 ->
viewScope.launchUI {
if (presenter.hasTooManyFeeds()) {
activity?.toast(R.string.too_many_in_feed)
return@launchUI
}
withUIContext {
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()
}
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 = cb@{ idOfSearch, name ->
if (presenter.hasTooManyFeeds()) {
activity?.toast(R.string.too_many_in_feed)
return@cb
}
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()
},
)
launchUI {
filterSheet?.setSavedSearches(presenter.loadSearches())
}
filterSheet?.setFilters(presenter.filterItems)
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.source.feed
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.feedSavedSearchMapper
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
@@ -16,11 +19,15 @@ 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.lang.withIOContext
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.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
@@ -46,11 +53,12 @@ sealed class SourceFeed {
* 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 database manages the database calls.
* @param preferences manages the preference calls.
*/
open class SourceFeedPresenter(
val source: CatalogueSource,
val database: DatabaseHandler = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
) : BasePresenter<SourceFeedController>() {
@@ -90,14 +98,11 @@ open class SourceFeedPresenter(
sourceFilters = source.getFilterList()
db.getSourceFeedSavedSearches(source.id)
.asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnEach {
database.subscribeToList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) }
.onEach {
getFeed()
}
.subscribe()
.let(::add)
.launchIn(presenterScope)
}
override fun onDestroy() {
@@ -106,35 +111,39 @@ open class SourceFeedPresenter(
super.onDestroy()
}
fun hasTooManyFeeds(): Boolean {
return db.getSourceFeedSavedSearches(source.id).executeAsBlocking().size > 10
suspend fun hasTooManyFeeds(): Boolean {
return withIOContext {
database.awaitList {
feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id)
}.size > 10
}
}
fun getSourceSavedSearches(): List<SavedSearch> {
return db.getSavedSearches(source.id).executeAsBlocking()
suspend fun getSourceSavedSearches(): List<SavedSearch> {
return database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }
}
fun createFeed(savedSearchId: Long) {
launchIO {
db.insertFeedSavedSearch(
FeedSavedSearch(
id = null,
database.await {
feed_saved_searchQueries.insertFeedSavedSearch(
_id = null,
source = source.id,
savedSearch = savedSearchId,
global = false,
),
).executeAsBlocking()
saved_search = savedSearchId,
global = false
)
}
}
}
fun deleteFeed(feed: FeedSavedSearch) {
launchIO {
db.deleteFeedSavedSearch(feed).executeAsBlocking()
database.await { feed_saved_searchQueries.deleteById(feed.id ?: return@await) }
}
}
private fun getSourcesToGetFeed(): List<SourceFeed> {
val savedSearches = db.getSourceSavedSearchesFeed(source.id).executeAsBlocking()
private suspend fun getSourcesToGetFeed(): List<SourceFeed> {
val savedSearches = database.awaitList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) }
.associateBy { it.id!! }
return listOfNotNull(
@@ -142,7 +151,7 @@ open class SourceFeedPresenter(
SourceFeed.Latest
} else null,
SourceFeed.Browse,
) + db.getSourceFeedSavedSearches(source.id).executeAsBlocking()
) + database.awaitList { feed_saved_searchQueries.selectBySource(source.id, feedSavedSearchMapper) }
.map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) }
}
@@ -159,7 +168,7 @@ open class SourceFeedPresenter(
/**
* Initiates get manga per feed.
*/
fun getFeed() {
suspend fun getFeed() {
// Create image fetch subscription
initializeFetchImageSubscription()
@@ -300,62 +309,69 @@ open class SourceFeedPresenter(
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<JsonArray>(it) }
?: return@runCatching null,
)
originalFilters
}.getOrNull(),
)
suspend fun loadSearch(searchId: Long): EXHSavedSearch? {
return withIOContext {
val search = database.awaitOneOrNull {
saved_searchQueries.selectById(searchId, savedSearchMapper)
} ?: return@withIOContext null
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<JsonArray>(it) }
?: return@runCatching null,
)
originalFilters
}.getOrNull(),
)
}
}
fun loadSearches(): List<EXHSavedSearch> {
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<JsonArray>(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(
suspend fun loadSearches(): List<EXHSavedSearch> {
return withIOContext {
database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }.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<JsonArray>(filtersJson)
} catch (e: Exception) {
if (e is CancellationException) throw e
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,
)
}
}
}
}