Search suggestions in library

Will automatically turn on in 24 hours
Then every 2 hours new suggestions to search for will show
Long pressing on it will automatically search that suggestion

There's an added option to turn it off, or turn it on earlier as well
This commit is contained in:
Jays2Kings 2021-04-28 16:10:23 -04:00
parent 62c4ee22a4
commit 7f72eabfc3
10 changed files with 230 additions and 6 deletions

View File

@ -69,7 +69,7 @@ fun limitAndOffset(endless: Boolean, isResuming: Boolean, offset: Int): String {
return when {
isResuming && endless && offset > 0 -> "LIMIT $offset"
endless -> "LIMIT ${RecentsPresenter.ENDLESS_LIMIT}\nOFFSET $offset"
else -> "LIMIT 25"
else -> "LIMIT ${RecentsPresenter.SHORT_LIMIT}"
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.data.preference
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class DelayedLibrarySuggestionsJob(context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>()
if (preferences.showLibrarySearchSuggestions().isNotSet()) {
preferences.showLibrarySearchSuggestions().set(true)
LibraryPresenter.setSearchSuggestion(preferences, Injekt.get(), Injekt.get())
}
return Result.success()
}
companion object {
private const val TAG = "DelayedLibrarySuggestions"
fun setupTask(enabled: Boolean) {
if (enabled) {
val request = OneTimeWorkRequestBuilder<DelayedLibrarySuggestionsJob>()
.setInitialDelay(1, TimeUnit.DAYS)
.addTag(TAG)
.build()
WorkManager.getInstance().enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request)
} else {
WorkManager.getInstance().cancelAllWorkByTag(TAG)
}
}
}
}

View File

@ -106,6 +106,10 @@ object PreferenceKeys {
const val folderPerManga = "create_folder_per_manga"
const val showLibrarySearchSuggestions = "show_library_search_suggestions"
const val librarySearchSuggestion = "library_search_suggestion"
const val numberOfBackups = "backup_slots"
const val backupInterval = "backup_interval"

View File

@ -234,6 +234,12 @@ class PreferencesHelper(val context: Context) {
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
fun librarySearchSuggestion() = flowPrefs.getString(Keys.librarySearchSuggestion, "")
fun showLibrarySearchSuggestions() = flowPrefs.getBoolean(Keys.showLibrarySearchSuggestions, false)
fun lastLibrarySuggestion() = flowPrefs.getLong("last_library_suggestion", 0L)
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)

View File

@ -203,7 +203,15 @@ class LibraryController(
override fun getTitle(): String? {
setSubtitle()
return searchTitle(view?.context?.getString(R.string.your_library)?.lowercase(Locale.ROOT))
return searchTitle(
if (preferences.showLibrarySearchSuggestions().get() &&
preferences.librarySearchSuggestion().get().isNotBlank()
) {
"\"${preferences.librarySearchSuggestion().get()}\""
} else {
view?.context?.getString(R.string.your_library)?.lowercase(Locale.ROOT)
}
)
}
private var scrollListener = object : RecyclerView.OnScrollListener() {
@ -849,6 +857,23 @@ class LibraryController(
binding.recyclerCover.isFocusable = false
singleCategory = presenter.categories.size <= 1
showDropdown()
if (preferences.showLibrarySearchSuggestions().get()) {
activityBinding?.cardToolbar?.setOnLongClickListener {
val suggestion = preferences.librarySearchSuggestion().get()
if (suggestion.isNotBlank()) {
val searchItem =
activityBinding?.cardToolbar?.menu?.findItem(R.id.action_search)
val searchView = searchItem?.actionView as? SearchView
?: return@setOnLongClickListener false
searchItem.expandActionView()
searchView.setQuery(suggestion, false)
true
} else {
false
}
}
}
} else {
updateFilterSheetY()
closeTip()
@ -856,6 +881,7 @@ class LibraryController(
binding.filterBottomSheet.filterBottomSheet.isInvisible = true
}
activityBinding?.toolbar?.hideDropdown()
activityBinding?.cardToolbar?.setOnLongClickListener(null)
}
}

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.DelayedLibrarySuggestionsJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.preference.minusAssign
@ -28,18 +29,26 @@ import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet.Companion.STATE_EXCLUDE
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet.Companion.STATE_IGNORE
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet.Companion.STATE_INCLUDE
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.lang.capitalizeWords
import eu.kanade.tachiyomi.util.lang.chopByWords
import eu.kanade.tachiyomi.util.lang.removeArticles
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.ArrayList
import java.util.Calendar
import java.util.Comparator
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.random.Random
/**
* Presenter of [LibraryController].
@ -98,6 +107,17 @@ class LibraryPresenter(
lastCategories = null
lastLibraryItems = null
getLibrary()
if (preferences.showLibrarySearchSuggestions().isNotSet()) {
DelayedLibrarySuggestionsJob.setupTask(true)
} else if (preferences.showLibrarySearchSuggestions().get() &&
Date().time >= preferences.lastLibrarySuggestion().get() + TimeUnit.HOURS.toMillis(2)
) {
// Doing this instead of a job in case the app isn't used often
presenterScope.launchIO {
setSearchSuggestion(preferences, db, sourceManager)
withUIContext { view.setTitle() }
}
}
}
/** Get favorited manga for library and sort and filter it */
@ -1026,6 +1046,82 @@ class LibraryPresenter(
private const val sourceSplitter = "◘•◘"
private const val dynamicCategorySplitter = "▄╪\t▄╪\t"
private val randomTags = arrayOf(0, 1, 2)
private const val randomSource = 4
private const val randomTitle = 3
private const val randomTag = 0
private val randomGroupOfTags = arrayOf(1, 2)
private const val randomGroupOfTagsNormal = 1
private const val randomGroupOfTagsNegate = 2
suspend fun setSearchSuggestion(
preferences: PreferencesHelper,
db: DatabaseHelper,
sourceManager: SourceManager
) {
val random: Random = {
val cal = Calendar.getInstance()
cal.time = Date()
cal[Calendar.MINUTE] = 0
cal[Calendar.SECOND] = 0
cal[Calendar.MILLISECOND] = 0
Random(cal.time.time)
}()
val recentManga by lazy {
runBlocking {
RecentsPresenter.getRecentManga(true).map { it.first }
}
}
val libraryManga by lazy { db.getLibraryMangas().executeAsBlocking() }
preferences.librarySearchSuggestion().set(
when (val value = random.nextInt(0, 5)) {
randomSource -> {
val distinctSources = libraryManga.distinctBy { it.source }
val randomSource =
sourceManager.get(
distinctSources.randomOrNull(random)?.source ?: 0L
)?.name
randomSource?.chopByWords(15)
}
randomTitle -> {
libraryManga.randomOrNull(random)?.title?.chopByWords(15)
}
in randomTags -> {
val tags = recentManga.map {
it.genre.orEmpty().split(",").map(String::trim)
}
.flatten()
.filter { it.isNotBlank() }
val distinctTags = tags.distinct()
if (value in randomGroupOfTags && distinctTags.size > 6) {
val shortestTagsSort = distinctTags.sortedBy { it.length }
val offset = random.nextInt(0, distinctTags.size / 2 - 2)
var offset2 = random.nextInt(0, distinctTags.size / 2 - 2)
while (offset2 == offset) {
offset2 = random.nextInt(0, distinctTags.size / 2 - 2)
}
if (value == randomGroupOfTagsNormal) {
"${shortestTagsSort[offset]}, " + shortestTagsSort[offset2]
} else {
"${shortestTagsSort[offset]}, -" + shortestTagsSort[offset2]
}
} else {
val group = tags.groupingBy { it }.eachCount()
val groupedTags = distinctTags.sortedByDescending { group[it] }
groupedTags.take(8).randomOrNull(random)
}
}
else -> ""
} ?: ""
)
if (preferences.showLibrarySearchSuggestions().isNotSet()) {
preferences.showLibrarySearchSuggestions().set(true)
}
preferences.lastLibrarySuggestion().set(Date().time)
}
/** Give library manga to a date added based on min chapter fetch */
fun updateDB() {
val db: DatabaseHelper = Injekt.get()

View File

@ -112,7 +112,8 @@ class RecentsPresenter(
retryCount: Int = 0,
itemCount: Int = 0,
limit: Boolean = false,
customViewType: Int? = null
customViewType: Int? = null,
includeReadAnyway: Boolean = false
) {
if (retryCount > 5) {
finished = true
@ -127,7 +128,8 @@ class RecentsPresenter(
}
val viewType = customViewType ?: viewType
val showRead = (preferences.showReadInAllRecents().get() || query.isNotEmpty()) && !limit
val showRead = ((preferences.showReadInAllRecents().get() || query.isNotEmpty()) && !limit) ||
includeReadAnyway == true
val isUngrouped = viewType > VIEW_TYPE_GROUP_ALL || query.isNotEmpty()
val groupChaptersUpdates = preferences.groupChaptersUpdates().get()
val groupChaptersHistory = preferences.groupChaptersHistory().get()
@ -487,11 +489,15 @@ class RecentsPresenter(
const val VIEW_TYPE_ONLY_HISTORY = 2
const val VIEW_TYPE_ONLY_UPDATES = 3
const val ENDLESS_LIMIT = 50
var SHORT_LIMIT = 25
private set
suspend fun getRecentManga(): List<Pair<Manga, Long>> {
suspend fun getRecentManga(includeRead: Boolean = false): List<Pair<Manga, Long>> {
val presenter = RecentsPresenter(null)
presenter.viewType = 1
presenter.runRecents(limit = true)
SHORT_LIMIT = if (includeRead) 50 else 25
presenter.runRecents(limit = true, includeReadAnyway = includeRead)
SHORT_LIMIT = 25
return presenter.recentItems.filter { it.mch.manga.id != null }.map { it.mch.manga to it.mch.history.last_read }
}
}

View File

@ -6,8 +6,11 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.DelayedLibrarySuggestionsJob
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.ui.library.display.TabbedLibraryDisplaySheet
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -28,6 +31,25 @@ class SettingsLibraryController : SettingsController() {
defaultValue = false
}
switchPreference {
key = Keys.showLibrarySearchSuggestions
titleRes = R.string.search_suggestions
summaryRes = R.string.search_tips_show_periodically
onChange {
it as Boolean
if (it) {
launchIO {
LibraryPresenter.setSearchSuggestion(preferences, db, Injekt.get())
}
} else {
DelayedLibrarySuggestionsJob.setupTask(false)
preferences.librarySearchSuggestion().set("")
}
true
}
}
preference {
key = "library_display_options"
isPersistent = false

View File

@ -29,6 +29,26 @@ fun String.chop(count: Int, replacement: String = "…"): String {
}
}
fun String.chopByWords(count: Int): String {
return if (length > count) {
val splitWords = split(" ")
val iterator = splitWords.iterator()
var newString = iterator.next()
return if (newString.length > count) {
chop(count)
} else {
var next = iterator.next()
while ("$newString $next".length <= count) {
newString = "$newString $next"
next = iterator.next()
}
newString
}
} else {
this
}
}
fun String.removeArticles(): String {
return when {
startsWith("a ", true) -> substring(2)

View File

@ -213,6 +213,8 @@
<string name="show_notification_error">Show a notification for errors</string>
<string name="display_buttons_bottom_reader">Buttons at bottom of reader</string>
<string name="certain_buttons_can_be_found">Certain buttons can be found in other places if disabled here</string>
<string name="search_suggestions">Search suggestions</string>
<string name="search_tips_show_periodically">Search tips will show up periodically. Long press the suggestion to search it.</string>
<!-- Recents -->
<string name="recents">Recents</string>