Use domain layer for saved searches

This commit is contained in:
Jobobby04 2022-07-04 18:10:43 -04:00
parent 485e6719c3
commit 141b0477e7
30 changed files with 654 additions and 278 deletions

View File

@ -0,0 +1,73 @@
package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.feedSavedSearchMapper
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.Flow
class FeedSavedSearchRepositoryImpl(
private val handler: DatabaseHandler,
) : FeedSavedSearchRepository {
override suspend fun getGlobal(): List<FeedSavedSearch> {
return handler.awaitList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) }
}
override fun getGlobalAsFlow(): Flow<List<FeedSavedSearch>> {
return handler.subscribeToList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) }
}
override suspend fun getGlobalFeedSavedSearch(): List<SavedSearch> {
return handler.awaitList { feed_saved_searchQueries.selectGlobalFeedSavedSearch(savedSearchMapper) }
}
override suspend fun countGlobal(): Long {
return handler.awaitOne { feed_saved_searchQueries.countGlobal() }
}
override suspend fun getBySourceId(sourceId: Long): List<FeedSavedSearch> {
return handler.awaitList { feed_saved_searchQueries.selectBySource(sourceId, feedSavedSearchMapper) }
}
override fun getBySourceIdAsFlow(sourceId: Long): Flow<List<FeedSavedSearch>> {
return handler.subscribeToList { feed_saved_searchQueries.selectBySource(sourceId, feedSavedSearchMapper) }
}
override suspend fun getBySourceIdFeedSavedSearch(sourceId: Long): List<SavedSearch> {
return handler.awaitList { feed_saved_searchQueries.selectSourceFeedSavedSearch(sourceId, savedSearchMapper) }
}
override suspend fun countBySourceId(sourceId: Long): Long {
return handler.awaitOne { feed_saved_searchQueries.countSourceFeedSavedSearch(sourceId) }
}
override suspend fun delete(feedSavedSearchId: Long) {
handler.await { feed_saved_searchQueries.deleteById(feedSavedSearchId) }
}
override suspend fun insert(feedSavedSearch: FeedSavedSearch): Long {
return handler.awaitOne(true) {
feed_saved_searchQueries.insert(
feedSavedSearch.source,
feedSavedSearch.savedSearch,
feedSavedSearch.global,
)
feed_saved_searchQueries.selectLastInsertedRowId()
}
}
override suspend fun insertAll(feedSavedSearch: List<FeedSavedSearch>) {
return handler.await(true) {
feedSavedSearch.forEach {
feed_saved_searchQueries.insert(
it.source,
it.savedSearch,
it.global,
)
}
}
}
}

View File

@ -0,0 +1,53 @@
package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.domain.source.repository.SavedSearchRepository
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.Flow
class SavedSearchRepositoryImpl(
private val handler: DatabaseHandler,
) : SavedSearchRepository {
override suspend fun getById(savedSearchId: Long): SavedSearch? {
return handler.awaitOneOrNull { saved_searchQueries.selectById(savedSearchId, savedSearchMapper) }
}
override suspend fun getBySourceId(sourceId: Long): List<SavedSearch> {
return handler.awaitList { saved_searchQueries.selectBySource(sourceId, savedSearchMapper) }
}
override fun getBySourceIdAsFlow(sourceId: Long): Flow<List<SavedSearch>> {
return handler.subscribeToList { saved_searchQueries.selectBySource(sourceId, savedSearchMapper) }
}
override suspend fun delete(savedSearchId: Long) {
handler.await { saved_searchQueries.deleteById(savedSearchId) }
}
override suspend fun insert(savedSearch: SavedSearch): Long {
return handler.awaitOne(true) {
saved_searchQueries.insert(
savedSearch.source,
savedSearch.name,
savedSearch.query,
savedSearch.filtersJson,
)
saved_searchQueries.selectLastInsertedRowId()
}
}
override suspend fun insertAll(savedSearch: List<SavedSearch>) {
handler.await(true) {
savedSearch.forEach {
saved_searchQueries.insert(
it.source,
it.name,
it.query,
it.filtersJson,
)
}
}
}
}

View File

@ -3,6 +3,8 @@ package eu.kanade.domain
import eu.kanade.data.manga.FavoritesEntryRepositoryImpl import eu.kanade.data.manga.FavoritesEntryRepositoryImpl
import eu.kanade.data.manga.MangaMergeRepositoryImpl import eu.kanade.data.manga.MangaMergeRepositoryImpl
import eu.kanade.data.manga.MangaMetadataRepositoryImpl import eu.kanade.data.manga.MangaMetadataRepositoryImpl
import eu.kanade.data.source.FeedSavedSearchRepositoryImpl
import eu.kanade.data.source.SavedSearchRepositoryImpl
import eu.kanade.domain.chapter.interactor.DeleteChapters import eu.kanade.domain.chapter.interactor.DeleteChapters
import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId
import eu.kanade.domain.manga.interactor.DeleteByMergeId import eu.kanade.domain.manga.interactor.DeleteByMergeId
@ -19,6 +21,7 @@ import eu.kanade.domain.manga.interactor.GetMergedManga
import eu.kanade.domain.manga.interactor.GetMergedMangaById import eu.kanade.domain.manga.interactor.GetMergedMangaById
import eu.kanade.domain.manga.interactor.GetMergedMangaForDownloading import eu.kanade.domain.manga.interactor.GetMergedMangaForDownloading
import eu.kanade.domain.manga.interactor.GetMergedReferencesById import eu.kanade.domain.manga.interactor.GetMergedReferencesById
import eu.kanade.domain.manga.interactor.GetSearchMetadata
import eu.kanade.domain.manga.interactor.GetSearchTags import eu.kanade.domain.manga.interactor.GetSearchTags
import eu.kanade.domain.manga.interactor.GetSearchTitles import eu.kanade.domain.manga.interactor.GetSearchTitles
import eu.kanade.domain.manga.interactor.InsertFavoriteEntries import eu.kanade.domain.manga.interactor.InsertFavoriteEntries
@ -29,16 +32,32 @@ import eu.kanade.domain.manga.interactor.UpdateMergedSettings
import eu.kanade.domain.manga.repository.FavoritesEntryRepository import eu.kanade.domain.manga.repository.FavoritesEntryRepository
import eu.kanade.domain.manga.repository.MangaMergeRepository import eu.kanade.domain.manga.repository.MangaMergeRepository
import eu.kanade.domain.manga.repository.MangaMetadataRepository import eu.kanade.domain.manga.repository.MangaMetadataRepository
import eu.kanade.domain.source.interactor.CountFeedSavedSearchBySourceId
import eu.kanade.domain.source.interactor.CountFeedSavedSearchGlobal
import eu.kanade.domain.source.interactor.DeleteFeedSavedSearchById
import eu.kanade.domain.source.interactor.DeleteSavedSearchById
import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId
import eu.kanade.domain.source.interactor.GetFeedSavedSearchGlobal
import eu.kanade.domain.source.interactor.GetSavedSearchById
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceId
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
import eu.kanade.domain.source.interactor.GetSavedSearchGlobalFeed
import eu.kanade.domain.source.interactor.GetShowLatest import eu.kanade.domain.source.interactor.GetShowLatest
import eu.kanade.domain.source.interactor.GetSourceCategories import eu.kanade.domain.source.interactor.GetSourceCategories
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
import eu.kanade.domain.source.interactor.InsertSavedSearch
import eu.kanade.domain.source.interactor.SetSourceCategories import eu.kanade.domain.source.interactor.SetSourceCategories
import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
import eu.kanade.domain.source.interactor.ToggleSources import eu.kanade.domain.source.interactor.ToggleSources
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import eu.kanade.domain.source.repository.SavedSearchRepository
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
class SYDomainModule : InjektModule { class SYDomainModule : InjektModule {
@ -53,11 +72,13 @@ class SYDomainModule : InjektModule {
addFactory { GetMangaBySource(get()) } addFactory { GetMangaBySource(get()) }
addFactory { DeleteChapters(get()) } addFactory { DeleteChapters(get()) }
addFactory { DeleteMangaById(get()) } addFactory { DeleteMangaById(get()) }
addFactory { FilterSerializer() }
addSingletonFactory<MangaMetadataRepository> { MangaMetadataRepositoryImpl(get()) } addSingletonFactory<MangaMetadataRepository> { MangaMetadataRepositoryImpl(get()) }
addFactory { GetFlatMetadataById(get()) } addFactory { GetFlatMetadataById(get()) }
addFactory { InsertFlatMetadata(get()) } addFactory { InsertFlatMetadata(get()) }
addFactory { GetExhFavoriteMangaWithMetadata(get()) } addFactory { GetExhFavoriteMangaWithMetadata(get()) }
addFactory { GetSearchMetadata(get()) }
addFactory { GetSearchTags(get()) } addFactory { GetSearchTags(get()) }
addFactory { GetSearchTitles(get()) } addFactory { GetSearchTitles(get()) }
addFactory { GetIdsOfFavoriteMangaWithMetadata(get()) } addFactory { GetIdsOfFavoriteMangaWithMetadata(get()) }
@ -77,5 +98,22 @@ class SYDomainModule : InjektModule {
addFactory { GetFavoriteEntries(get()) } addFactory { GetFavoriteEntries(get()) }
addFactory { InsertFavoriteEntries(get()) } addFactory { InsertFavoriteEntries(get()) }
addFactory { DeleteFavoriteEntries(get()) } addFactory { DeleteFavoriteEntries(get()) }
addSingletonFactory<SavedSearchRepository> { SavedSearchRepositoryImpl(get()) }
addFactory { GetSavedSearchById(get()) }
addFactory { GetSavedSearchBySourceId(get()) }
addFactory { DeleteSavedSearchById(get()) }
addFactory { InsertSavedSearch(get()) }
addFactory { GetExhSavedSearch(get(), get(), get()) }
addSingletonFactory<FeedSavedSearchRepository> { FeedSavedSearchRepositoryImpl(get()) }
addFactory { InsertFeedSavedSearch(get()) }
addFactory { DeleteFeedSavedSearchById(get()) }
addFactory { GetFeedSavedSearchGlobal(get()) }
addFactory { GetFeedSavedSearchBySourceId(get()) }
addFactory { CountFeedSavedSearchGlobal(get()) }
addFactory { CountFeedSavedSearchBySourceId(get()) }
addFactory { GetSavedSearchGlobalFeed(get()) }
addFactory { GetSavedSearchBySourceIdFeed(get()) }
} }
} }

View File

@ -0,0 +1,12 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
class CountFeedSavedSearchBySourceId(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun await(sourceId: Long): Long {
return feedSavedSearchRepository.countBySourceId(sourceId)
}
}

View File

@ -0,0 +1,12 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
class CountFeedSavedSearchGlobal(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun await(): Long {
return feedSavedSearchRepository.countGlobal()
}
}

View File

@ -0,0 +1,12 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
class DeleteFeedSavedSearchById(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun await(feedSavedSearchId: Long) {
return feedSavedSearchRepository.delete(feedSavedSearchId)
}
}

View File

@ -0,0 +1,12 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.SavedSearchRepository
class DeleteSavedSearchById(
private val savedSearchRepository: SavedSearchRepository,
) {
suspend fun await(savedSearchId: Long) {
return savedSearchRepository.delete(savedSearchId)
}
}

View File

@ -0,0 +1,70 @@
package eu.kanade.domain.source.interactor
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.log.xLogE
import exh.savedsearches.EXHSavedSearch
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
class GetExhSavedSearch(
private val getSavedSearchById: GetSavedSearchById,
private val getSavedSearchBySourceId: GetSavedSearchBySourceId,
private val filterSerializer: FilterSerializer,
) {
suspend fun awaitOne(savedSearchId: Long, getFilterList: () -> FilterList): EXHSavedSearch? {
val search = getSavedSearchById.awaitOrNull(savedSearchId) ?: return null
return withIOContext { loadSearch(search, getFilterList) }
}
suspend fun await(sourceId: Long, getFilterList: () -> FilterList): List<EXHSavedSearch> {
return withIOContext { loadSearches(getSavedSearchBySourceId.await(sourceId), getFilterList) }
}
fun subscribe(sourceId: Long, getFilterList: () -> FilterList): Flow<List<EXHSavedSearch>> {
return getSavedSearchBySourceId.subscribe(sourceId)
.map { loadSearches(it, getFilterList) }
.flowOn(Dispatchers.IO)
}
private fun loadSearches(searches: List<SavedSearch>, getFilterList: () -> FilterList): List<EXHSavedSearch> {
return searches.map { loadSearch(it, getFilterList) }
}
private fun loadSearch(search: SavedSearch, getFilterList: () -> FilterList): EXHSavedSearch {
val filters = getFilters(search.filtersJson)
return EXHSavedSearch(
id = search.id,
name = search.name,
query = search.query.orEmpty(),
filterList = filters?.let { deserializeFilters(it, getFilterList) },
)
}
private fun getFilters(filtersJson: String?): JsonArray? {
return runCatching {
filtersJson?.let { Json.decodeFromString<JsonArray>(it) }
}.onFailure {
xLogE("Failed to load saved search!", it)
}.getOrNull()
}
private fun deserializeFilters(filters: JsonArray, getFilterList: () -> FilterList): FilterList? {
return runCatching {
val originalFilters = getFilterList()
filterSerializer.deserialize(originalFilters, filters)
originalFilters
}.onFailure {
xLogE("Failed to load saved search!", it)
}.getOrNull()
}
}

View File

@ -0,0 +1,18 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import exh.savedsearches.models.FeedSavedSearch
import kotlinx.coroutines.flow.Flow
class GetFeedSavedSearchBySourceId(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun await(sourceId: Long): List<FeedSavedSearch> {
return feedSavedSearchRepository.getBySourceId(sourceId)
}
fun subscribe(sourceId: Long): Flow<List<FeedSavedSearch>> {
return feedSavedSearchRepository.getBySourceIdAsFlow(sourceId)
}
}

View File

@ -0,0 +1,18 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import exh.savedsearches.models.FeedSavedSearch
import kotlinx.coroutines.flow.Flow
class GetFeedSavedSearchGlobal(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun await(): List<FeedSavedSearch> {
return feedSavedSearchRepository.getGlobal()
}
fun subscribe(): Flow<List<FeedSavedSearch>> {
return feedSavedSearchRepository.getGlobalAsFlow()
}
}

View File

@ -0,0 +1,17 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.SavedSearchRepository
import exh.savedsearches.models.SavedSearch
class GetSavedSearchById(
private val savedSearchRepository: SavedSearchRepository,
) {
suspend fun await(savedSearchId: Long): SavedSearch {
return savedSearchRepository.getById(savedSearchId)!!
}
suspend fun awaitOrNull(savedSearchId: Long): SavedSearch? {
return savedSearchRepository.getById(savedSearchId)
}
}

View File

@ -0,0 +1,18 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.SavedSearchRepository
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.Flow
class GetSavedSearchBySourceId(
private val savedSearchRepository: SavedSearchRepository,
) {
suspend fun await(sourceId: Long): List<SavedSearch> {
return savedSearchRepository.getBySourceId(sourceId)
}
fun subscribe(sourceId: Long): Flow<List<SavedSearch>> {
return savedSearchRepository.getBySourceIdAsFlow(sourceId)
}
}

View File

@ -0,0 +1,13 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import exh.savedsearches.models.SavedSearch
class GetSavedSearchBySourceIdFeed(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun await(sourceId: Long): List<SavedSearch> {
return feedSavedSearchRepository.getBySourceIdFeedSavedSearch(sourceId)
}
}

View File

@ -0,0 +1,13 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import exh.savedsearches.models.SavedSearch
class GetSavedSearchGlobalFeed(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun await(): List<SavedSearch> {
return feedSavedSearchRepository.getGlobalFeedSavedSearch()
}
}

View File

@ -0,0 +1,29 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
import eu.kanade.tachiyomi.util.system.logcat
import exh.savedsearches.models.FeedSavedSearch
import logcat.LogPriority
import logcat.asLog
class InsertFeedSavedSearch(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun await(feedSavedSearch: FeedSavedSearch): Long? {
return try {
feedSavedSearchRepository.insert(feedSavedSearch)
} catch (e: Exception) {
logcat(LogPriority.ERROR) { e.asLog() }
null
}
}
suspend fun awaitAll(feedSavedSearch: List<FeedSavedSearch>) {
try {
feedSavedSearchRepository.insertAll(feedSavedSearch)
} catch (e: Exception) {
logcat(LogPriority.ERROR) { e.asLog() }
}
}
}

View File

@ -0,0 +1,29 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.repository.SavedSearchRepository
import eu.kanade.tachiyomi.util.system.logcat
import exh.savedsearches.models.SavedSearch
import logcat.LogPriority
import logcat.asLog
class InsertSavedSearch(
private val savedSearchRepository: SavedSearchRepository,
) {
suspend fun await(savedSearch: SavedSearch): Long? {
return try {
savedSearchRepository.insert(savedSearch)
} catch (e: Exception) {
logcat(LogPriority.ERROR) { e.asLog() }
null
}
}
suspend fun awaitAll(savedSearch: List<SavedSearch>) {
try {
savedSearchRepository.insertAll(savedSearch)
} catch (e: Exception) {
logcat(LogPriority.ERROR) { e.asLog() }
}
}
}

View File

@ -0,0 +1,30 @@
package eu.kanade.domain.source.repository
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.Flow
interface FeedSavedSearchRepository {
suspend fun getGlobal(): List<FeedSavedSearch>
fun getGlobalAsFlow(): Flow<List<FeedSavedSearch>>
suspend fun getGlobalFeedSavedSearch(): List<SavedSearch>
suspend fun countGlobal(): Long
suspend fun getBySourceId(sourceId: Long): List<FeedSavedSearch>
fun getBySourceIdAsFlow(sourceId: Long): Flow<List<FeedSavedSearch>>
suspend fun getBySourceIdFeedSavedSearch(sourceId: Long): List<SavedSearch>
suspend fun countBySourceId(sourceId: Long): Long
suspend fun delete(feedSavedSearchId: Long)
suspend fun insert(feedSavedSearch: FeedSavedSearch): Long?
suspend fun insertAll(feedSavedSearch: List<FeedSavedSearch>)
}

View File

@ -0,0 +1,19 @@
package eu.kanade.domain.source.repository
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.Flow
interface SavedSearchRepository {
suspend fun getById(savedSearchId: Long): SavedSearch?
suspend fun getBySourceId(sourceId: Long): List<SavedSearch>
fun getBySourceIdAsFlow(sourceId: Long): Flow<List<SavedSearch>>
suspend fun delete(savedSearchId: Long)
suspend fun insert(savedSearch: SavedSearch): Long?
suspend fun insertAll(savedSearch: List<SavedSearch>)
}

View File

@ -500,12 +500,11 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
backupSavedSearches.filter { backupSavedSearch -> backupSavedSearches.filter { backupSavedSearch ->
currentSavedSearches.none { it.source == backupSavedSearch.source && it.name == backupSavedSearch.name } currentSavedSearches.none { it.source == backupSavedSearch.source && it.name == backupSavedSearch.name }
}.forEach { }.forEach {
saved_searchQueries.insertSavedSearch( saved_searchQueries.insert(
_id = null,
source = it.source, source = it.source,
name = it.name, name = it.name,
query = it.query.nullIfBlank(), query = it.query.nullIfBlank(),
filters_json = it.filterList.nullIfBlank() filtersJson = it.filterList.nullIfBlank()
?.takeUnless { it == "[]" }, ?.takeUnless { it == "[]" },
) )
} }

View File

@ -91,7 +91,7 @@ open class FeedController :
private fun addFeedSearch(source: CatalogueSource) { private fun addFeedSearch(source: CatalogueSource) {
viewScope.launchUI { viewScope.launchUI {
val items = presenter.getSourceSavedSearches(source) val items = presenter.getSourceSavedSearches(source.id)
val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name } val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name }
var selectedIndex = 0 var selectedIndex = 0

View File

@ -73,6 +73,6 @@ class FeedItem(
* @return hashcode * @return hashcode
*/ */
override fun hashCode(): Int { override fun hashCode(): Int {
return feed.id!!.toInt() return feed.id.toInt()
} }
} }

View File

@ -1,14 +1,17 @@
package eu.kanade.tachiyomi.ui.browse.feed package eu.kanade.tachiyomi.ui.browse.feed
import android.os.Bundle import android.os.Bundle
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.feedSavedSearchMapper
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.InsertManga import eu.kanade.domain.manga.interactor.InsertManga
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDbManga import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.manga.model.toMangaUpdate import eu.kanade.domain.manga.model.toMangaUpdate
import eu.kanade.domain.source.interactor.CountFeedSavedSearchGlobal
import eu.kanade.domain.source.interactor.DeleteFeedSavedSearchById
import eu.kanade.domain.source.interactor.GetFeedSavedSearchGlobal
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceId
import eu.kanade.domain.source.interactor.GetSavedSearchGlobalFeed
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toDomainManga import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
@ -46,16 +49,20 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
* Function calls should be done from here. UI calls should be done from the controller. * Function calls should be done from here. UI calls should be done from the controller.
* *
* @param sourceManager manages the different sources. * @param sourceManager manages the different sources.
* @param handler manages the database calls.
* @param preferences manages the preference calls. * @param preferences manages the preference calls.
*/ */
open class FeedPresenter( open class FeedPresenter(
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val handler: DatabaseHandler = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val insertManga: InsertManga = Injekt.get(), private val insertManga: InsertManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val getFeedSavedSearchGlobal: GetFeedSavedSearchGlobal = Injekt.get(),
private val getSavedSearchGlobalFeed: GetSavedSearchGlobalFeed = Injekt.get(),
private val countFeedSavedSearchGlobal: CountFeedSavedSearchGlobal = Injekt.get(),
private val getSavedSearchBySourceId: GetSavedSearchBySourceId = Injekt.get(),
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
) : BasePresenter<FeedController>() { ) : BasePresenter<FeedController>() {
/** /**
@ -76,9 +83,9 @@ open class FeedPresenter(
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
handler.subscribeToList { feed_saved_searchQueries.selectAllGlobal() } getFeedSavedSearchGlobal.subscribe()
.onEach { .onEach {
getFeed() getFeed(it)
} }
.launchIn(presenterScope) .launchIn(presenterScope)
} }
@ -90,7 +97,7 @@ open class FeedPresenter(
} }
suspend fun hasTooManyFeeds(): Boolean { suspend fun hasTooManyFeeds(): Boolean {
return handler.awaitOne { feed_saved_searchQueries.countGlobal() } > 10 return countFeedSavedSearchGlobal.await() > 10
} }
fun getEnabledSources(): List<CatalogueSource> { fun getEnabledSources(): List<CatalogueSource> {
@ -104,36 +111,33 @@ open class FeedPresenter(
return list.sortedBy { it.id.toString() !in pinnedSources } return list.sortedBy { it.id.toString() !in pinnedSources }
} }
suspend fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> { suspend fun getSourceSavedSearches(sourceId: Long): List<SavedSearch> {
return handler.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) } return getSavedSearchBySourceId.await(sourceId)
} }
fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) { fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) {
launchIO { launchIO {
handler.await { insertFeedSavedSearch.await(
feed_saved_searchQueries.insertFeedSavedSearch( FeedSavedSearch(
_id = null, id = -1,
source = source.id, source = source.id,
saved_search = savedSearch?.id, savedSearch = savedSearch?.id,
global = true, global = true,
) ),
} )
} }
} }
fun deleteFeed(feed: FeedSavedSearch) { fun deleteFeed(feed: FeedSavedSearch) {
launchIO { launchIO {
handler.await { deleteFeedSavedSearchById.await(feed.id)
feed_saved_searchQueries.deleteById(feed.id ?: return@await)
}
} }
} }
private suspend fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> { private suspend fun getSourcesToGetFeed(feedSavedSearch: List<FeedSavedSearch>): List<Pair<FeedSavedSearch, SavedSearch?>> {
val savedSearches = handler.awaitList { val savedSearches = getSavedSearchGlobalFeed.await()
feed_saved_searchQueries.selectGlobalFeedSavedSearch(savedSearchMapper) .associateBy { it.id }
}.associateBy { it.id } return feedSavedSearch
return handler.awaitList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) }
.map { it to savedSearches[it.savedSearch] } .map { it to savedSearches[it.savedSearch] }
} }
@ -152,12 +156,12 @@ open class FeedPresenter(
/** /**
* Initiates get manga per feed. * Initiates get manga per feed.
*/ */
suspend fun getFeed() { private suspend fun getFeed(feedSavedSearch: List<FeedSavedSearch>) {
// Create image fetch subscription // Create image fetch subscription
initializeFetchImageSubscription() initializeFetchImageSubscription()
// Create items with the initial state // Create items with the initial state
val initialItems = getSourcesToGetFeed().map { (feed, savedSearch) -> val initialItems = getSourcesToGetFeed(feedSavedSearch).map { (feed, savedSearch) ->
createCatalogueSearchItem( createCatalogueSearchItem(
feed, feed,
savedSearch, savedSearch,
@ -168,7 +172,7 @@ open class FeedPresenter(
var items = initialItems var items = initialItems
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchSourcesSubscription = Observable.from(getSourcesToGetFeed()) fetchSourcesSubscription = Observable.from(getSourcesToGetFeed(feedSavedSearch))
.flatMap( .flatMap(
{ (feed, savedSearch) -> { (feed, savedSearch) ->
val source = sourceManager.get(feed.source) as? CatalogueSource val source = sourceManager.get(feed.source) as? CatalogueSource

View File

@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.interactor.SetMangaCategories
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
@ -14,6 +12,9 @@ import eu.kanade.domain.manga.interactor.InsertManga
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDbManga import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.manga.model.toMangaUpdate import eu.kanade.domain.manga.model.toMangaUpdate
import eu.kanade.domain.source.interactor.DeleteSavedSearchById
import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.InsertSavedSearch
import eu.kanade.domain.track.interactor.InsertTrack import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
@ -48,12 +49,9 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.lang.launchIO 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.lang.withUIContext
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import exh.log.xLogE
import exh.savedsearches.EXHSavedSearch
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import exh.util.nullIfBlank import exh.util.nullIfBlank
@ -90,7 +88,6 @@ open class BrowseSourcePresenter(
private val savedSearch: Long? = null, private val savedSearch: Long? = null,
// SY <-- // SY <--
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val database: DatabaseHandler = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(), private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
@ -102,6 +99,11 @@ open class BrowseSourcePresenter(
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(),
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(), private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
// SY -->
private val deleteSavedSearchById: DeleteSavedSearchById = Injekt.get(),
private val insertSavedSearch: InsertSavedSearch = Injekt.get(),
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
// SY <--
) : BasePresenter<BrowseSourceController>() { ) : BasePresenter<BrowseSourceController>() {
/** /**
@ -161,18 +163,12 @@ open class BrowseSourcePresenter(
val savedSearchFilters = savedSearch val savedSearchFilters = savedSearch
val jsonFilters = filters val jsonFilters = filters
if (savedSearchFilters != null) { if (savedSearchFilters != null) {
runCatching { val savedSearch = runBlocking { getExhSavedSearch.awaitOne(savedSearchFilters) { sourceFilters } }
val savedSearch = runBlocking { if (savedSearch != null) {
database.awaitOneOrNull { query = savedSearch.query
saved_searchQueries.selectById(savedSearchFilters, savedSearchMapper) if (savedSearch.filterList != null) {
} appliedFilters = savedSearch.filterList
} ?: return@runCatching }
query = savedSearch.query.orEmpty()
val filtersJson = savedSearch.filtersJson
?: return@runCatching
val filters = Json.decodeFromString<JsonArray>(filtersJson)
filterSerializer.deserialize(sourceFilters, filters)
appliedFilters = sourceFilters
} }
} else if (jsonFilters != null) { } else if (jsonFilters != null) {
runCatching { runCatching {
@ -182,8 +178,7 @@ open class BrowseSourcePresenter(
} }
} }
database.subscribeToList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) } getExhSavedSearch.subscribe(source.id, source::getFilterList)
.map { loadSearches(it) }
.onEach { .onEach {
withUIContext { withUIContext {
view?.setSavedSearches(it) view?.setSavedSearches(it)
@ -529,92 +524,28 @@ open class BrowseSourcePresenter(
// EXH --> // EXH -->
fun saveSearch(name: String, query: String, filterList: FilterList) { fun saveSearch(name: String, query: String, filterList: FilterList) {
launchIO { launchIO {
kotlin.runCatching { insertSavedSearch.await(
database.await { SavedSearch(
saved_searchQueries.insertSavedSearch( id = -1,
_id = null, source = source.id,
source = source.id, name = name.trim(),
name = name.trim(), query = query.nullIfBlank(),
query = query.nullIfBlank(), filtersJson = runCatching { filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) } }.getOrNull(),
filters_json = filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) }, ),
)
}
}
}
}
fun deleteSearch(searchId: Long) {
launchIO {
database.await { saved_searchQueries.deleteById(searchId) }
}
}
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(),
) )
} }
} }
suspend fun loadSearches(searches: List<SavedSearch>? = null): List<EXHSavedSearch> { fun deleteSearch(savedSearchId: Long) {
return withIOContext { launchIO {
(searches ?: (database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) })) deleteSavedSearchById.await(savedSearchId)
.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,
)
}
}
} }
} }
suspend fun loadSearch(searchId: Long) =
getExhSavedSearch.awaitOne(searchId, source::getFilterList)
suspend fun loadSearches() =
getExhSavedSearch.await(source.id, source::getFilterList)
// EXH <-- // EXH <--
} }

View File

@ -2,14 +2,18 @@ package eu.kanade.tachiyomi.ui.browse.source.feed
import android.os.Bundle import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible 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.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.InsertManga import eu.kanade.domain.manga.interactor.InsertManga
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDbManga import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.manga.model.toMangaUpdate import eu.kanade.domain.manga.model.toMangaUpdate
import eu.kanade.domain.source.interactor.CountFeedSavedSearchBySourceId
import eu.kanade.domain.source.interactor.DeleteFeedSavedSearchById
import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceId
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toDomainManga import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
@ -24,19 +28,14 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import exh.log.xLogE
import exh.savedsearches.EXHSavedSearch
import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import logcat.LogPriority import logcat.LogPriority
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
@ -63,11 +62,17 @@ sealed class SourceFeed {
*/ */
open class SourceFeedPresenter( open class SourceFeedPresenter(
val source: CatalogueSource, val source: CatalogueSource,
val handler: DatabaseHandler = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val insertManga: InsertManga = Injekt.get(), private val insertManga: InsertManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val getFeedSavedSearchBySourceId: GetFeedSavedSearchBySourceId = Injekt.get(),
private val getSavedSearchBySourceIdFeed: GetSavedSearchBySourceIdFeed = Injekt.get(),
private val countFeedSavedSearchBySourceId: CountFeedSavedSearchBySourceId = Injekt.get(),
private val getSavedSearchBySourceId: GetSavedSearchBySourceId = Injekt.get(),
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
) : BasePresenter<SourceFeedController>() { ) : BasePresenter<SourceFeedController>() {
/** /**
@ -105,9 +110,9 @@ open class SourceFeedPresenter(
sourceFilters = source.getFilterList() sourceFilters = source.getFilterList()
handler.subscribeToList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) } getFeedSavedSearchBySourceId.subscribe(source.id)
.onEach { .onEach {
getFeed() getFeed(it)
} }
.launchIn(presenterScope) .launchIn(presenterScope)
} }
@ -119,46 +124,42 @@ open class SourceFeedPresenter(
} }
suspend fun hasTooManyFeeds(): Boolean { suspend fun hasTooManyFeeds(): Boolean {
return withIOContext { return countFeedSavedSearchBySourceId.await(source.id) > 10
handler.awaitList {
feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id)
}.size > 10
}
} }
suspend fun getSourceSavedSearches(): List<SavedSearch> { suspend fun getSourceSavedSearches(): List<SavedSearch> {
return handler.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) } return getSavedSearchBySourceId.await(source.id)
} }
fun createFeed(savedSearchId: Long) { fun createFeed(savedSearchId: Long) {
launchIO { launchIO {
handler.await { insertFeedSavedSearch.await(
feed_saved_searchQueries.insertFeedSavedSearch( FeedSavedSearch(
_id = null, id = -1,
source = source.id, source = source.id,
saved_search = savedSearchId, savedSearch = savedSearchId,
global = false, global = false,
) ),
} )
} }
} }
fun deleteFeed(feed: FeedSavedSearch) { fun deleteFeed(feed: FeedSavedSearch) {
launchIO { launchIO {
handler.await { feed_saved_searchQueries.deleteById(feed.id ?: return@await) } deleteFeedSavedSearchById.await(feed.id)
} }
} }
private suspend fun getSourcesToGetFeed(): List<SourceFeed> { private suspend fun getSourcesToGetFeed(feedSavedSearch: List<FeedSavedSearch>): List<SourceFeed> {
val savedSearches = handler.awaitList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) } val savedSearches = getSavedSearchBySourceIdFeed.await(source.id)
.associateBy { it.id!! } .associateBy { it.id }
return listOfNotNull( return listOfNotNull(
if (source.supportsLatest) { if (source.supportsLatest) {
SourceFeed.Latest SourceFeed.Latest
} else null, } else null,
SourceFeed.Browse, SourceFeed.Browse,
) + handler.awaitList { feed_saved_searchQueries.selectBySource(source.id, feedSavedSearchMapper) } ) + feedSavedSearch
.map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) } .map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) }
} }
@ -175,12 +176,12 @@ open class SourceFeedPresenter(
/** /**
* Initiates get manga per feed. * Initiates get manga per feed.
*/ */
suspend fun getFeed() { private suspend fun getFeed(feedSavedSearch: List<FeedSavedSearch>) {
// Create image fetch subscription // Create image fetch subscription
initializeFetchImageSubscription() initializeFetchImageSubscription()
// Create items with the initial state // Create items with the initial state
val initialItems = getSourcesToGetFeed().map { val initialItems = getSourcesToGetFeed(feedSavedSearch).map {
createCatalogueSearchItem( createCatalogueSearchItem(
it, it,
null, null,
@ -189,7 +190,7 @@ open class SourceFeedPresenter(
var items = initialItems var items = initialItems
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchSourcesSubscription = Observable.from(getSourcesToGetFeed()) fetchSourcesSubscription = Observable.from(getSourcesToGetFeed(feedSavedSearch))
.flatMap( .flatMap(
{ sourceFeed -> { sourceFeed ->
Observable.defer { Observable.defer {
@ -323,70 +324,9 @@ open class SourceFeedPresenter(
return localManga?.toDbManga()!! return localManga?.toDbManga()!!
} }
suspend fun loadSearch(searchId: Long): EXHSavedSearch? { suspend fun loadSearch(searchId: Long) =
return withIOContext { getExhSavedSearch.awaitOne(searchId, source::getFilterList)
val search = handler.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(),
)
}
}
suspend fun loadSearches(): List<EXHSavedSearch> { suspend fun loadSearches() =
return withIOContext { getExhSavedSearch.await(source.id, source::getFilterList)
handler.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,
)
}
}
}
}
} }

View File

@ -14,6 +14,8 @@ import eu.kanade.domain.manga.interactor.GetMangaBySource
import eu.kanade.domain.manga.interactor.InsertMergedReference import eu.kanade.domain.manga.interactor.InsertMergedReference
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.MangaUpdate import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
import eu.kanade.domain.source.interactor.InsertSavedSearch
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -40,6 +42,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import exh.eh.EHentaiUpdateWorker import exh.eh.EHentaiUpdateWorker
import exh.log.xLogE import exh.log.xLogE
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.HBROWSE_SOURCE_ID import exh.source.HBROWSE_SOURCE_ID
@ -76,6 +80,8 @@ object EXHMigrations {
private val updateChapter: UpdateChapter by injectLazy() private val updateChapter: UpdateChapter by injectLazy()
private val deleteChapters: DeleteChapters by injectLazy() private val deleteChapters: DeleteChapters by injectLazy()
private val insertMergedReference: InsertMergedReference by injectLazy() private val insertMergedReference: InsertMergedReference by injectLazy()
private val insertSavedSearch: InsertSavedSearch by injectLazy()
private val insertFeedSavedSearch: InsertFeedSavedSearch by injectLazy()
/** /**
* Performs a migration when the application is updated. * Performs a migration when the application is updated.
@ -360,29 +366,32 @@ object EXHMigrations {
} }
if (oldVersion under 31) { if (oldVersion under 31) {
runBlocking { runBlocking {
handler.await(true) { val savedSearch = prefs.getStringSet("eh_saved_searches", emptySet())?.mapNotNull {
prefs.getStringSet("eh_saved_searches", emptySet())?.forEach { runCatching {
kotlin.runCatching { val content = Json.decodeFromString<JsonObject>(it.substringAfter(':'))
val content = Json.decodeFromString<JsonObject>(it.substringAfter(':')) SavedSearch(
saved_searchQueries.insertSavedSearch( id = -1,
_id = null, source = it.substringBefore(':').toLongOrNull()
source = it.substringBefore(':').toLongOrNull() ?: return@forEach, ?: return@runCatching null,
name = content["name"]!!.jsonPrimitive.content, name = content["name"]!!.jsonPrimitive.content,
query = content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(), query = content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
filters_json = Json.encodeToString(content["filters"]!!.jsonArray), filtersJson = Json.encodeToString(content["filters"]!!.jsonArray),
)
}
}
}
handler.await(true) {
prefs.getStringSet("latest_tab_sources", emptySet())?.forEach {
feed_saved_searchQueries.insertFeedSavedSearch(
_id = null,
source = it.toLong(),
saved_search = null,
global = true,
) )
} }.getOrNull()
}
if (!savedSearch.isNullOrEmpty()) {
insertSavedSearch.awaitAll(savedSearch)
}
val feedSavedSearch = prefs.getStringSet("latest_tab_sources", emptySet())?.map {
FeedSavedSearch(
id = -1,
source = it.toLong(),
savedSearch = null,
global = true,
)
}
if (!feedSavedSearch.isNullOrEmpty()) {
insertFeedSavedSearch.awaitAll(feedSavedSearch)
} }
} }
prefs.edit(commit = true) { prefs.edit(commit = true) {

View File

@ -3,11 +3,11 @@ package exh.favorites
import android.content.Context import android.content.Context
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.PowerManager import android.os.PowerManager
import eu.kanade.data.AndroidDatabaseHandler
import eu.kanade.data.DatabaseHandler import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.interactor.SetMangaCategories
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.domain.manga.interactor.GetLibraryManga
import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
@ -49,6 +49,7 @@ import kotlin.time.Duration.Companion.seconds
// TODO only apply database changes after sync // TODO only apply database changes after sync
class FavoritesSyncHelper(val context: Context) { class FavoritesSyncHelper(val context: Context) {
private val handler: DatabaseHandler by injectLazy() private val handler: DatabaseHandler by injectLazy()
private val getLibraryManga: GetLibraryManga by injectLazy()
private val getCategories: GetCategories by injectLazy() private val getCategories: GetCategories by injectLazy()
private val getManga: GetManga by injectLazy() private val getManga: GetManga by injectLazy()
private val updateManga: UpdateManga by injectLazy() private val updateManga: UpdateManga by injectLazy()
@ -96,7 +97,7 @@ class FavoritesSyncHelper(val context: Context) {
// Validate library state // Validate library state
status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_verifying_library), context = context) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_verifying_library), context = context)
val libraryManga = handler.awaitList { (handler as AndroidDatabaseHandler).getLibraryQuery() } val libraryManga = getLibraryManga.await()
val seenManga = HashSet<Long>(libraryManga.size) val seenManga = HashSet<Long>(libraryManga.size)
libraryManga.forEach { libraryManga.forEach {
if (!it.isEhBasedManga()) return@forEach if (!it.isEhBasedManga()) return@forEach

View File

@ -2,14 +2,14 @@ package exh.savedsearches.models
data class FeedSavedSearch( data class FeedSavedSearch(
// Tag identifier, unique // Tag identifier, unique
var id: Long?, val id: Long,
// Source for the saved search // Source for the saved search
var source: Long, val source: Long,
// If -1 then get latest, if set get the saved search // If -1 then get latest, if set get the saved search
var savedSearch: Long?, val savedSearch: Long?,
// If the feed is a global or source specific feed // If the feed is a global or source specific feed
var global: Boolean, val global: Boolean,
) )

View File

@ -2,17 +2,17 @@ package exh.savedsearches.models
data class SavedSearch( data class SavedSearch(
// Tag identifier, unique // Tag identifier, unique
var id: Long?, val id: Long,
// The source the saved search is for // The source the saved search is for
var source: Long, val source: Long,
// If false the manga will not grab chapter updates // If false the manga will not grab chapter updates
var name: String, val name: String,
// The query if there is any // The query if there is any
var query: String?, val query: String?,
// The filter list // The filter list
var filtersJson: String?, val filtersJson: String?,
) )

View File

@ -16,13 +16,13 @@ countGlobal:
SELECT count(*) FROM feed_saved_search WHERE global = 1; SELECT count(*) FROM feed_saved_search WHERE global = 1;
selectBySource: selectBySource:
SELECT * FROM feed_saved_search WHERE source = ? AND global = 0; SELECT * FROM feed_saved_search WHERE source = :sourceId AND global = 0;
insertFeedSavedSearch: insert:
INSERT INTO feed_saved_search (_id, source, saved_search, global) VALUES (?, ?, ?, ?); INSERT INTO feed_saved_search (source, saved_search, global) VALUES (:sourceId, :savedSearch, :global);
deleteById: deleteById:
DELETE FROM feed_saved_search WHERE _id = ?; DELETE FROM feed_saved_search WHERE _id = :id;
deleteAll: deleteAll:
DELETE FROM feed_saved_search; DELETE FROM feed_saved_search;
@ -38,7 +38,7 @@ ON saved_search._id = M.saved_search;
selectSourceFeedSavedSearch: selectSourceFeedSavedSearch:
SELECT saved_search.* SELECT saved_search.*
FROM ( FROM (
SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = ? SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = :sourceId
) AS M ) AS M
JOIN saved_search JOIN saved_search
ON saved_search._id = M.saved_search; ON saved_search._id = M.saved_search;
@ -46,7 +46,10 @@ ON saved_search._id = M.saved_search;
countSourceFeedSavedSearch: countSourceFeedSavedSearch:
SELECT count(*) SELECT count(*)
FROM ( FROM (
SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = ? SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = :sourceId
) AS M ) AS M
JOIN saved_search JOIN saved_search
ON saved_search._id = M.saved_search; ON saved_search._id = M.saved_search;
selectLastInsertedRowId:
SELECT last_insert_rowid();

View File

@ -7,30 +7,33 @@ CREATE TABLE saved_search(
); );
selectBySource: selectBySource:
SELECT * FROM saved_search WHERE source = ?; SELECT * FROM saved_search WHERE source = :sourceId;
deleteBySource: deleteBySource:
DELETE FROM saved_search WHERE source = ?; DELETE FROM saved_search WHERE source = :sourceId;
selectAll: selectAll:
SELECT * FROM saved_search; SELECT * FROM saved_search;
selectById: selectById:
SELECT * FROM saved_search WHERE _id = ?; SELECT * FROM saved_search WHERE _id = :id;
selectByIds: selectByIds:
SELECT * FROM saved_search WHERE _id IN ?; SELECT * FROM saved_search WHERE _id IN :ids;
selectNamesAndSources: selectNamesAndSources:
SELECT source, name SELECT source, name
FROM saved_search; FROM saved_search;
insertSavedSearch: insert:
INSERT INTO saved_search (_id, source, name, query, filters_json) INSERT INTO saved_search (source, name, query, filters_json)
VALUES (?, ?, ?, ?, ?); VALUES (:source, :name, :query, :filtersJson);
deleteById: deleteById:
DELETE FROM saved_search WHERE _id = ?; DELETE FROM saved_search WHERE _id = :id;
deleteAll: deleteAll:
DELETE FROM saved_search; DELETE FROM saved_search;
selectLastInsertedRowId:
SELECT last_insert_rowid();