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 { return when {
isResuming && endless && offset > 0 -> "LIMIT $offset" isResuming && endless && offset > 0 -> "LIMIT $offset"
endless -> "LIMIT ${RecentsPresenter.ENDLESS_LIMIT}\nOFFSET $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 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 numberOfBackups = "backup_slots"
const val backupInterval = "backup_interval" const val backupInterval = "backup_interval"

View File

@ -234,6 +234,12 @@ class PreferencesHelper(val context: Context) {
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false) 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 numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0) fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)

View File

@ -203,7 +203,15 @@ class LibraryController(
override fun getTitle(): String? { override fun getTitle(): String? {
setSubtitle() 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() { private var scrollListener = object : RecyclerView.OnScrollListener() {
@ -849,6 +857,23 @@ class LibraryController(
binding.recyclerCover.isFocusable = false binding.recyclerCover.isFocusable = false
singleCategory = presenter.categories.size <= 1 singleCategory = presenter.categories.size <= 1
showDropdown() 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 { } else {
updateFilterSheetY() updateFilterSheetY()
closeTip() closeTip()
@ -856,6 +881,7 @@ class LibraryController(
binding.filterBottomSheet.filterBottomSheet.isInvisible = true binding.filterBottomSheet.filterBottomSheet.isInvisible = true
} }
activityBinding?.toolbar?.hideDropdown() 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.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager 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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.preference.minusAssign 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_EXCLUDE
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet.Companion.STATE_IGNORE 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.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.capitalizeWords
import eu.kanade.tachiyomi.util.lang.chopByWords
import eu.kanade.tachiyomi.util.lang.removeArticles import eu.kanade.tachiyomi.util.lang.removeArticles
import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.ArrayList import java.util.ArrayList
import java.util.Calendar
import java.util.Comparator import java.util.Comparator
import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.random.Random
/** /**
* Presenter of [LibraryController]. * Presenter of [LibraryController].
@ -98,6 +107,17 @@ class LibraryPresenter(
lastCategories = null lastCategories = null
lastLibraryItems = null lastLibraryItems = null
getLibrary() 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 */ /** Get favorited manga for library and sort and filter it */
@ -1026,6 +1046,82 @@ class LibraryPresenter(
private const val sourceSplitter = "◘•◘" private const val sourceSplitter = "◘•◘"
private const val dynamicCategorySplitter = "▄╪\t▄╪\t" 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 */ /** Give library manga to a date added based on min chapter fetch */
fun updateDB() { fun updateDB() {
val db: DatabaseHelper = Injekt.get() val db: DatabaseHelper = Injekt.get()

View File

@ -112,7 +112,8 @@ class RecentsPresenter(
retryCount: Int = 0, retryCount: Int = 0,
itemCount: Int = 0, itemCount: Int = 0,
limit: Boolean = false, limit: Boolean = false,
customViewType: Int? = null customViewType: Int? = null,
includeReadAnyway: Boolean = false
) { ) {
if (retryCount > 5) { if (retryCount > 5) {
finished = true finished = true
@ -127,7 +128,8 @@ class RecentsPresenter(
} }
val viewType = customViewType ?: viewType 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 isUngrouped = viewType > VIEW_TYPE_GROUP_ALL || query.isNotEmpty()
val groupChaptersUpdates = preferences.groupChaptersUpdates().get() val groupChaptersUpdates = preferences.groupChaptersUpdates().get()
val groupChaptersHistory = preferences.groupChaptersHistory().get() val groupChaptersHistory = preferences.groupChaptersHistory().get()
@ -487,11 +489,15 @@ class RecentsPresenter(
const val VIEW_TYPE_ONLY_HISTORY = 2 const val VIEW_TYPE_ONLY_HISTORY = 2
const val VIEW_TYPE_ONLY_UPDATES = 3 const val VIEW_TYPE_ONLY_UPDATES = 3
const val ENDLESS_LIMIT = 50 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) val presenter = RecentsPresenter(null)
presenter.viewType = 1 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 } 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.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob 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.category.CategoryController
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.ui.library.display.TabbedLibraryDisplaySheet import eu.kanade.tachiyomi.ui.library.display.TabbedLibraryDisplaySheet
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -28,6 +31,25 @@ class SettingsLibraryController : SettingsController() {
defaultValue = false 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 { preference {
key = "library_display_options" key = "library_display_options"
isPersistent = false 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 { fun String.removeArticles(): String {
return when { return when {
startsWith("a ", true) -> substring(2) startsWith("a ", true) -> substring(2)

View File

@ -213,6 +213,8 @@
<string name="show_notification_error">Show a notification for errors</string> <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="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="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 --> <!-- Recents -->
<string name="recents">Recents</string> <string name="recents">Recents</string>