diff --git a/app/build.gradle b/app/build.gradle index f461b9e22c..c31d594822 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -273,6 +273,8 @@ dependencies { testImplementation "org.robolectric:shadows-multidex:$robolectric_version" testImplementation "org.robolectric:shadows-play-services:$robolectric_version" + implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN" + final coroutines_version = '1.3.9' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index ce43275517..62a98904b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -253,6 +253,10 @@ class PreferencesHelper(val context: Context) { 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 filterChapterByDownloaded() = prefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt index 31f72bc728..8a5b3abf87 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt @@ -13,7 +13,7 @@ import uy.kohesive.injekt.api.get class ExtensionFilterController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.label_extensions val activeLangs = preferences.enabledLanguages().get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt index 7d629be0b4..4781db7f1e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt @@ -24,7 +24,7 @@ class SourceFilterController : SettingsController() { private val onlineSources by lazy { Injekt.get().getOnlineSources() } - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.label_sources // 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 switchPreferenceCategory { - preferenceScreen.addPreference(this) + this@apply.addPreference(this) title = LocaleHelper.getSourceDisplayName(lang, context) isPersistent = false if (lang in activeLangsCodes) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt index 1100041df7..75d0aa6e92 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt @@ -41,10 +41,11 @@ 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.pref_category_about preference { + key = "pref_about_version" titleRes = R.string.version summary = if (BuildConfig.DEBUG) { "Preview r${BuildConfig.COMMIT_COUNT} (${BuildConfig.COMMIT_SHA})" @@ -55,17 +56,20 @@ class AboutController : SettingsController() { onClick { copyDebugInfo() } } preference { + key = "pref_about_build_time" titleRes = R.string.build_time summary = getFormattedBuildTime() } if (isUpdaterEnabled) { preference { + key = "pref_about_check_for_updates" titleRes = R.string.check_for_updates onClick { checkVersion() } } } preference { + key = "pref_about_whats_new" titleRes = R.string.whats_new onClick { @@ -81,6 +85,7 @@ class AboutController : SettingsController() { } if (BuildConfig.DEBUG) { preference { + key = "pref_about_notices" titleRes = R.string.notices onClick { @@ -92,6 +97,7 @@ class AboutController : SettingsController() { preferenceCategory { preference { + key = "pref_about_website" titleRes = R.string.website val url = "https://tachiyomi.org" summary = url @@ -101,6 +107,7 @@ class AboutController : SettingsController() { } } preference { + key = "pref_about_discord" title = "Discord" val url = "https://discord.gg/tachiyomi" summary = url @@ -110,6 +117,7 @@ class AboutController : SettingsController() { } } preference { + key = "pref_about_github" title = "GitHub" val url = "https://github.com/inorichi/tachiyomi" summary = url @@ -119,6 +127,7 @@ class AboutController : SettingsController() { } } preference { + key = "pref_about_label_extensions" titleRes = R.string.label_extensions val url = "https://github.com/inorichi/tachiyomi-extensions" summary = url @@ -128,6 +137,7 @@ class AboutController : SettingsController() { } } preference { + key = "pref_about_licenses" titleRes = R.string.licenses onClick { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt index 24062f3ecc..aa839a9ec1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt @@ -38,7 +38,7 @@ class MoreController : private var isDownloading: Boolean = false private var downloadQueueSize: Int = 0 - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.label_more val tintColor = context.getResourceColor(R.attr.colorAccent) 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 5d532bee0b..036896b240 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 @@ -33,7 +33,6 @@ import uy.kohesive.injekt.injectLazy import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys class SettingsAdvancedController : SettingsController() { - private val network: NetworkHelper by injectLazy() private val chapterCache: ChapterCache by injectLazy() @@ -41,9 +40,8 @@ class SettingsAdvancedController : SettingsController() { private val db: DatabaseHelper by injectLazy() @SuppressLint("BatteryLife") - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.pref_category_advanced - switchPreference { key = "acra.enable" titleRes = R.string.pref_enable_acra @@ -53,6 +51,7 @@ class SettingsAdvancedController : SettingsController() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { preference { + key = "pref_disable_battery_optimization" titleRes = R.string.pref_disable_battery_optimization summaryRes = R.string.pref_disable_battery_optimization_summary @@ -86,6 +85,7 @@ class SettingsAdvancedController : SettingsController() { onClick { clearChapterCache() } } preference { + key = "pref_clear_database" titleRes = R.string.pref_clear_database summaryRes = R.string.pref_clear_database_summary @@ -101,6 +101,7 @@ class SettingsAdvancedController : SettingsController() { titleRes = R.string.label_network preference { + key = "pref_clear_cookies" titleRes = R.string.pref_clear_cookies onClick { @@ -120,11 +121,13 @@ class SettingsAdvancedController : SettingsController() { titleRes = R.string.label_library preference { + key = "pref_refresh_library_covers" titleRes = R.string.pref_refresh_library_covers onClick { LibraryUpdateService.start(context, target = Target.COVERS) } } preference { + key = "pref_refresh_library_tracking" titleRes = R.string.pref_refresh_library_tracking summaryRes = R.string.pref_refresh_library_tracking_summary 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 dbcafbb2ab..dd1ef0bf98 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 @@ -49,10 +49,11 @@ 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 preference { + key = "pref_create_backup" titleRes = R.string.pref_create_backup summaryRes = R.string.pref_create_backup_summ @@ -67,6 +68,7 @@ class SettingsBackupController : SettingsController() { } } preference { + key = "pref_restore_backup" titleRes = R.string.pref_restore_backup summaryRes = R.string.pref_restore_backup_summ 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 0def71f54a..61defdcfb5 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 @@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys class SettingsBrowseController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.browse preferenceCategory { 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 ee9c15366d..62414f7ef1 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 @@ -9,12 +12,14 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -26,6 +31,7 @@ import uy.kohesive.injekt.api.get abstract class SettingsController : PreferenceController() { + var preferenceKey: String? = null val preferences: PreferencesHelper = Injekt.get() val scope = CoroutineScope(Job() + Dispatchers.Main) @@ -39,6 +45,24 @@ abstract class SettingsController : PreferenceController() { 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) { super.onDestroyView(view) untilDestroySubscriptions.unsubscribe() @@ -50,7 +74,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() @@ -58,6 +82,17 @@ abstract class SettingsController : PreferenceController() { 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? { 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 ad9e7097c7..d11d8dea85 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 @@ -40,7 +40,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.pref_category_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 4154e8fe5b..8052663c6e 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 @@ -24,7 +24,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values class SettingsGeneralController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.pref_category_general intListPreference { @@ -47,6 +47,7 @@ class SettingsGeneralController : SettingsController() { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { preference { + key = "pref_manage_notifications" titleRes = R.string.pref_manage_notifications onClick { val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { 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 09d433af71..46f2d21366 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 @@ -40,7 +40,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.pref_category_library val dbCategories = db.getCategories().executeAsBlocking() @@ -50,6 +50,7 @@ class SettingsLibraryController : SettingsController() { titleRes = R.string.pref_category_display preference { + key = "pref_library_columns" titleRes = R.string.pref_library_columns onClick { LibraryColumnsDialog().showDialog(router) @@ -83,6 +84,7 @@ class SettingsLibraryController : SettingsController() { titleRes = R.string.pref_category_library_categories preference { + key = "pref_action_edit_categories" titleRes = R.string.action_edit_categories val catCount = dbCategories.size 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 9c3f48580b..a7794073f6 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 @@ -1,8 +1,13 @@ 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 eu.kanade.tachiyomi.R 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.iconTint import eu.kanade.tachiyomi.util.preference.onClick @@ -12,7 +17,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor class SettingsMainController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.label_settings val tintColor = context.getResourceColor(R.attr.colorAccent) @@ -82,4 +87,29 @@ class SettingsMainController : SettingsController() { private fun navigateTo(controller: SettingsController) { 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 + } + }) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsParentalControlsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsParentalControlsController.kt index 60e92af7f5..58859bdc8f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsParentalControlsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsParentalControlsController.kt @@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values class SettingsParentalControlsController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.pref_category_parental_controls listPreference { 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 2a9d84843d..f4162958e1 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 @@ -19,7 +19,7 @@ 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.pref_category_reader intListPreference { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt index 82483286c8..ba8e9a6500 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt @@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys class SettingsSecurityController : SettingsController() { - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = R.string.pref_category_security if (BiometricManager.from(context).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { 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 5099075a01..ffb2daee8d 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 @@ -31,7 +31,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.pref_category_tracking switchPreference { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchAdapter.kt new file mode 100644 index 0000000000..a0a58e6207 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchAdapter.kt @@ -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(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/settingssearch/SettingsSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchController.kt new file mode 100644 index 0000000000..3c57e612cb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchController.kt @@ -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(), + 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 { + 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) { + 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()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchHelper.kt new file mode 100644 index 0000000000..57fabdda6b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchHelper.kt @@ -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 = mutableListOf() + private set + + /** + * 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, + 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 { + 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 + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchHolder.kt new file mode 100644 index 0000000000..469310eca1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchHolder.kt @@ -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 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchItem.kt new file mode 100644 index 0000000000..cb6417472a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchItem.kt @@ -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?) : + 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/settingssearch/SettingsSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchPresenter.kt new file mode 100644 index 0000000000..90e59ba001 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/settingssearch/SettingsSearchPresenter.kt @@ -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() { + + /** + * 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/res/layout/settings_search_controller.xml b/app/src/main/res/layout/settings_search_controller.xml new file mode 100644 index 0000000000..807f904dda --- /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..ada7639e7a --- /dev/null +++ b/app/src/main/res/layout/settings_search_controller_card.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/settings_main.xml b/app/src/main/res/menu/settings_main.xml new file mode 100644 index 0000000000..112431a726 --- /dev/null +++ b/app/src/main/res/menu/settings_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f2c1aa9f9..7c8a5a8bfb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,6 +44,7 @@ Latest chapter Date added Search + Search settings Global search Select all Select inverse @@ -411,6 +412,7 @@ Open source licenses Check for updates Updated to v%1$s + Resources Send crash reports