Migrate saved searches to the db

This commit is contained in:
Jobobby04 2022-03-27 14:11:37 -04:00
parent 1ebcfc53d4
commit 5d330c4f75
20 changed files with 465 additions and 212 deletions

View File

@ -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()}\"")

View File

@ -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<BackupSavedSearch> {
return preferences.savedSearches().get().mapNotNull {
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
val content = Json.decodeFromString<JsonSavedSearch>(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<BackupSavedSearch>) {
val currentSavedSearches = preferences.savedSearches().get().mapNotNull {
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
val content = try {
Json.decodeFromString<JsonSavedSearch>(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)
}
}
/**

View File

@ -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<JsonSavedSearch>(it.substringAfter(':'))
id to content
val content = parser.decodeFromString<JsonObject>(it.substringAfter(':'))
SavedSearch(
id = null,
source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null,
content["name"]!!.jsonPrimitive.content,
content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
Json.encodeToString(content["filters"]!!.jsonArray)
)
}.getOrNull()
}.toMutableSet()
}.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.ifEmpty { null }
val currentSources = newSavedSearches.map(Pair<Long, *>::first).toSet()
newSavedSearches += preferences.savedSearches().get().mapNotNull {
kotlin.runCatching {
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
val content = parser.decodeFromString<JsonSavedSearch>(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)
}
/**

View File

@ -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()

View File

@ -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) {

View File

@ -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)

View File

@ -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<EXHSavedSearch>) {
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 <--
}

View File

@ -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<JsonSavedSearch>(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<JsonArray>(filtersJson)
filterSerializer.deserialize(sourceFilters, filters)
appliedFilters = sourceFilters
}
} else if (jsonFilters != null) {
runCatching {
val filters = Json.decodeFromString<JsonArray>(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<EXHSavedSearch>) {
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<EXHSavedSearch> {
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<JsonSavedSearch>(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<JsonArray>(it) }
?: return@runCatching null
)
originalFilters
}.getOrNull()
)
}
fun loadSearches(searches: List<SavedSearch> = db.getSavedSearches(source.id).executeAsBlocking()): List<EXHSavedSearch> {
return searches.map {
val filtersJson = it.filtersJson ?: return@map EXHSavedSearch(
id = it.id!!,
name = it.name,
query = it.query.orEmpty(),
filterList = null
)
val filters = try {
Json.decodeFromString<JsonArray>(filtersJson)
} catch (e: Exception) {
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
)
}
}

View File

@ -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<EXHSavedSearch>): List<Chip> {
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 <--
}
}

View File

@ -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() {

View File

@ -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<JsonArray>(it) }
?: return@runCatching null
)
originalFilters
}.getOrNull()
)
}
fun loadSearches(): List<EXHSavedSearch> {
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<JsonSavedSearch>(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<JsonArray>(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
)
}
}

View File

@ -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<JsonObject>(it.substringAfter(':'))
SavedSearch(
id = null,
source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null,
content["name"]!!.jsonPrimitive.content,
content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
Json.encodeToString(content["filters"]!!.jsonArray)
)
}.getOrNull()
}?.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

View File

@ -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 {

View File

@ -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?

View File

@ -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
)

View File

@ -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<SavedSearch>(
SavedSearchPutResolver(),
SavedSearchGetResolver(),
SavedSearchDeleteResolver()
)
class SavedSearchPutResolver : DefaultPutResolver<SavedSearch>() {
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<SavedSearch>() {
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<SavedSearch>() {
override fun mapToDeleteQuery(obj: SavedSearch) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -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?,
)

View File

@ -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<SavedSearch>) = 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<SavedSearch>) {
db.inTransaction {
deleteSavedSearches(mergedMangaId).executeAsBlocking()
mergedMangases.chunked(100) { chunk ->
insertSavedSearches(chunk).executeAsBlocking()
}
}
}*/
}

View File

@ -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
)"""
}

View File

@ -355,6 +355,7 @@
<string name="save_search_delete">Delete saved search query?</string>
<string name="save_search_delete_message">Are you sure you wish to delete your saved search query: \'%1$s\'?</string>
<string name="save_search_invalid">Saved search invalid, filters have changed</string>
<string name="save_search_invalid_name">Invalid saved search name</string>
<!-- Source Categories -->
<string name="no_source_categories">No source categories available</string>