#3520 Searchable Settings (#3683)

* Adding class stubs for settings search, UI elements.

* -  implement searchable settings
- `SettingsController.setupPreferenceScreen` must return a PreferenceScreen

* Remove unneeded SettingsControllerFactory.

* Set query hint, clean up code smell.

* Add search button to MoreController, stop infinite recursion.

* - initialize SearchResultCollection once in Activity.onCreate

* - implement prefernce highlighting after settings search

* - Ensure all Preferences have a key set or else the highlighting effect will have no effect on it.
- remove ExtensionFilterController and SourceFilterController from settingControllersList in SettingsSearchHelper, since those are related to Extensions and not Settings

* Limiting search to settings menu only, localized breadcrumb string, and code cleanup after code review.

* - moved call to SettingsSearchHelper.initPreferenceSearchResultCollection() into SettingsSearchController

* Code review cleanup and refactoring.

* Inlined non-reused key strings.

* Adding more UI polish, add comments for future enhancements.

* - retain search query when navigating *away* from SettingsSearchController
- keep `searchItem` in `expandActionView` state until user goes back (fixes the empty view in `SettingsSearchSearchController` issue)

Co-authored-by: mpm11011 <markuscicero5@gmail.com>
Co-authored-by: lmj0011 <9396189+lmj0011@users.noreply.github.com>
This commit is contained in:
arkon 2020-09-22 22:23:38 -04:00 committed by GitHub
commit 766f9e37b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 712 additions and 19 deletions

View file

@ -273,6 +273,8 @@ dependencies {
testImplementation "org.robolectric:shadows-multidex:$robolectric_version" testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
testImplementation "org.robolectric:shadows-play-services:$robolectric_version" testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
final coroutines_version = '1.3.9' final coroutines_version = '1.3.9'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

View file

@ -253,6 +253,10 @@ class PreferencesHelper(val context: Context) {
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false) fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
fun lastSearchQuerySearchSettings() = prefs.getString("last_search_query", "")
fun lastSearchQuerySearchSettings(query: String) = prefs.edit { putString("last_search_query", query) }
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL) fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
fun filterChapterByDownloaded() = prefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL) fun filterChapterByDownloaded() = prefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL)

View file

@ -13,7 +13,7 @@ import uy.kohesive.injekt.api.get
class ExtensionFilterController : SettingsController() { class ExtensionFilterController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_extensions titleRes = R.string.label_extensions
val activeLangs = preferences.enabledLanguages().get() val activeLangs = preferences.enabledLanguages().get()

View file

@ -24,7 +24,7 @@ class SourceFilterController : SettingsController() {
private val onlineSources by lazy { Injekt.get<SourceManager>().getOnlineSources() } private val onlineSources by lazy { Injekt.get<SourceManager>().getOnlineSources() }
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_sources titleRes = R.string.label_sources
// Get the list of active language codes. // Get the list of active language codes.
@ -46,7 +46,7 @@ class SourceFilterController : SettingsController() {
// Create a preference group and set initial state and change listener // Create a preference group and set initial state and change listener
switchPreferenceCategory { switchPreferenceCategory {
preferenceScreen.addPreference(this) this@apply.addPreference(this)
title = LocaleHelper.getSourceDisplayName(lang, context) title = LocaleHelper.getSourceDisplayName(lang, context)
isPersistent = false isPersistent = false
if (lang in activeLangsCodes) { if (lang in activeLangsCodes) {

View file

@ -41,10 +41,11 @@ class AboutController : SettingsController() {
private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_about titleRes = R.string.pref_category_about
preference { preference {
key = "pref_about_version"
titleRes = R.string.version titleRes = R.string.version
summary = if (BuildConfig.DEBUG) { summary = if (BuildConfig.DEBUG) {
"Preview r${BuildConfig.COMMIT_COUNT} (${BuildConfig.COMMIT_SHA})" "Preview r${BuildConfig.COMMIT_COUNT} (${BuildConfig.COMMIT_SHA})"
@ -55,17 +56,20 @@ class AboutController : SettingsController() {
onClick { copyDebugInfo() } onClick { copyDebugInfo() }
} }
preference { preference {
key = "pref_about_build_time"
titleRes = R.string.build_time titleRes = R.string.build_time
summary = getFormattedBuildTime() summary = getFormattedBuildTime()
} }
if (isUpdaterEnabled) { if (isUpdaterEnabled) {
preference { preference {
key = "pref_about_check_for_updates"
titleRes = R.string.check_for_updates titleRes = R.string.check_for_updates
onClick { checkVersion() } onClick { checkVersion() }
} }
} }
preference { preference {
key = "pref_about_whats_new"
titleRes = R.string.whats_new titleRes = R.string.whats_new
onClick { onClick {
@ -81,6 +85,7 @@ class AboutController : SettingsController() {
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
preference { preference {
key = "pref_about_notices"
titleRes = R.string.notices titleRes = R.string.notices
onClick { onClick {
@ -92,6 +97,7 @@ class AboutController : SettingsController() {
preferenceCategory { preferenceCategory {
preference { preference {
key = "pref_about_website"
titleRes = R.string.website titleRes = R.string.website
val url = "https://tachiyomi.org" val url = "https://tachiyomi.org"
summary = url summary = url
@ -101,6 +107,7 @@ class AboutController : SettingsController() {
} }
} }
preference { preference {
key = "pref_about_discord"
title = "Discord" title = "Discord"
val url = "https://discord.gg/tachiyomi" val url = "https://discord.gg/tachiyomi"
summary = url summary = url
@ -110,6 +117,7 @@ class AboutController : SettingsController() {
} }
} }
preference { preference {
key = "pref_about_github"
title = "GitHub" title = "GitHub"
val url = "https://github.com/inorichi/tachiyomi" val url = "https://github.com/inorichi/tachiyomi"
summary = url summary = url
@ -119,6 +127,7 @@ class AboutController : SettingsController() {
} }
} }
preference { preference {
key = "pref_about_label_extensions"
titleRes = R.string.label_extensions titleRes = R.string.label_extensions
val url = "https://github.com/inorichi/tachiyomi-extensions" val url = "https://github.com/inorichi/tachiyomi-extensions"
summary = url summary = url
@ -128,6 +137,7 @@ class AboutController : SettingsController() {
} }
} }
preference { preference {
key = "pref_about_licenses"
titleRes = R.string.licenses titleRes = R.string.licenses
onClick { onClick {

View file

@ -38,7 +38,7 @@ class MoreController :
private var isDownloading: Boolean = false private var isDownloading: Boolean = false
private var downloadQueueSize: Int = 0 private var downloadQueueSize: Int = 0
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_more titleRes = R.string.label_more
val tintColor = context.getResourceColor(R.attr.colorAccent) val tintColor = context.getResourceColor(R.attr.colorAccent)

View file

@ -33,7 +33,6 @@ import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsAdvancedController : SettingsController() { class SettingsAdvancedController : SettingsController() {
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
private val chapterCache: ChapterCache by injectLazy() private val chapterCache: ChapterCache by injectLazy()
@ -41,9 +40,8 @@ class SettingsAdvancedController : SettingsController() {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_advanced titleRes = R.string.pref_category_advanced
switchPreference { switchPreference {
key = "acra.enable" key = "acra.enable"
titleRes = R.string.pref_enable_acra titleRes = R.string.pref_enable_acra
@ -53,6 +51,7 @@ class SettingsAdvancedController : SettingsController() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
preference { preference {
key = "pref_disable_battery_optimization"
titleRes = R.string.pref_disable_battery_optimization titleRes = R.string.pref_disable_battery_optimization
summaryRes = R.string.pref_disable_battery_optimization_summary summaryRes = R.string.pref_disable_battery_optimization_summary
@ -86,6 +85,7 @@ class SettingsAdvancedController : SettingsController() {
onClick { clearChapterCache() } onClick { clearChapterCache() }
} }
preference { preference {
key = "pref_clear_database"
titleRes = R.string.pref_clear_database titleRes = R.string.pref_clear_database
summaryRes = R.string.pref_clear_database_summary summaryRes = R.string.pref_clear_database_summary
@ -101,6 +101,7 @@ class SettingsAdvancedController : SettingsController() {
titleRes = R.string.label_network titleRes = R.string.label_network
preference { preference {
key = "pref_clear_cookies"
titleRes = R.string.pref_clear_cookies titleRes = R.string.pref_clear_cookies
onClick { onClick {
@ -120,11 +121,13 @@ class SettingsAdvancedController : SettingsController() {
titleRes = R.string.label_library titleRes = R.string.label_library
preference { preference {
key = "pref_refresh_library_covers"
titleRes = R.string.pref_refresh_library_covers titleRes = R.string.pref_refresh_library_covers
onClick { LibraryUpdateService.start(context, target = Target.COVERS) } onClick { LibraryUpdateService.start(context, target = Target.COVERS) }
} }
preference { preference {
key = "pref_refresh_library_tracking"
titleRes = R.string.pref_refresh_library_tracking titleRes = R.string.pref_refresh_library_tracking
summaryRes = R.string.pref_refresh_library_tracking_summary summaryRes = R.string.pref_refresh_library_tracking_summary

View file

@ -49,10 +49,11 @@ class SettingsBackupController : SettingsController() {
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 500) requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 500)
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.backup titleRes = R.string.backup
preference { preference {
key = "pref_create_backup"
titleRes = R.string.pref_create_backup titleRes = R.string.pref_create_backup
summaryRes = R.string.pref_create_backup_summ summaryRes = R.string.pref_create_backup_summ
@ -67,6 +68,7 @@ class SettingsBackupController : SettingsController() {
} }
} }
preference { preference {
key = "pref_restore_backup"
titleRes = R.string.pref_restore_backup titleRes = R.string.pref_restore_backup
summaryRes = R.string.pref_restore_backup_summ summaryRes = R.string.pref_restore_backup_summ

View file

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsBrowseController : SettingsController() { class SettingsBrowseController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.browse titleRes = R.string.browse
preferenceCategory { preferenceCategory {

View file

@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
@ -9,12 +12,14 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceController import androidx.preference.PreferenceController
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -26,6 +31,7 @@ import uy.kohesive.injekt.api.get
abstract class SettingsController : PreferenceController() { abstract class SettingsController : PreferenceController() {
var preferenceKey: String? = null
val preferences: PreferencesHelper = Injekt.get() val preferences: PreferencesHelper = Injekt.get()
val scope = CoroutineScope(Job() + Dispatchers.Main) val scope = CoroutineScope(Job() + Dispatchers.Main)
@ -39,6 +45,24 @@ abstract class SettingsController : PreferenceController() {
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }
override fun onAttach(view: View) {
super.onAttach(view)
preferenceKey?.let { prefKey ->
val adapter = listView.adapter
scrollToPreference(prefKey)
listView.post {
if (adapter is PreferenceGroup.PreferencePositionCallback) {
val pos = adapter.getPreferenceAdapterPosition(prefKey)
listView.findViewHolderForAdapterPosition(pos)?.let {
animatePreferenceHighlight(it.itemView)
}
}
}
}
}
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) super.onDestroyView(view)
untilDestroySubscriptions.unsubscribe() untilDestroySubscriptions.unsubscribe()
@ -50,7 +74,7 @@ abstract class SettingsController : PreferenceController() {
setupPreferenceScreen(screen) setupPreferenceScreen(screen)
} }
abstract fun setupPreferenceScreen(screen: PreferenceScreen): Any? abstract fun setupPreferenceScreen(screen: PreferenceScreen): PreferenceScreen
private fun getThemedContext(): Context { private fun getThemedContext(): Context {
val tv = TypedValue() val tv = TypedValue()
@ -58,6 +82,17 @@ abstract class SettingsController : PreferenceController() {
return ContextThemeWrapper(activity, tv.resourceId) return ContextThemeWrapper(activity, tv.resourceId)
} }
private fun animatePreferenceHighlight(view: View) {
ValueAnimator
.ofObject(ArgbEvaluator(), Color.TRANSPARENT, view.context.getResourceColor(R.attr.rippleColor))
.apply {
duration = 500L
repeatCount = 2
addUpdateListener { animator -> view.setBackgroundColor(animator.animatedValue as Int) }
reverse()
}
}
open fun getTitle(): String? { open fun getTitle(): String? {
return preferenceScreen?.title?.toString() return preferenceScreen?.title?.toString()
} }

View file

@ -40,7 +40,7 @@ class SettingsDownloadController : SettingsController() {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_downloads titleRes = R.string.pref_category_downloads
preference { preference {

View file

@ -24,7 +24,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
class SettingsGeneralController : SettingsController() { class SettingsGeneralController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_general titleRes = R.string.pref_category_general
intListPreference { intListPreference {
@ -47,6 +47,7 @@ class SettingsGeneralController : SettingsController() {
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
preference { preference {
key = "pref_manage_notifications"
titleRes = R.string.pref_manage_notifications titleRes = R.string.pref_manage_notifications
onClick { onClick {
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {

View file

@ -40,7 +40,7 @@ class SettingsLibraryController : SettingsController() {
private val db: DatabaseHelper = Injekt.get() private val db: DatabaseHelper = Injekt.get()
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_library titleRes = R.string.pref_category_library
val dbCategories = db.getCategories().executeAsBlocking() val dbCategories = db.getCategories().executeAsBlocking()
@ -50,6 +50,7 @@ class SettingsLibraryController : SettingsController() {
titleRes = R.string.pref_category_display titleRes = R.string.pref_category_display
preference { preference {
key = "pref_library_columns"
titleRes = R.string.pref_library_columns titleRes = R.string.pref_library_columns
onClick { onClick {
LibraryColumnsDialog().showDialog(router) LibraryColumnsDialog().showDialog(router)
@ -83,6 +84,7 @@ class SettingsLibraryController : SettingsController() {
titleRes = R.string.pref_category_library_categories titleRes = R.string.pref_category_library_categories
preference { preference {
key = "pref_action_edit_categories"
titleRes = R.string.action_edit_categories titleRes = R.string.action_edit_categories
val catCount = dbCategories.size val catCount = dbCategories.size

View file

@ -1,8 +1,13 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.setting.settingssearch.SettingsSearchController
import eu.kanade.tachiyomi.util.preference.iconRes import eu.kanade.tachiyomi.util.preference.iconRes
import eu.kanade.tachiyomi.util.preference.iconTint import eu.kanade.tachiyomi.util.preference.iconTint
import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.onClick
@ -12,7 +17,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
class SettingsMainController : SettingsController() { class SettingsMainController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_settings titleRes = R.string.label_settings
val tintColor = context.getResourceColor(R.attr.colorAccent) val tintColor = context.getResourceColor(R.attr.colorAccent)
@ -82,4 +87,29 @@ class SettingsMainController : SettingsController() {
private fun navigateTo(controller: SettingsController) { private fun navigateTo(controller: SettingsController) {
router.pushController(controller.withFadeTransaction()) router.pushController(controller.withFadeTransaction())
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.settings_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_search_settings)
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
preferences.lastSearchQuerySearchSettings("") // reset saved search query
router.pushController(SettingsSearchController().withFadeTransaction())
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
})
}
} }

View file

@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
class SettingsParentalControlsController : SettingsController() { class SettingsParentalControlsController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_parental_controls titleRes = R.string.pref_category_parental_controls
listPreference { listPreference {

View file

@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsReaderController : SettingsController() { class SettingsReaderController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_reader titleRes = R.string.pref_category_reader
intListPreference { intListPreference {

View file

@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsSecurityController : SettingsController() { class SettingsSecurityController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_security titleRes = R.string.pref_category_security
if (BiometricManager.from(context).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { if (BiometricManager.from(context).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {

View file

@ -31,7 +31,7 @@ class SettingsTrackingController :
private val trackManager: TrackManager by injectLazy() private val trackManager: TrackManager by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_tracking titleRes = R.string.pref_category_tracking
switchPreference { switchPreference {

View file

@ -0,0 +1,80 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.ui.setting.SettingsController
/**
* Adapter that holds the search cards.
*
* @param controller instance of [SettingsSearchController].
*/
class SettingsSearchAdapter(val controller: SettingsSearchController) :
FlexibleAdapter<SettingsSearchItem>(null, controller, true) {
val titleClickListener: OnTitleClickListener = controller
/**
* Bundle where the view state of the holders is saved.
*/
private var bundle = Bundle()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
super.onBindViewHolder(holder, position, payloads)
restoreHolderState(holder)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
saveHolderState(holder, bundle)
}
override fun onSaveInstanceState(outState: Bundle) {
val holdersBundle = Bundle()
allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) }
outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)!!
}
/**
* Saves the view state of the given holder.
*
* @param holder The holder to save.
* @param outState The bundle where the state is saved.
*/
private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
val key = "holder_${holder.bindingAdapterPosition}"
val holderState = SparseArray<Parcelable>()
holder.itemView.saveHierarchyState(holderState)
outState.putSparseParcelableArray(key, holderState)
}
/**
* Restores the view state of the given holder.
*
* @param holder The holder to restore.
*/
private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
val key = "holder_${holder.bindingAdapterPosition}"
bundle.getSparseParcelableArray<Parcelable>(key)?.let {
holder.itemView.restoreHierarchyState(it)
bundle.remove(key)
}
}
interface OnTitleClickListener {
fun onTitleClick(ctrl: SettingsController)
}
private companion object {
const val HOLDER_BUNDLE_KEY = "holder_bundle"
}
}

View file

@ -0,0 +1,168 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.setting.SettingsController
/**
* This controller shows and manages the different search result in settings search.
* [SettingsSearchAdapter.OnTitleClickListener] called when preference is clicked in settings search
*/
class SettingsSearchController :
NucleusController<SettingsSearchControllerBinding, SettingsSearchPresenter>(),
SettingsSearchAdapter.OnTitleClickListener {
/**
* Adapter containing search results grouped by lang.
*/
protected var adapter: SettingsSearchAdapter? = null
lateinit var searchView: SearchView
init {
setHasOptionsMenu(true)
}
/**
* Initiate the view with [R.layout.settings_search_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SettingsSearchControllerBinding.inflate(inflater)
return binding.root
}
override fun getTitle(): String? {
return presenter.query
}
/**
* Create the [SettingsSearchPresenter] used in controller.
*
* @return instance of [SettingsSearchPresenter]
*/
override fun createPresenter(): SettingsSearchPresenter {
return SettingsSearchPresenter()
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
inflater.inflate(R.menu.settings_main, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Change hint to show "search settings."
searchView.queryHint = applicationContext?.getString(R.string.action_search_settings)
searchItem.expandActionView()
setItems(getResultSet())
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
router.popCurrentController()
return false
}
})
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
setItems(getResultSet(query))
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
setItems(getResultSet(newText))
return false
}
})
searchView.setQuery(presenter.preferences.lastSearchQuerySearchSettings(), true)
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SettingsSearchAdapter(this)
// Create recycler and set adapter.
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter
// load all search results
SettingsSearchHelper.initPreferenceSearchResultCollection(presenter.preferences.context)
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onSaveViewState(view: View, outState: Bundle) {
super.onSaveViewState(view, outState)
adapter?.onSaveInstanceState(outState)
}
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
adapter?.onRestoreInstanceState(savedViewState)
}
/**
* returns a list of `SettingsSearchItem` to be shown as search results
* Future update: should we add a minimum length to the query before displaying results? Consider other languages.
*/
fun getResultSet(query: String? = null): List<SettingsSearchItem> {
if (!query.isNullOrBlank()) {
return SettingsSearchHelper.getFilteredResults(query)
.map { SettingsSearchItem(it, null) }
}
return mutableListOf()
}
/**
* Add search result to adapter.
*
* @param searchResult result of search.
*/
fun setItems(searchResult: List<SettingsSearchItem>) {
adapter?.updateDataSet(searchResult)
}
/**
* Opens a catalogue with the given search.
*/
override fun onTitleClick(ctrl: SettingsController) {
searchView.query.let {
presenter.preferences.lastSearchQuerySearchSettings(it.toString())
}
router.pushController(ctrl.withFadeTransaction())
}
}

View file

@ -0,0 +1,136 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.ui.setting.SettingsAdvancedController
import eu.kanade.tachiyomi.ui.setting.SettingsBackupController
import eu.kanade.tachiyomi.ui.setting.SettingsBrowseController
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.ui.setting.SettingsDownloadController
import eu.kanade.tachiyomi.ui.setting.SettingsGeneralController
import eu.kanade.tachiyomi.ui.setting.SettingsLibraryController
import eu.kanade.tachiyomi.ui.setting.SettingsParentalControlsController
import eu.kanade.tachiyomi.ui.setting.SettingsReaderController
import eu.kanade.tachiyomi.ui.setting.SettingsSecurityController
import eu.kanade.tachiyomi.ui.setting.SettingsTrackingController
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.isLTR
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance
object SettingsSearchHelper {
var prefSearchResultList: MutableList<SettingsSearchResult> = mutableListOf()
private set
/**
* All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable.
*/
private val settingControllersList: List<KClass<out SettingsController>> = listOf(
SettingsAdvancedController::class,
SettingsBackupController::class,
SettingsBrowseController::class,
SettingsDownloadController::class,
SettingsGeneralController::class,
SettingsLibraryController::class,
SettingsParentalControlsController::class,
SettingsReaderController::class,
SettingsSecurityController::class,
SettingsTrackingController::class
)
/**
* Must be called to populate `prefSearchResultList`
*/
@SuppressLint("RestrictedApi")
fun initPreferenceSearchResultCollection(context: Context) {
val preferenceManager = PreferenceManager(context)
prefSearchResultList.clear()
launchNow {
settingControllersList.forEach { kClass ->
val ctrl = kClass.createInstance()
val settingsPrefScreen = ctrl.setupPreferenceScreen(preferenceManager.createPreferenceScreen(context))
val prefCount = settingsPrefScreen.preferenceCount
for (i in 0 until prefCount) {
val rootPref = settingsPrefScreen.getPreference(i)
if (rootPref.title == null) continue // no title, not a preference. (note: only info notes appear to not have titles)
getSettingSearchResult(ctrl, rootPref, "${settingsPrefScreen.title}")
}
}
}
}
fun getFilteredResults(query: String): List<SettingsSearchResult> {
return prefSearchResultList.filter {
val inTitle = it.title.contains(query, true)
val inSummary = it.summary.contains(query, true)
val inBreadcrumb = it.breadcrumb.contains(query, true)
return@filter inTitle || inSummary || inBreadcrumb
}
}
/**
* Extracts the data needed from a `Preference` to create a `SettingsSearchResult`, and then adds it to `prefSearchResultList`
* Future enhancement: make bold the text matched by the search query.
*/
private fun getSettingSearchResult(ctrl: SettingsController, pref: Preference, breadcrumbs: String = "") {
when (pref) {
is PreferenceGroup -> {
val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}")
for (x in 0 until pref.preferenceCount) {
val subPref = pref.getPreference(x)
getSettingSearchResult(ctrl, subPref, breadcrumbsStr) // recursion
}
}
is PreferenceCategory -> {
val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}")
for (x in 0 until pref.preferenceCount) {
val subPref = pref.getPreference(x)
getSettingSearchResult(ctrl, subPref, breadcrumbsStr) // recursion
}
}
else -> {
// Is an actual preference
val title = pref.title.toString()
val summary = if (pref.summary != null) pref.summary.toString() else ""
val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}")
prefSearchResultList.add(
SettingsSearchResult(
key = pref.key,
title = title,
summary = summary,
breadcrumb = breadcrumbsStr,
searchController = ctrl
)
)
}
}
}
private fun addLocalizedBreadcrumb(path: String, node: String): String {
return if (Resources.getSystem().isLTR) {
// This locale reads left to right.
"$path > $node"
} else {
// This locale reads right to left.
"$node < $path"
}
}
data class SettingsSearchResult(
val key: String?,
val title: String,
val summary: String,
val breadcrumb: String,
val searchController: SettingsController
)
}

View file

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.view.View
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlin.reflect.full.createInstance
import kotlinx.android.synthetic.main.settings_search_controller_card.search_result_pref_breadcrumb
import kotlinx.android.synthetic.main.settings_search_controller_card.search_result_pref_summary
import kotlinx.android.synthetic.main.settings_search_controller_card.search_result_pref_title
import kotlinx.android.synthetic.main.settings_search_controller_card.title_wrapper
/**
* Holder that binds the [SettingsSearchItem] containing catalogue cards.
*
* @param view view of [SettingsSearchItem]
* @param adapter instance of [SettingsSearchAdapter]
*/
class SettingsSearchHolder(view: View, val adapter: SettingsSearchAdapter) :
BaseFlexibleViewHolder(view, adapter) {
init {
title_wrapper.setOnClickListener {
adapter.getItem(bindingAdapterPosition)?.let {
val ctrl = it.settingsSearchResult.searchController::class.createInstance()
ctrl.preferenceKey = it.settingsSearchResult.key
// must pass a new Controller instance to avoid this error https://github.com/bluelinelabs/Conductor/issues/446
adapter.titleClickListener.onTitleClick(ctrl)
}
}
}
/**
* Show the loading of source search result.
*
* @param item item of card.
*/
fun bind(item: SettingsSearchItem) {
search_result_pref_title.text = item.settingsSearchResult.title
search_result_pref_summary.text = item.settingsSearchResult.summary
search_result_pref_breadcrumb.text = item.settingsSearchResult.breadcrumb
}
}

View file

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
/**
* Item that contains search result information.
*
* @param pref the source for the search results.
* @param results the search results.
*/
class SettingsSearchItem(val settingsSearchResult: SettingsSearchHelper.SettingsSearchResult, val results: List<SettingsSearchItem>?) :
AbstractFlexibleItem<SettingsSearchHolder>() {
override fun getLayoutRes(): Int {
return R.layout.settings_search_controller_card
}
/**
* Create view holder (see [SettingsSearchAdapter].
*
* @return holder of view.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SettingsSearchHolder {
return SettingsSearchHolder(view, adapter as SettingsSearchAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: SettingsSearchHolder,
position: Int,
payloads: List<Any?>?
) {
holder.bind(this)
}
override fun equals(other: Any?): Boolean {
if (other is SettingsSearchItem) {
return settingsSearchResult == settingsSearchResult
}
return false
}
override fun hashCode(): Int {
return settingsSearchResult.hashCode()
}
}

View file

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.os.Bundle
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [SettingsSearchController]
* Function calls should be done from here. UI calls should be done from the controller.
*/
open class SettingsSearchPresenter : BasePresenter<SettingsSearchController>() {
/**
* Query from the view.
*/
var query = ""
private set
val preferences: PreferencesHelper = Injekt.get()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
query = savedState?.getString(SettingsSearchPresenter::query.name) ?: "" // TODO - Some way to restore previous query?
}
override fun onSave(state: Bundle) {
state.putString(SettingsSearchPresenter::query.name, query)
super.onSave(state)
}
}

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="4dp"
android:paddingBottom="4dp"
tools:listitem="@layout/settings_search_controller_card" />
<FrameLayout
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:alpha="0.75" />
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>
</FrameLayout>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/title_wrapper"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/search_result_pref_title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<TextView
android:id="@+id/search_result_pref_summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_result_pref_title"
tools:text="Summary" />
<TextView
android:id="@+id/search_result_pref_breadcrumb"
style="@style/TextAppearance.Regular.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_result_pref_summary"
tools:text="Location" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/action_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="collapseActionView|ifRoom" />
</menu>

View file

@ -44,6 +44,7 @@
<string name="action_sort_latest_chapter">Latest chapter</string> <string name="action_sort_latest_chapter">Latest chapter</string>
<string name="action_sort_date_added">Date added</string> <string name="action_sort_date_added">Date added</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_search_settings">Search settings</string>
<string name="action_global_search">Global search</string> <string name="action_global_search">Global search</string>
<string name="action_select_all">Select all</string> <string name="action_select_all">Select all</string>
<string name="action_select_inverse">Select inverse</string> <string name="action_select_inverse">Select inverse</string>
@ -411,6 +412,7 @@
<string name="licenses">Open source licenses</string> <string name="licenses">Open source licenses</string>
<string name="check_for_updates">Check for updates</string> <string name="check_for_updates">Check for updates</string>
<string name="updated_version">Updated to v%1$s</string> <string name="updated_version">Updated to v%1$s</string>
<string name="about_resources">Resources</string>
<!-- ACRA --> <!-- ACRA -->
<string name="pref_enable_acra">Send crash reports</string> <string name="pref_enable_acra">Send crash reports</string>