diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96b7e570cd..4d84310f17 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -156,6 +156,8 @@ dependencies { implementation("com.squareup.retrofit2:retrofit:${Versions.RETROFIT}") implementation("com.squareup.retrofit2:converter-gson:${Versions.RETROFIT}") + implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN)) + // JSON implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.KOTLINSERIALIZATION}") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:${Versions.KOTLINSERIALIZATION}") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/SettingsExtensionsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/SettingsExtensionsController.kt index 73f9140952..3c7ae356c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/SettingsExtensionsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/SettingsExtensionsController.kt @@ -14,7 +14,7 @@ import uy.kohesive.injekt.api.get class SettingsExtensionsController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.filter val activeLangs = preferences.enabledLanguages().get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt index 009400490f..0e720e5856 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt @@ -52,7 +52,7 @@ class AboutController : SettingsController() { private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.about preference { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 85f44ecb8e..45ab82ffae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -50,7 +50,7 @@ class SettingsAdvancedController : SettingsController() { private val coverCache: CoverCache by injectLazy() @SuppressLint("BatteryLife") - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.advanced switchPreference { @@ -70,6 +70,7 @@ class SettingsAdvancedController : SettingsController() { } preference { + key = "clean_cached_covers" titleRes = R.string.clean_up_cached_covers summary = context.getString( R.string.delete_old_covers_in_library_used_, @@ -82,6 +83,7 @@ class SettingsAdvancedController : SettingsController() { } } preference { + key = "clear_cached_not_library" titleRes = R.string.clear_cached_covers_non_library summary = context.getString( R.string.delete_all_covers__not_in_library_used_, @@ -94,6 +96,7 @@ class SettingsAdvancedController : SettingsController() { } } preference { + key = "clean_downloaded_chapters" titleRes = R.string.clean_up_downloaded_chapters summaryRes = R.string.delete_unused_chapters @@ -105,6 +108,7 @@ class SettingsAdvancedController : SettingsController() { } } preference { + key = "clear_database" titleRes = R.string.clear_database summaryRes = R.string.clear_database_summary @@ -119,6 +123,7 @@ class SettingsAdvancedController : SettingsController() { preferenceCategory { titleRes = R.string.network preference { + key = "clear_cookies" titleRes = R.string.clear_cookies onClick { @@ -128,6 +133,7 @@ class SettingsAdvancedController : SettingsController() { } switchPreference { + key = "enable_doh" key = PreferenceKeys.enableDoh titleRes = R.string.dns_over_https summaryRes = R.string.requires_app_restart @@ -138,12 +144,14 @@ class SettingsAdvancedController : SettingsController() { preferenceCategory { titleRes = R.string.library preference { + key = "refresh_lib_meta" titleRes = R.string.refresh_library_metadata summaryRes = R.string.updates_covers_genres_desc onClick { LibraryUpdateService.start(context, target = Target.DETAILS) } } preference { + key = "refresh_teacking_meta" titleRes = R.string.refresh_tracking_metadata summaryRes = R.string.updates_tracking_details @@ -155,6 +163,7 @@ class SettingsAdvancedController : SettingsController() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager? if (pm != null) preference { + key = "disable_batt_opt" titleRes = R.string.disable_battery_optimization summaryRes = R.string.disable_if_issues_with_updating diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt index c176c1000c..2f4903b1e8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt @@ -46,7 +46,7 @@ class SettingsBackupController : SettingsController() { requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 500) } - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.backup preferenceCategory { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt index 54fc512389..1562903ed9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt @@ -19,7 +19,7 @@ class SettingsBrowseController : SettingsController() { val sourceManager: SourceManager by injectLazy() - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.sources preferenceCategory { @@ -64,6 +64,7 @@ class SettingsBrowseController : SettingsController() { } } preference { + key = "match_pinned_sources" titleRes = R.string.match_pinned_sources summaryRes = R.string.only_enable_pinned_for_migration onClick { @@ -84,6 +85,7 @@ class SettingsBrowseController : SettingsController() { } preference { + key = "match_enabled_sources" titleRes = R.string.match_enabled_sources summaryRes = R.string.only_enable_enabled_for_migration onClick { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt index 5e22e8e3f7..91ca153fc7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt @@ -1,6 +1,9 @@ package eu.kanade.tachiyomi.ui.setting +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator import android.content.Context +import android.graphics.Color import android.os.Bundle import android.util.TypedValue import android.view.ContextThemeWrapper @@ -8,13 +11,16 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.preference.PreferenceController +import androidx.preference.PreferenceGroup import androidx.preference.PreferenceScreen import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.controller.BaseController +import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.scrollViewWith import kotlinx.coroutines.MainScope import rx.Observable @@ -25,6 +31,7 @@ import uy.kohesive.injekt.api.get abstract class SettingsController : PreferenceController() { + var preferenceKey: String? = null val preferences: PreferencesHelper = Injekt.get() val viewScope = MainScope() @@ -40,6 +47,24 @@ abstract class SettingsController : PreferenceController() { 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) { super.onDestroyView(view) untilDestroySubscriptions.unsubscribe() @@ -51,7 +76,7 @@ abstract class SettingsController : PreferenceController() { setupPreferenceScreen(screen) } - abstract fun setupPreferenceScreen(screen: PreferenceScreen): Any? + abstract fun setupPreferenceScreen(screen: PreferenceScreen): PreferenceScreen private fun getThemedContext(): Context { val tv = TypedValue() @@ -59,6 +84,17 @@ abstract class SettingsController : PreferenceController() { 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? { return preferenceScreen?.title?.toString() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index 16ce958e3e..6910748ba8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -30,7 +30,7 @@ class SettingsDownloadController : SettingsController() { private val db: DatabaseHelper by injectLazy() - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.downloads preference { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt index d55e0d623c..4dfd468117 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt @@ -15,7 +15,7 @@ class SettingsGeneralController : SettingsController() { private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.general intListPreference(activity) { 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 fb3f699dc9..a4b35de9a5 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 @@ -17,7 +17,7 @@ class SettingsLibraryController : SettingsController() { private val db: DatabaseHelper = Injekt.get() - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.library preferenceCategory { titleRes = R.string.general @@ -34,6 +34,7 @@ class SettingsLibraryController : SettingsController() { preferenceCategory { titleRes = R.string.categories preference { + key = "edit_categories" val catCount = db.getCategories().executeAsBlocking().size 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 4b9cc60894..c837608383 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -3,10 +3,14 @@ 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 com.bluelinelabs.conductor.Controller +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import eu.kanade.tachiyomi.BuildConfig 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.openInBrowser import eu.kanade.tachiyomi.util.view.withFadeTransaction @@ -76,16 +80,38 @@ class SettingsMainController : SettingsController() { titleRes = R.string.about onClick { navigateTo(AboutController()) } } + this } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 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 { when (item.itemId) { R.id.action_help -> activity?.openInBrowser(URL_HELP) - R.id.action_bug_report -> activity?.openInBrowser(URL_BUG_REPORT) else -> return super.onOptionsItemSelected(item) } return true @@ -97,6 +123,5 @@ class SettingsMainController : SettingsController() { private companion object { private const val URL_HELP = "https://tachiyomi.org/help/" - private const val URL_BUG_REPORT = "https://github.com/Jays2Kings/tachiyomiJ2K/issues" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 3ef9e65315..d6a97b88b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -3,11 +3,13 @@ package eu.kanade.tachiyomi.ui.setting import android.os.Build import androidx.preference.PreferenceScreen 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 class SettingsReaderController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.reader preferenceCategory { @@ -200,7 +202,9 @@ class SettingsReaderController : SettingsController() { key = Keys.readWithVolumeKeysInverted titleRes = R.string.invert_volume_keys defaultValue = false - }.apply { dependency = Keys.readWithVolumeKeys } + + preferences.readWithVolumeKeys().asImmediateFlow { isVisible = it }.launchIn(viewScope) + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt index efd401419d..c9b7043676 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt @@ -36,7 +36,7 @@ class SettingsSourcesController : SettingsController() { private var sourcesByLang: TreeMap> = TreeMap() private var sorting = SourcesSort.Alpha - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.filter sorting = SourcesSort.from(preferences.sourceSorting().getOrDefault()) ?: SourcesSort.Alpha activity?.invalidateOptionsMenu() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt index caaca2624b..e6dc13c654 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt @@ -27,7 +27,7 @@ class SettingsTrackingController : private val trackManager: TrackManager by injectLazy() - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.tracking switchPreference { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchAdapter.kt new file mode 100644 index 0000000000..93dd4820e6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchAdapter.kt @@ -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(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 + ) { + 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() + 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(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" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt new file mode 100644 index 0000000000..1c421f907d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt @@ -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(), + 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 { + 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) { + 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 = "" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt new file mode 100644 index 0000000000..2a095a416d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt @@ -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 = mutableListOf() + + /** + * All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable. + */ + private val settingControllersList: List> = 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 { + 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 + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHolder.kt new file mode 100644 index 0000000000..4bfe35562f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHolder.kt @@ -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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchItem.kt new file mode 100644 index 0000000000..3d0dc860a9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchItem.kt @@ -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?, + val searchResult: String +) : + AbstractFlexibleItem() { + + 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> + ): SettingsSearchHolder { + return SettingsSearchHolder(view, adapter as SettingsSearchAdapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: SettingsSearchHolder, + position: Int, + payloads: List? + ) { + 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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt new file mode 100644 index 0000000000..acb595359f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt @@ -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() { + + /** + * 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) + } +} 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 463d40f73f..84b6200c68 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 @@ -1,7 +1,15 @@ 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 + /** * 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`. @@ -54,3 +62,25 @@ fun String.capitalizeWords(): String { fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int { 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 { + val list = mutableListOf() + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/settings_search_controller.xml b/app/src/main/res/layout/settings_search_controller.xml new file mode 100644 index 0000000000..618fdf8754 --- /dev/null +++ b/app/src/main/res/layout/settings_search_controller.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/settings_search_controller_card.xml b/app/src/main/res/layout/settings_search_controller_card.xml new file mode 100644 index 0000000000..50dcf119b8 --- /dev/null +++ b/app/src/main/res/layout/settings_search_controller_card.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/menu/settings_main.xml b/app/src/main/res/menu/settings_main.xml index 45a1c81f95..61803ec38d 100644 --- a/app/src/main/res/menu/settings_main.xml +++ b/app/src/main/res/menu/settings_main.xml @@ -3,10 +3,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + android:id="@+id/action_search" + android:icon="@drawable/ic_search_24dp" + android:title="@string/search" + app:actionViewClass="androidx.appcompat.widget.SearchView" + app:showAsAction="collapseActionView|ifRoom" /> Advanced About Help + Search settings App theme