diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index e698fe107a..eed83429e4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -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}" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/DelayedLibrarySuggestionsJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/DelayedLibrarySuggestionsJob.kt new file mode 100644 index 0000000000..91c8f08b44 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/DelayedLibrarySuggestionsJob.kt @@ -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() + 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() + .setInitialDelay(1, TimeUnit.DAYS) + .addTag(TAG) + .build() + + WorkManager.getInstance().enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request) + } else { + WorkManager.getInstance().cancelAllWorkByTag(TAG) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index e98f07b192..f0e6d79be4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -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" 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 3156dadf8c..7d54f767b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 762d0275a0..87a03cfb83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 524ca34d38..3a4bf1135a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index d2c237ecc1..457b4edb64 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -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> { + suspend fun getRecentManga(includeRead: Boolean = false): List> { 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 } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt index 53f0d1a680..f630a82a2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt index b528ee0733..b698b11b1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt @@ -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) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 11ea9d97a6..04f7e19a44 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -213,6 +213,8 @@ Show a notification for errors Buttons at bottom of reader Certain buttons can be found in other places if disabled here + Search suggestions + Search tips will show up periodically. Long press the suggestion to search it. Recents