diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6b9bebf16..e36dcd5ab 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,7 +25,7 @@ android { applicationId = "eu.kanade.tachiyomi.sy" minSdk = AndroidConfig.minSdk targetSdk = AndroidConfig.targetSdk - versionCode = 30 + versionCode = 31 versionName = "1.8.1" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt index 5608d062d..110bdbf7e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt @@ -38,13 +38,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.JsonSavedSearch +import exh.savedsearches.models.SavedSearch import exh.source.MERGED_SOURCE_ID import exh.source.getMainSource import exh.util.executeOnIO -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import exh.util.nullIfBlank import kotlinx.serialization.protobuf.ProtoBuf import logcat.LogPriority import okio.buffer @@ -164,14 +162,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * @return list of [BackupSavedSearch] to be backed up */ private fun backupSavedSearches(): List { - return preferences.savedSearches().get().mapNotNull { - val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null - val content = Json.decodeFromString(it.substringAfter(':')) + return databaseHelper.getSavedSearches().executeAsBlocking().map { BackupSavedSearch( - content.name, - content.query, - content.filters.toString(), - sourceId + it.name, + it.query.orEmpty(), + it.filtersJson ?: "[]", + it.source ) } } @@ -431,34 +427,25 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { // SY --> internal fun restoreSavedSearches(backupSavedSearches: List) { - val currentSavedSearches = preferences.savedSearches().get().mapNotNull { - val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null - val content = try { - Json.decodeFromString(it.substringAfter(':')) - } catch (e: Exception) { - return@mapNotNull null - } - BackupSavedSearch( - content.name, - content.query, - content.filters.toString(), - sourceId - ) - } + val currentSavedSearches = databaseHelper.getSavedSearches() + .executeAsBlocking() val newSavedSearches = backupSavedSearches.filter { backupSavedSearch -> currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } }.map { - "${it.source}:" + Json.encodeToString( - JsonSavedSearch( - it.name, - it.query, - Json.decodeFromString(it.filterList) - ) + SavedSearch( + id = null, + it.source, + it.name, + it.query.nullIfBlank(), + filtersJson = it.filterList.nullIfBlank() + ?.takeUnless { it == "[]" } ) - }.toSet() + }.ifEmpty { null } - preferences.savedSearches().set(newSavedSearches + preferences.savedSearches().get()) + if (newSavedSearches != null) { + databaseHelper.insertSavedSearches(newSavedSearches) + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt index 49afe80ed..2ecd1cc01 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt @@ -28,11 +28,16 @@ import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.online.all.MergedSource import exh.eh.EHentaiThrottleManager import exh.merged.sql.models.MergedMangaReference -import exh.savedsearches.JsonSavedSearch +import exh.savedsearches.models.SavedSearch import exh.source.MERGED_SOURCE_ID +import exh.util.nullIfBlank import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual import kotlin.math.max @@ -287,34 +292,26 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab internal fun restoreSavedSearches(jsonSavedSearches: String) { val backupSavedSearches = jsonSavedSearches.split("***").toSet() + val currentSavedSearches = databaseHelper.getSavedSearches().executeAsBlocking() + val newSavedSearches = backupSavedSearches.mapNotNull { runCatching { - val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null - val content = parser.decodeFromString(it.substringAfter(':')) - id to content + val content = parser.decodeFromString(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() - }.toMutableSet() + }.filter { backupSavedSearch -> + currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } + }.ifEmpty { null } - val currentSources = newSavedSearches.map(Pair::first).toSet() - - newSavedSearches += preferences.savedSearches().get().mapNotNull { - kotlin.runCatching { - val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null - val content = parser.decodeFromString(it.substringAfter(':')) - id to content - }.getOrNull() + if (newSavedSearches != null) { + databaseHelper.insertSavedSearches(newSavedSearches) } - - val otherSerialized = preferences.savedSearches().get().mapNotNull { - val sourceId = it.substringBefore(":").toLongOrNull() ?: return@mapNotNull null - if (sourceId in currentSources) return@mapNotNull null - it - }.toSet() - - val newSerialized = newSavedSearches.map { (source, savedSearch) -> - "$source:" + Json.encodeToString(savedSearch) - }.toSet() - preferences.savedSearches().set(otherSerialized + newSerialized) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index 0406967f6..31f52b8b8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -36,13 +36,16 @@ import exh.metadata.sql.models.SearchTitle import exh.metadata.sql.queries.SearchMetadataQueries import exh.metadata.sql.queries.SearchTagQueries import exh.metadata.sql.queries.SearchTitleQueries +import exh.savedsearches.mappers.SavedSearchTypeMapping +import exh.savedsearches.models.SavedSearch +import exh.savedsearches.queries.SavedSearchQueries import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory /** * This class provides operations to manage the database through its interfaces. */ open class DatabaseHelper(context: Context) : - MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, FavoriteEntryQueries /* SY <-- */ { + MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, FavoriteEntryQueries, SavedSearchQueries /* SY <-- */ { private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) .name(DbOpenCallback.DATABASE_NAME) @@ -63,6 +66,7 @@ open class DatabaseHelper(context: Context) : .addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping()) .addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping()) .addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping()) + .addTypeMapping(SavedSearch::class.java, SavedSearchTypeMapping()) // SY <-- .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index 3f07bee6c..7a58e0eca 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -13,6 +13,7 @@ import exh.merged.sql.tables.MergedTable import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.tables.SearchTagTable import exh.metadata.sql.tables.SearchTitleTable +import exh.savedsearches.tables.SavedSearchTable class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { @@ -25,7 +26,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { /** * Version of the database. */ - const val DATABASE_VERSION = /* SY --> */ 12 /* SY <-- */ + const val DATABASE_VERSION = /* SY --> */ 13 /* SY <-- */ } override fun onCreate(db: SupportSQLiteDatabase) = with(db) { @@ -41,6 +42,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { execSQL(SearchTitleTable.createTableQuery) execSQL(MergedTable.createTableQuery) execSQL(FavoriteEntryTable.createTableQuery) + execSQL(SavedSearchTable.createTableQuery) // SY <-- // DB indexes @@ -101,6 +103,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { if (oldVersion < 12) { db.execSQL(FavoriteEntryTable.fixTableQuery) } + if (oldVersion < 13) { + db.execSQL(SavedSearchTable.createTableQuery) + } } override fun onConfigure(db: SupportSQLiteDatabase) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 372111b69..1f232e03e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -411,8 +411,6 @@ class PreferencesHelper(val context: Context) { fun ehLastVersionCode() = flowPrefs.getInt("eh_last_version_code", 0) - fun savedSearches() = flowPrefs.getStringSet("eh_saved_searches", emptySet()) - fun logLevel() = flowPrefs.getInt(Keys.eh_logLevel, 0) fun enableSourceBlacklist() = flowPrefs.getBoolean("eh_enable_source_blacklist", true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index d3a8f5c06..9f944d25d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -37,7 +37,6 @@ import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController import eu.kanade.tachiyomi.ui.browse.source.SourceController -import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet.FilterNavigationView.Companion.MAX_SAVED_SEARCHES import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting @@ -84,6 +83,7 @@ open class BrowseSourceController(bundle: Bundle) : searchQuery: String? = null, // SY --> smartSearchConfig: SourceController.SmartSearchConfig? = null, + savedSearch: Long? = null, filterList: String? = null // SY <-- ) : this( @@ -99,6 +99,10 @@ open class BrowseSourceController(bundle: Bundle) : putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig) } + if (savedSearch != null) { + putLong(SAVED_SEARCH_CONFIG_KEY, savedSearch) + } + if (filterList != null) { putString(FILTERS_CONFIG_KEY, filterList) } @@ -154,7 +158,8 @@ open class BrowseSourceController(bundle: Bundle) : return BrowseSourcePresenter( args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY), - filters = args.getString(FILTERS_CONFIG_KEY) + filters = args.getString(FILTERS_CONFIG_KEY), + savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L } ) // SY <-- } @@ -179,6 +184,10 @@ open class BrowseSourceController(bundle: Bundle) : // SY <-- } + fun setSavedSearches(savedSearches: List) { + filterSheet?.setSavedSearches(savedSearches) + } + open fun initFilterSheet() { if (presenter.sourceFilters.isEmpty()) { // SY --> @@ -207,6 +216,7 @@ 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) @@ -214,27 +224,18 @@ open class BrowseSourceController(bundle: Bundle) : searchName = input } .setPositiveButton(R.string.action_save) { _, _ -> - val oldSavedSearches = presenter.loadSearches() - if (searchName.isNotBlank() && - oldSavedSearches.size < MAX_SAVED_SEARCHES - ) { - val newSearches = oldSavedSearches + EXHSavedSearch( - searchName.trim(), - presenter.query, - presenter.sourceFilters - ) - presenter.saveSearches(newSearches) - filterSheet?.setSavedSearches(newSearches) + 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@{ indexToSearch -> - val savedSearches = presenter.loadSearches() - - val search = savedSearches.getOrNull(indexToSearch) + onSavedSearchClicked = cb@{ idOfSearch -> + val search = presenter.loadSearch(idOfSearch) if (search == null) { filterSheet?.context?.let { @@ -261,32 +262,14 @@ open class BrowseSourceController(bundle: Bundle) : presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters) activity?.invalidateOptionsMenu() }, - onSavedSearchDeleteClicked = cb@{ indexToDelete, name -> - val savedSearches = presenter.loadSearches() - - val search = savedSearches.getOrNull(indexToDelete) - - if (search == null || search.name != name) { - filterSheet?.context?.let { - MaterialAlertDialogBuilder(it) - .setTitle(R.string.save_search_failed_to_delete) - .setMessage(R.string.save_search_failed_to_delete_message) - .show() - } - return@cb - } - + onSavedSearchDeleteClicked = cb@{ idToDelete, name -> filterSheet?.context?.let { MaterialAlertDialogBuilder(it) .setTitle(R.string.save_search_delete) - .setMessage(it.getString(R.string.save_search_delete_message, search.name)) + .setMessage(it.getString(R.string.save_search_delete_message, name)) .setPositiveButton(R.string.action_cancel, null) .setNegativeButton(android.R.string.ok) { _, _ -> - val newSearches = savedSearches.filterIndexed { index, _ -> - index != indexToDelete - } - presenter.saveSearches(newSearches) - filterSheet?.setSavedSearches(newSearches) + presenter.deleteSearch(idToDelete) } .show() } @@ -836,6 +819,7 @@ open class BrowseSourceController(bundle: Bundle) : // SY --> const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig" + const val SAVED_SEARCH_CONFIG_KEY = "savedSearch" const val FILTERS_CONFIG_KEY = "filters" // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt index 2d2f161bc..f829b048b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt @@ -42,8 +42,9 @@ import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.system.logcat import exh.log.xLogE import exh.savedsearches.EXHSavedSearch -import exh.savedsearches.JsonSavedSearch +import exh.savedsearches.models.SavedSearch import exh.source.isEhBasedSource +import exh.util.nullIfBlank import kotlinx.coroutines.Job import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch @@ -73,6 +74,7 @@ open class BrowseSourcePresenter( searchQuery: String? = null, // SY --> private val filters: String? = null, + private val savedSearch: Long? = null, // SY <-- private val sourceManager: SourceManager = Injekt.get(), private val db: DatabaseHelper = Injekt.get(), @@ -134,21 +136,45 @@ open class BrowseSourcePresenter( sourceFilters = source.getFilterList() // SY --> + val savedSearchFilters = savedSearch val jsonFilters = filters - if (jsonFilters != null) { + if (savedSearchFilters != null) { runCatching { - val filters = Json.decodeFromString(jsonFilters) - filterSerializer.deserialize(sourceFilters, filters.filters) + val savedSearch = db.getSavedSearch(savedSearchFilters).executeAsBlocking() ?: return@runCatching + query = savedSearch.query.orEmpty() + val filtersJson = savedSearch.filtersJson + ?: return@runCatching + val filters = Json.decodeFromString(filtersJson) + filterSerializer.deserialize(sourceFilters, filters) + appliedFilters = sourceFilters + } + } else if (jsonFilters != null) { + runCatching { + val filters = Json.decodeFromString(jsonFilters) + filterSerializer.deserialize(sourceFilters, filters) + appliedFilters = sourceFilters } } - val allDefault = sourceFilters == source.getFilterList() + + db.getSavedSearches(source.id) + .asRxObservable() + .map { + loadSearches(it) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache( + { controller, savedSearches -> + controller.setSavedSearches(savedSearches) + } + ) // SY <-- if (savedState != null) { query = savedState.getString(::query.name, "") } - restartPager(/* SY -->*/ filters = if (allDefault) this.appliedFilters else sourceFilters /* SY <--*/) + restartPager() } override fun onSave(state: Bundle) { @@ -319,7 +345,7 @@ open class BrowseSourcePresenter( .forEach { service -> launchIO { try { - service.match(manga)?.let { track -> + service.match(source, manga)?.let { track -> track.manga_id = manga.id!! (service as TrackService).bind(track) db.insertTrack(track).executeAsBlocking() @@ -456,48 +482,84 @@ open class BrowseSourcePresenter( } // EXH --> - fun saveSearches(searches: List) { - val otherSerialized = prefs.savedSearches().get().filterNot { - it.startsWith("${source.id}:") - }.toSet() - val newSerialized = searches.map { - "${source.id}:" + Json.encodeToString( - JsonSavedSearch( - it.name, - it.query, - if (it.filterList != null) { - filterSerializer.serialize(it.filterList) - } else JsonArray(emptyList()) + 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() + } } - prefs.savedSearches().set(otherSerialized + newSerialized) } - fun loadSearches(): List { - return prefs.savedSearches().get().mapNotNull { - val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null - if (id != source.id) return@mapNotNull null - val content = try { - Json.decodeFromString(it.substringAfter(':')) + fun deleteSearch(searchId: Long) { + launchIO { + db.deleteSavedSearch(searchId).executeAsBlocking() + } + } + + fun loadSearch(searchId: Long): EXHSavedSearch? { + val search = db.getSavedSearch(searchId).executeAsBlocking() ?: return null + return EXHSavedSearch( + id = search.id!!, + name = search.name, + query = search.query.orEmpty(), + filterList = runCatching { + val originalFilters = source.getFilterList() + filterSerializer.deserialize( + filters = originalFilters, + json = search.filtersJson + ?.let { Json.decodeFromString(it) } + ?: return@runCatching null + ) + originalFilters + }.getOrNull() + ) + } + + fun loadSearches(searches: List = db.getSavedSearches(source.id).executeAsBlocking()): List { + 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(filtersJson) } catch (e: Exception) { - return@mapNotNull null - } + 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, content.filters) + filterSerializer.deserialize(originalFilters, filters) EXHSavedSearch( - content.name, - content.query, - originalFilters + 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( - content.name, - content.query, - null + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt index b13f48ab1..dc66c483a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt @@ -19,7 +19,6 @@ import eu.kanade.tachiyomi.widget.SimpleNavigationView import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog import exh.savedsearches.EXHSavedSearch import exh.source.getMainSource -import exh.util.under class SourceFilterSheet( activity: Activity, @@ -32,8 +31,8 @@ class SourceFilterSheet( private val onResetClicked: () -> Unit, // EXH --> private val onSaveClicked: () -> Unit, - var onSavedSearchClicked: (Int) -> Unit = {}, - var onSavedSearchDeleteClicked: (Int, String) -> Unit = { _, _ -> } + var onSavedSearchClicked: (Long) -> Unit = {}, + var onSavedSearchDeleteClicked: (Long, String) -> Unit = { _, _ -> } // EXH <-- ) : BaseBottomSheetDialog(activity) { @@ -97,9 +96,9 @@ class SourceFilterSheet( // SY --> var onSaveClicked = {} - var onSavedSearchClicked: (Int) -> Unit = {} + var onSavedSearchClicked: (Long) -> Unit = {} - var onSavedSearchDeleteClicked: (Int, String) -> Unit = { _, _ -> } + var onSavedSearchDeleteClicked: (Long, String) -> Unit = { _, _ -> } private val savedSearchesAdapter = SavedSearchesAdapter(getSavedSearchesChips(searches)) // SY <-- @@ -143,17 +142,13 @@ class SourceFilterSheet( } private fun getSavedSearchesChips(searches: List): List { - recycler.post { - binding.saveSearchBtn.isVisible = searches.size under MAX_SAVED_SEARCHES - } - return searches.withIndex() - .sortedBy { it.value.name } - .map { (index, search) -> + return searches + .map { search -> Chip(context).apply { text = search.name - setOnClickListener { onSavedSearchClicked(index) } + setOnClickListener { onSavedSearchClicked(search.id) } setOnLongClickListener { - onSavedSearchDeleteClicked(index, search.name); true + onSavedSearchDeleteClicked(search.id, search.name); true } } } @@ -163,10 +158,6 @@ class SourceFilterSheet( fun hideFilterButton() { binding.filterBtn.isVisible = false } - - companion object { - const val MAX_SAVED_SEARCHES = 500 // if you want more than this, fuck you, i guess - } // EXH <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt index d87544708..4a53b66e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt @@ -24,7 +24,6 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.toast -import exh.savedsearches.JsonSavedSearch import exh.util.nullIfBlank import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -196,25 +195,21 @@ open class IndexController : onFilterClicked = { val allDefault = presenter.sourceFilters == presenter.source.getFilterList() filterSheet?.dismiss() - val json = if (allDefault) { - null + if (allDefault) { + onBrowseClick( + presenter.query.nullIfBlank() + ) } else { - Json.encodeToString( - JsonSavedSearch( - "", - "", - filterSerializer.serialize(presenter.sourceFilters) - ) + onBrowseClick( + presenter.query.nullIfBlank(), + filters = Json.encodeToString(filterSerializer.serialize(presenter.sourceFilters)) ) } - onBrowseClick(presenter.query.nullIfBlank(), json) }, onResetClicked = {}, onSaveClicked = {}, - onSavedSearchClicked = cb@{ indexToSearch -> - val savedSearches = presenter.loadSearches() - - val search = savedSearches.getOrNull(indexToSearch) + onSavedSearchClicked = cb@{ idOfSearch -> + val search = presenter.loadSearch(idOfSearch) if (search == null) { filterSheet?.context?.let { @@ -237,14 +232,10 @@ open class IndexController : filterSheet?.dismiss() if (!allDefault) { - val json = Json.encodeToString( - JsonSavedSearch( - "", - "", - filterSerializer.serialize(presenter.sourceFilters) - ) + onBrowseClick( + search = presenter.query.nullIfBlank(), + savedSearch = search.id ) - onBrowseClick(presenter.query.nullIfBlank(), json) } }, onSavedSearchDeleteClicked = { _, _ -> } @@ -325,8 +316,8 @@ open class IndexController : super.onDestroyView(view) } - fun onBrowseClick(search: String? = null, filters: String? = null) { - router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction()) + fun onBrowseClick(search: String? = null, savedSearch: Long? = null, filters: String? = null) { + router.replaceTopController(BrowseSourceController(presenter.source, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction()) } private fun onLatestClick() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexPresenter.kt index f8aef3787..142ad2d5c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexPresenter.kt @@ -19,7 +19,6 @@ import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.logcat import exh.log.xLogE import exh.savedsearches.EXHSavedSearch -import exh.savedsearches.JsonSavedSearch import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -212,30 +212,61 @@ open class IndexPresenter( private val filterSerializer = FilterSerializer() + fun loadSearch(searchId: Long): EXHSavedSearch? { + val search = db.getSavedSearch(searchId).executeAsBlocking() ?: return null + return EXHSavedSearch( + id = search.id!!, + name = search.name, + query = search.query.orEmpty(), + filterList = runCatching { + val originalFilters = source.getFilterList() + filterSerializer.deserialize( + filters = originalFilters, + json = search.filtersJson + ?.let { Json.decodeFromString(it) } + ?: return@runCatching null + ) + originalFilters + }.getOrNull() + ) + } + fun loadSearches(): List { - return preferences.savedSearches().get().mapNotNull { - val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null - if (id != source.id) return@mapNotNull null - val content = try { - Json.decodeFromString(it.substringAfter(':')) + return db.getSavedSearches(source.id).executeAsBlocking().map { + val filtersJson = it.filtersJson ?: return@map EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null + ) + val filters = try { + Json.decodeFromString(filtersJson) } catch (e: Exception) { - return@mapNotNull null - } + null + } ?: return@map EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null + ) + try { val originalFilters = source.getFilterList() - filterSerializer.deserialize(originalFilters, content.filters) + filterSerializer.deserialize(originalFilters, filters) EXHSavedSearch( - content.name, - content.query, - originalFilters + 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( - content.name, - content.query, - null + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null ) } } diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index 02d458742..af0d551b5 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -40,6 +40,7 @@ import exh.eh.EHentaiUpdateWorker import exh.log.xLogE import exh.log.xLogW import exh.merged.sql.models.MergedMangaReference +import exh.savedsearches.models.SavedSearch import exh.source.BlacklistedSources import exh.source.EH_SOURCE_ID import exh.source.HBROWSE_SOURCE_ID @@ -47,11 +48,17 @@ import exh.source.MERGED_SOURCE_ID import exh.source.PERV_EDEN_EN_SOURCE_ID import exh.source.PERV_EDEN_IT_SOURCE_ID import exh.source.TSUMINO_SOURCE_ID +import exh.util.nullIfBlank import exh.util.under import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -395,6 +402,26 @@ object EXHMigrations { if (oldVersion under 30) { BackupCreatorJob.setupTask(context) } + if (oldVersion under 31) { + val savedSearches = prefs.getStringSet("eh_saved_searches", emptySet())?.mapNotNull { + kotlin.runCatching { + val content = Json.decodeFromString(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() + }?.ifEmpty { null } + if (savedSearches != null) { + db.insertSavedSearches(savedSearches) + } + prefs.edit(commit = true) { + remove("eh_saved_searches") + } + } // if (oldVersion under 1) { } (1 is current release version) // do stuff here when releasing changed crap diff --git a/app/src/main/java/exh/debug/DebugFunctions.kt b/app/src/main/java/exh/debug/DebugFunctions.kt index 50da8d04a..6f67f9f5b 100644 --- a/app/src/main/java/exh/debug/DebugFunctions.kt +++ b/app/src/main/java/exh/debug/DebugFunctions.kt @@ -7,18 +7,15 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.online.all.NHentai import exh.EXHMigrations import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiUpdateWorker -import exh.log.xLogE import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.insertFlatMetadataAsync -import exh.savedsearches.JsonSavedSearch import exh.source.EH_SOURCE_ID import exh.source.EXH_SOURCE_ID import exh.source.isEhBasedManga @@ -30,11 +27,7 @@ import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import uy.kohesive.injekt.injectLazy -import java.lang.RuntimeException import java.util.UUID @Suppress("unused") @@ -171,7 +164,7 @@ object DebugFunctions { it.favorite && db.getSearchMetadataForManga(it.id!!).executeAsBlocking() == null } - fun clearSavedSearches() = prefs.savedSearches().set(emptySet()) + fun clearSavedSearches() = db.deleteAllSavedSearches().executeAsBlocking() fun listAllSources() = sourceManager.getCatalogueSources().joinToString("\n") { "${it.id}: ${it.name} (${it.lang.uppercase()})" @@ -249,7 +242,7 @@ object DebugFunctions { ) } - fun copyEHentaiSavedSearchesToExhentai() { + /*fun copyEHentaiSavedSearchesToExhentai() { runBlocking { val source = sourceManager.get(EH_SOURCE_ID) as? CatalogueSource ?: return@runBlocking val newSource = sourceManager.get(EXH_SOURCE_ID) as? CatalogueSource ?: return@runBlocking @@ -325,7 +318,7 @@ object DebugFunctions { } prefs.savedSearches().set((otherSerialized + newSerialized).toSet()) } - } + }*/ fun fixReaderViewerBackupBug() { db.inTransaction { diff --git a/app/src/main/java/exh/savedsearches/EXHSavedSearch.kt b/app/src/main/java/exh/savedsearches/EXHSavedSearch.kt index fe3151478..672511080 100644 --- a/app/src/main/java/exh/savedsearches/EXHSavedSearch.kt +++ b/app/src/main/java/exh/savedsearches/EXHSavedSearch.kt @@ -3,6 +3,7 @@ package exh.savedsearches import eu.kanade.tachiyomi.source.model.FilterList data class EXHSavedSearch( + val id: Long, val name: String, val query: String, val filterList: FilterList? diff --git a/app/src/main/java/exh/savedsearches/JsonSavedSearch.kt b/app/src/main/java/exh/savedsearches/JsonSavedSearch.kt deleted file mode 100644 index 560551cc4..000000000 --- a/app/src/main/java/exh/savedsearches/JsonSavedSearch.kt +++ /dev/null @@ -1,11 +0,0 @@ -package exh.savedsearches - -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonArray - -@Serializable -data class JsonSavedSearch( - val name: String, - val query: String, - val filters: JsonArray -) diff --git a/app/src/main/java/exh/savedsearches/mappers/SavedSearchTypeMapping.kt b/app/src/main/java/exh/savedsearches/mappers/SavedSearchTypeMapping.kt new file mode 100644 index 000000000..f312ea49e --- /dev/null +++ b/app/src/main/java/exh/savedsearches/mappers/SavedSearchTypeMapping.kt @@ -0,0 +1,66 @@ +package exh.savedsearches.mappers + +import android.database.Cursor +import androidx.core.content.contentValuesOf +import androidx.core.database.getStringOrNull +import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping +import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver +import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver +import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver +import com.pushtorefresh.storio.sqlite.queries.DeleteQuery +import com.pushtorefresh.storio.sqlite.queries.InsertQuery +import com.pushtorefresh.storio.sqlite.queries.UpdateQuery +import exh.savedsearches.models.SavedSearch +import exh.savedsearches.tables.SavedSearchTable.COL_FILTERS_JSON +import exh.savedsearches.tables.SavedSearchTable.COL_ID +import exh.savedsearches.tables.SavedSearchTable.COL_NAME +import exh.savedsearches.tables.SavedSearchTable.COL_QUERY +import exh.savedsearches.tables.SavedSearchTable.COL_SOURCE +import exh.savedsearches.tables.SavedSearchTable.TABLE + +class SavedSearchTypeMapping : SQLiteTypeMapping( + SavedSearchPutResolver(), + SavedSearchGetResolver(), + SavedSearchDeleteResolver() +) + +class SavedSearchPutResolver : DefaultPutResolver() { + + override fun mapToInsertQuery(obj: SavedSearch) = InsertQuery.builder() + .table(TABLE) + .build() + + override fun mapToUpdateQuery(obj: SavedSearch) = UpdateQuery.builder() + .table(TABLE) + .where("$COL_ID = ?") + .whereArgs(obj.id) + .build() + + override fun mapToContentValues(obj: SavedSearch) = contentValuesOf( + COL_ID to obj.id, + COL_SOURCE to obj.source, + COL_NAME to obj.name, + COL_QUERY to obj.query, + COL_FILTERS_JSON to obj.filtersJson + ) +} + +class SavedSearchGetResolver : DefaultGetResolver() { + + override fun mapFromCursor(cursor: Cursor): SavedSearch = SavedSearch( + id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID)), + source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE)), + name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME)), + query = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(COL_QUERY)), + filtersJson = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(COL_FILTERS_JSON)) + ) +} + +class SavedSearchDeleteResolver : DefaultDeleteResolver() { + + override fun mapToDeleteQuery(obj: SavedSearch) = DeleteQuery.builder() + .table(TABLE) + .where("$COL_ID = ?") + .whereArgs(obj.id) + .build() +} diff --git a/app/src/main/java/exh/savedsearches/models/SavedSearch.kt b/app/src/main/java/exh/savedsearches/models/SavedSearch.kt new file mode 100644 index 000000000..6015d0f76 --- /dev/null +++ b/app/src/main/java/exh/savedsearches/models/SavedSearch.kt @@ -0,0 +1,18 @@ +package exh.savedsearches.models + +data class SavedSearch( + // Tag identifier, unique + var id: Long?, + + // The source the saved search is for + var source: Long, + + // If false the manga will not grab chapter updates + var name: String, + + // The query if there is any + var query: String?, + + // The filter list + var filtersJson: String?, +) diff --git a/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt b/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt new file mode 100644 index 000000000..b62aae462 --- /dev/null +++ b/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt @@ -0,0 +1,82 @@ +package exh.savedsearches.queries + +import com.pushtorefresh.storio.sqlite.queries.DeleteQuery +import com.pushtorefresh.storio.sqlite.queries.Query +import eu.kanade.tachiyomi.data.database.DbProvider +import exh.savedsearches.models.SavedSearch +import exh.savedsearches.tables.SavedSearchTable + +interface SavedSearchQueries : DbProvider { + fun getSavedSearches(source: Long) = db.get() + .listOfObjects(SavedSearch::class.java) + .withQuery( + Query.builder() + .table(SavedSearchTable.TABLE) + .where("${SavedSearchTable.COL_SOURCE} = ?") + .whereArgs(source) + .build() + ) + .prepare() + + fun deleteSavedSearches(source: Long) = db.delete() + .byQuery( + DeleteQuery.builder() + .table(SavedSearchTable.TABLE) + .where("${SavedSearchTable.COL_SOURCE} = ?") + .whereArgs(source) + .build() + ) + .prepare() + + fun getSavedSearches() = db.get() + .listOfObjects(SavedSearch::class.java) + .withQuery( + Query.builder() + .table(SavedSearchTable.TABLE) + .orderBy(SavedSearchTable.COL_ID) + .build() + ) + .prepare() + + fun getSavedSearch(id: Long) = db.get() + .`object`(SavedSearch::class.java) + .withQuery( + Query.builder() + .table(SavedSearchTable.TABLE) + .where("${SavedSearchTable.COL_ID} = ?") + .whereArgs(id) + .build() + ) + .prepare() + + fun insertSavedSearch(savedSearch: SavedSearch) = db.put().`object`(savedSearch).prepare() + + fun insertSavedSearches(savedSearches: List) = db.put().objects(savedSearches).prepare() + + fun deleteSavedSearch(savedSearch: SavedSearch) = db.delete().`object`(savedSearch).prepare() + + fun deleteSavedSearch(id: Long) = db.delete() + .byQuery( + DeleteQuery.builder() + .table(SavedSearchTable.TABLE) + .where("${SavedSearchTable.COL_ID} = ?") + .whereArgs(id) + .build() + ).prepare() + + fun deleteAllSavedSearches() = db.delete().byQuery( + DeleteQuery.builder() + .table(SavedSearchTable.TABLE) + .build() + ) + .prepare() + + /*fun setMangasForMergedManga(mergedMangaId: Long, mergedMangases: List) { + db.inTransaction { + deleteSavedSearches(mergedMangaId).executeAsBlocking() + mergedMangases.chunked(100) { chunk -> + insertSavedSearches(chunk).executeAsBlocking() + } + } + }*/ +} diff --git a/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt b/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt new file mode 100644 index 000000000..3a345fb05 --- /dev/null +++ b/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt @@ -0,0 +1,26 @@ +package exh.savedsearches.tables + +object SavedSearchTable { + + const val TABLE = "saved_search" + + const val COL_ID = "_id" + + const val COL_SOURCE = "source" + + const val COL_NAME = "name" + + const val COL_QUERY = "query" + + const val COL_FILTERS_JSON = "filters_json" + + val createTableQuery: String + get() = + """CREATE TABLE $TABLE( + $COL_ID INTEGER NOT NULL PRIMARY KEY, + $COL_SOURCE INTEGER NOT NULL, + $COL_NAME TEXT NOT NULL, + $COL_QUERY TEXT, + $COL_FILTERS_JSON TEXT + )""" +} diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index 002958dc8..d944ca306 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -355,6 +355,7 @@ Delete saved search query? Are you sure you wish to delete your saved search query: \'%1$s\'? Saved search invalid, filters have changed + Invalid saved search name No source categories available