Added Search for settings

Also added highlighting to the search results as well

Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2021-03-23 23:40:01 -04:00
parent 6654882c1f
commit eb585e79d8
25 changed files with 738 additions and 20 deletions

View File

@ -156,6 +156,8 @@ dependencies {
implementation("com.squareup.retrofit2:retrofit:${Versions.RETROFIT}") implementation("com.squareup.retrofit2:retrofit:${Versions.RETROFIT}")
implementation("com.squareup.retrofit2:converter-gson:${Versions.RETROFIT}") implementation("com.squareup.retrofit2:converter-gson:${Versions.RETROFIT}")
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
// JSON // JSON
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.KOTLINSERIALIZATION}") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.KOTLINSERIALIZATION}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:${Versions.KOTLINSERIALIZATION}") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:${Versions.KOTLINSERIALIZATION}")

View File

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

View File

@ -52,7 +52,7 @@ 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.about titleRes = R.string.about
preference { preference {

View File

@ -50,7 +50,7 @@ class SettingsAdvancedController : SettingsController() {
private val coverCache: CoverCache by injectLazy() private val coverCache: CoverCache by injectLazy()
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.advanced titleRes = R.string.advanced
switchPreference { switchPreference {
@ -70,6 +70,7 @@ class SettingsAdvancedController : SettingsController() {
} }
preference { preference {
key = "clean_cached_covers"
titleRes = R.string.clean_up_cached_covers titleRes = R.string.clean_up_cached_covers
summary = context.getString( summary = context.getString(
R.string.delete_old_covers_in_library_used_, R.string.delete_old_covers_in_library_used_,
@ -82,6 +83,7 @@ class SettingsAdvancedController : SettingsController() {
} }
} }
preference { preference {
key = "clear_cached_not_library"
titleRes = R.string.clear_cached_covers_non_library titleRes = R.string.clear_cached_covers_non_library
summary = context.getString( summary = context.getString(
R.string.delete_all_covers__not_in_library_used_, R.string.delete_all_covers__not_in_library_used_,
@ -94,6 +96,7 @@ class SettingsAdvancedController : SettingsController() {
} }
} }
preference { preference {
key = "clean_downloaded_chapters"
titleRes = R.string.clean_up_downloaded_chapters titleRes = R.string.clean_up_downloaded_chapters
summaryRes = R.string.delete_unused_chapters summaryRes = R.string.delete_unused_chapters
@ -105,6 +108,7 @@ class SettingsAdvancedController : SettingsController() {
} }
} }
preference { preference {
key = "clear_database"
titleRes = R.string.clear_database titleRes = R.string.clear_database
summaryRes = R.string.clear_database_summary summaryRes = R.string.clear_database_summary
@ -119,6 +123,7 @@ class SettingsAdvancedController : SettingsController() {
preferenceCategory { preferenceCategory {
titleRes = R.string.network titleRes = R.string.network
preference { preference {
key = "clear_cookies"
titleRes = R.string.clear_cookies titleRes = R.string.clear_cookies
onClick { onClick {
@ -128,6 +133,7 @@ class SettingsAdvancedController : SettingsController() {
} }
switchPreference { switchPreference {
key = "enable_doh"
key = PreferenceKeys.enableDoh key = PreferenceKeys.enableDoh
titleRes = R.string.dns_over_https titleRes = R.string.dns_over_https
summaryRes = R.string.requires_app_restart summaryRes = R.string.requires_app_restart
@ -138,12 +144,14 @@ class SettingsAdvancedController : SettingsController() {
preferenceCategory { preferenceCategory {
titleRes = R.string.library titleRes = R.string.library
preference { preference {
key = "refresh_lib_meta"
titleRes = R.string.refresh_library_metadata titleRes = R.string.refresh_library_metadata
summaryRes = R.string.updates_covers_genres_desc summaryRes = R.string.updates_covers_genres_desc
onClick { LibraryUpdateService.start(context, target = Target.DETAILS) } onClick { LibraryUpdateService.start(context, target = Target.DETAILS) }
} }
preference { preference {
key = "refresh_teacking_meta"
titleRes = R.string.refresh_tracking_metadata titleRes = R.string.refresh_tracking_metadata
summaryRes = R.string.updates_tracking_details summaryRes = R.string.updates_tracking_details
@ -155,6 +163,7 @@ class SettingsAdvancedController : SettingsController() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager? val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager?
if (pm != null) preference { if (pm != null) preference {
key = "disable_batt_opt"
titleRes = R.string.disable_battery_optimization titleRes = R.string.disable_battery_optimization
summaryRes = R.string.disable_if_issues_with_updating summaryRes = R.string.disable_if_issues_with_updating

View File

@ -46,7 +46,7 @@ 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
preferenceCategory { preferenceCategory {

View File

@ -19,7 +19,7 @@ class SettingsBrowseController : SettingsController() {
val sourceManager: SourceManager by injectLazy() val sourceManager: SourceManager by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.sources titleRes = R.string.sources
preferenceCategory { preferenceCategory {
@ -64,6 +64,7 @@ class SettingsBrowseController : SettingsController() {
} }
} }
preference { preference {
key = "match_pinned_sources"
titleRes = R.string.match_pinned_sources titleRes = R.string.match_pinned_sources
summaryRes = R.string.only_enable_pinned_for_migration summaryRes = R.string.only_enable_pinned_for_migration
onClick { onClick {
@ -84,6 +85,7 @@ class SettingsBrowseController : SettingsController() {
} }
preference { preference {
key = "match_enabled_sources"
titleRes = R.string.match_enabled_sources titleRes = R.string.match_enabled_sources
summaryRes = R.string.only_enable_enabled_for_migration summaryRes = R.string.only_enable_enabled_for_migration
onClick { onClick {

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
@ -8,13 +11,16 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
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 eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import rx.Observable import rx.Observable
@ -25,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 viewScope = MainScope() val viewScope = MainScope()
@ -40,6 +47,24 @@ abstract class SettingsController : PreferenceController() {
return view return view
} }
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()
@ -51,7 +76,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()
@ -59,6 +84,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, ContextCompat.getColor(view.context, R.color.fullRippleColor))
.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

@ -30,7 +30,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.downloads titleRes = R.string.downloads
preference { preference {

View File

@ -15,7 +15,7 @@ class SettingsGeneralController : 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.general titleRes = R.string.general
intListPreference(activity) { intListPreference(activity) {

View File

@ -17,7 +17,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.library titleRes = R.string.library
preferenceCategory { preferenceCategory {
titleRes = R.string.general titleRes = R.string.general
@ -34,6 +34,7 @@ class SettingsLibraryController : SettingsController() {
preferenceCategory { preferenceCategory {
titleRes = R.string.categories titleRes = R.string.categories
preference { preference {
key = "edit_categories"
val catCount = db.getCategories().executeAsBlocking().size val catCount = db.getCategories().executeAsBlocking().size
titleRes = if (catCount > 0) R.string.edit_categories else R.string.add_categories titleRes = if (catCount > 0) R.string.edit_categories else R.string.add_categories
if (catCount > 0) summary = context.resources.getQuantityString(R.plurals.category, catCount, catCount) if (catCount > 0) summary = context.resources.getQuantityString(R.plurals.category, catCount, catCount)

View File

@ -3,10 +3,14 @@ package eu.kanade.tachiyomi.ui.setting
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchController
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
@ -76,16 +80,38 @@ class SettingsMainController : SettingsController() {
titleRes = R.string.about titleRes = R.string.about
onClick { navigateTo(AboutController()) } onClick { navigateTo(AboutController()) }
} }
this
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.settings_main, menu) inflater.inflate(R.menu.settings_main, menu)
menu.findItem(R.id.action_bug_report).isVisible = BuildConfig.DEBUG
// 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.search_settings)
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
SettingsSearchController.lastSearch = "" // reset saved search query
router.pushController(
RouterTransaction.with(SettingsSearchController()))
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
}
)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_help -> activity?.openInBrowser(URL_HELP) R.id.action_help -> activity?.openInBrowser(URL_HELP)
R.id.action_bug_report -> activity?.openInBrowser(URL_BUG_REPORT)
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
return true return true
@ -97,6 +123,5 @@ class SettingsMainController : SettingsController() {
private companion object { private companion object {
private const val URL_HELP = "https://tachiyomi.org/help/" private const val URL_HELP = "https://tachiyomi.org/help/"
private const val URL_BUG_REPORT = "https://github.com/Jays2Kings/tachiyomiJ2K/issues"
} }
} }

View File

@ -3,11 +3,13 @@ package eu.kanade.tachiyomi.ui.setting
import android.os.Build import android.os.Build
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import kotlinx.coroutines.flow.launchIn
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys 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.reader titleRes = R.string.reader
preferenceCategory { preferenceCategory {
@ -200,7 +202,9 @@ class SettingsReaderController : SettingsController() {
key = Keys.readWithVolumeKeysInverted key = Keys.readWithVolumeKeysInverted
titleRes = R.string.invert_volume_keys titleRes = R.string.invert_volume_keys
defaultValue = false defaultValue = false
}.apply { dependency = Keys.readWithVolumeKeys }
preferences.readWithVolumeKeys().asImmediateFlow { isVisible = it }.launchIn(viewScope)
}
} }
} }
} }

View File

@ -36,7 +36,7 @@ class SettingsSourcesController : SettingsController() {
private var sourcesByLang: TreeMap<String, MutableList<HttpSource>> = TreeMap() private var sourcesByLang: TreeMap<String, MutableList<HttpSource>> = TreeMap()
private var sorting = SourcesSort.Alpha private var sorting = SourcesSort.Alpha
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.filter titleRes = R.string.filter
sorting = SourcesSort.from(preferences.sourceSorting().getOrDefault()) ?: SourcesSort.Alpha sorting = SourcesSort.from(preferences.sourceSorting().getOrDefault()) ?: SourcesSort.Alpha
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()

View File

@ -27,7 +27,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.tracking titleRes = R.string.tracking
switchPreference { switchPreference {

View File

@ -0,0 +1,84 @@
package eu.kanade.tachiyomi.ui.setting.search
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,181 @@
package eu.kanade.tachiyomi.ui.setting.search
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.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.view.liftAppbarWith
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import kotlinx.android.synthetic.main.settings_search_controller.*
/**
* 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<SettingsSearchPresenter>(),
SettingsSearchAdapter.OnTitleClickListener {
/**
* Adapter containing search results grouped by lang.
*/
protected var adapter: SettingsSearchAdapter? = null
private 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 {
return inflater.inflate(R.layout.settings_search_controller, container, false)
}
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
menu.findItem(R.id.action_help).isVisible = false
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.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 {
if (!newText.isNullOrBlank()) {
lastSearch = newText
}
setItems(getResultSet(newText))
return false
}
}
)
searchView.setQuery(lastSearch, true)
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SettingsSearchAdapter(this)
liftAppbarWith(recycler)
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(view.context)
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, query) }
}
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 {
lastSearch = it.toString()
}
router.pushController(ctrl.withFadeTransaction())
}
companion object {
var lastSearch = ""
}
}

View File

@ -0,0 +1,136 @@
package eu.kanade.tachiyomi.ui.setting.search
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.SettingsReaderController
import eu.kanade.tachiyomi.ui.setting.SettingsTrackingController
import eu.kanade.tachiyomi.util.system.isLTR
import eu.kanade.tachiyomi.util.system.launchNow
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance
object SettingsSearchHelper {
private var prefSearchResultList: MutableList<SettingsSearchResult> = mutableListOf()
/**
* 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,
SettingsReaderController::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.replace(">", "").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
}
}
pref 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
}
}
(pref.title != null && pref.isVisible) -> {
// Is an actual preference
val title = pref.title.toString()
// ListPreferences occasionally run into ArrayIndexOutOfBoundsException issues
val summary = try { pref.summary?.toString() ?: "" } catch (e: Throwable) { "" }
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,46 @@
package eu.kanade.tachiyomi.ui.setting.search
import android.text.Html
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.highlightText
import kotlinx.android.synthetic.main.settings_search_controller_card.view.*
import kotlin.reflect.full.createInstance
/**
* 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) :
FlexibleViewHolder(view, adapter) {
init {
view.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) {
val color = ColorUtils.setAlphaComponent(ContextCompat.getColor(itemView.context, R.color.colorAccent), 75)
itemView.search_result_pref_title.text = item.settingsSearchResult.title.highlightText(item.searchResult, color)
itemView.search_result_pref_summary.text = item.settingsSearchResult.summary.highlightText(item.searchResult, color)
itemView.search_result_pref_breadcrumb.text = item.settingsSearchResult.breadcrumb.highlightText(item.searchResult, color)
}
}

View File

@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.ui.setting.search
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>?,
val searchResult: String
) :
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.search
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

@ -1,7 +1,15 @@
package eu.kanade.tachiyomi.util.lang package eu.kanade.tachiyomi.util.lang
import android.graphics.Color
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.BackgroundColorSpan
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import kotlin.math.floor import kotlin.math.floor
/** /**
* Replaces the given string to have at most [count] characters using [replacement] at its end. * Replaces the given string to have at most [count] characters using [replacement] at its end.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`. * If [replacement] is longer than [count] an exception will be thrown when `length > count`.
@ -54,3 +62,25 @@ fun String.capitalizeWords(): String {
fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int { fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int {
return String.CASE_INSENSITIVE_ORDER.then(naturalOrder()).compare(this, other) return String.CASE_INSENSITIVE_ORDER.then(naturalOrder()).compare(this, other)
} }
fun String.highlightText(highlight: String, @ColorInt color: Int): Spanned {
val wordToSpan: Spannable = SpannableString(this)
indexesOf(highlight).forEach {
wordToSpan.setSpan(BackgroundColorSpan(color), it, it + highlight.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return wordToSpan
}
fun String.indexesOf(substr: String, ignoreCase: Boolean = true): List<Int> {
val list = mutableListOf<Int>()
if (substr.isBlank()) return list
var i = -1
while(true) {
i = indexOf(substr, i + 1, ignoreCase)
when (i) {
-1 -> return list
else -> list.add(i)
}
}
}

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:alpha="0.75"
android:background="?attr/colorSurface" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
</FrameLayout>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/title_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/search_result_pref_title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Title" />
<TextView
android:id="@+id/search_result_pref_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Summary" />
<TextView
android:id="@+id/search_result_pref_breadcrumb"
style="@style/TextAppearance.Regular.Caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
tools:text="Location" />
</LinearLayout>

View File

@ -3,10 +3,11 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/action_bug_report" android:id="@+id/action_search"
android:title="@string/bug_report" android:icon="@drawable/ic_search_24dp"
android:icon="@drawable/ic_bug_report_24dp" android:title="@string/search"
app:showAsAction="ifRoom"/> app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="collapseActionView|ifRoom" />
<item <item
android:id="@+id/action_help" android:id="@+id/action_help"

View File

@ -496,6 +496,7 @@
<string name="advanced">Advanced</string> <string name="advanced">Advanced</string>
<string name="about">About</string> <string name="about">About</string>
<string name="help">Help</string> <string name="help">Help</string>
<string name="search_settings">Search settings</string>
<!-- General settings --> <!-- General settings -->
<string name="app_theme">App theme</string> <string name="app_theme">App theme</string>