浏览代码

Migrate settings search view to Compose

arkon 2 年之前
父节点
当前提交
9b0d85bf6c

+ 85 - 0
app/src/main/java/eu/kanade/presentation/more/settings/SettingsSearchScreen.kt

@@ -0,0 +1,85 @@
+package eu.kanade.presentation.more.settings
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.dp
+import eu.kanade.presentation.components.ScrollbarLazyColumn
+import eu.kanade.presentation.util.horizontalPadding
+import eu.kanade.tachiyomi.ui.setting.SettingsController
+import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchHelper
+import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchPresenter
+import kotlin.reflect.full.createInstance
+
+@Composable
+fun SettingsSearchScreen(
+    nestedScroll: NestedScrollConnection,
+    presenter: SettingsSearchPresenter,
+    onClickResult: (SettingsController) -> Unit,
+) {
+    val results by presenter.state.collectAsState()
+
+    val scrollState = rememberLazyListState()
+    ScrollbarLazyColumn(
+        modifier = Modifier
+            .nestedScroll(nestedScroll),
+        contentPadding = WindowInsets.navigationBars.asPaddingValues(),
+        state = scrollState,
+    ) {
+        items(
+            items = results,
+            key = { it.key.toString() },
+        ) { result ->
+            SearchResult(result, onClickResult)
+        }
+    }
+}
+
+@Composable
+private fun SearchResult(
+    result: SettingsSearchHelper.SettingsSearchResult,
+    onClickResult: (SettingsController) -> Unit,
+) {
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(horizontal = horizontalPadding, vertical = 8.dp)
+            .clickable {
+                // Must pass a new Controller instance to avoid this error
+                // https://github.com/bluelinelabs/Conductor/issues/446
+                val controller = result.searchController::class.createInstance()
+                controller.preferenceKey = result.key
+                onClickResult(controller)
+            },
+    ) {
+        Text(
+            text = result.title,
+        )
+
+        Text(
+            text = result.summary,
+            style = MaterialTheme.typography.bodySmall.copy(
+                color = MaterialTheme.colorScheme.outline,
+            ),
+        )
+
+        Text(
+            text = result.breadcrumb,
+            style = MaterialTheme.typography.bodySmall,
+        )
+    }
+}

+ 0 - 82
app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchAdapter.kt

@@ -1,82 +0,0 @@
-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 const val HOLDER_BUNDLE_KEY = "holder_bundle"

+ 22 - 105
app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt

@@ -1,68 +1,45 @@
 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 androidx.appcompat.widget.SearchView
-import androidx.recyclerview.widget.LinearLayoutManager
-import dev.chrisbanes.insetter.applyInsetter
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import eu.kanade.presentation.more.settings.SettingsSearchScreen
 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.ComposeController
 import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.setting.SettingsController
 
-/**
- * This controller shows and manages the different search result in settings search.
- * [SettingsSearchAdapter.OnTitleClickListener] called when preference is clicked in settings search
- */
-class SettingsSearchController :
-    NucleusController<SettingsSearchControllerBinding, SettingsSearchPresenter>(),
-    SettingsSearchAdapter.OnTitleClickListener {
+class SettingsSearchController : ComposeController<SettingsSearchPresenter>() {
 
-    /**
-     * Adapter containing search results grouped by lang.
-     */
-    private var adapter: SettingsSearchAdapter? = null
     private lateinit var searchView: SearchView
 
     init {
         setHasOptionsMenu(true)
     }
 
-    override fun createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater)
+    override fun getTitle() = presenter.query
 
-    override fun getTitle(): String? {
-        return presenter.query
-    }
+    override fun createPresenter() = SettingsSearchPresenter()
 
-    /**
-     * Create the [SettingsSearchPresenter] used in controller.
-     *
-     * @return instance of [SettingsSearchPresenter]
-     */
-    override fun createPresenter(): SettingsSearchPresenter {
-        return SettingsSearchPresenter()
+    @Composable
+    override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
+        SettingsSearchScreen(
+            nestedScroll = nestedScrollInterop,
+            presenter = presenter,
+            onClickResult = { controller ->
+                searchView.query.let {
+                    presenter.setLastSearchQuerySearchSettings(it.toString())
+                }
+                router.pushController(controller)
+            },
+        )
     }
 
-    /**
-     * 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) {
         inflater.inflate(R.menu.settings_main, menu)
 
-        binding.recycler.applyInsetter {
-            type(navigationBars = true) {
-                padding()
-            }
-        }
-
         // Initialize search menu
         val searchItem = menu.findItem(R.id.action_search)
         searchView = searchItem.actionView as SearchView
@@ -70,7 +47,6 @@ class SettingsSearchController :
         searchView.queryHint = applicationContext?.getString(R.string.action_search_settings)
 
         searchItem.expandActionView()
-        setItems(getResultSet())
 
         searchItem.setOnActionExpandListener(
             object : MenuItem.OnActionExpandListener {
@@ -88,76 +64,17 @@ class SettingsSearchController :
         searchView.setOnQueryTextListener(
             object : SearchView.OnQueryTextListener {
                 override fun onQueryTextSubmit(query: String?): Boolean {
-                    setItems(getResultSet(query))
+                    presenter.searchSettings(query)
                     return false
                 }
 
                 override fun onQueryTextChange(newText: String?): Boolean {
-                    setItems(getResultSet(newText))
+                    presenter.searchSettings(newText)
                     return false
                 }
             },
         )
 
-        searchView.setQuery(presenter.preferences.lastSearchQuerySearchSettings().get(), true)
-    }
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        adapter = SettingsSearchAdapter(this)
-        binding.recycler.layoutManager = LinearLayoutManager(view.context)
-        binding.recycler.adapter = adapter
-
-        // load all search results
-        SettingsSearchHelper.initPreferenceSearchResultCollection(presenter.preferences.context)
-    }
-
-    override fun onDestroyView(view: View) {
-        adapter = null
-        super.onDestroyView(view)
-    }
-
-    override fun onSaveViewState(view: View, outState: Bundle) {
-        super.onSaveViewState(view, outState)
-        adapter?.onSaveInstanceState(outState)
-    }
-
-    override fun onRestoreViewState(view: View, savedViewState: Bundle) {
-        super.onRestoreViewState(view, savedViewState)
-        adapter?.onRestoreInstanceState(savedViewState)
-    }
-
-    /**
-     * returns a list of `SettingsSearchItem` to be shown as search results
-     * Future update: should we add a minimum length to the query before displaying results? Consider other languages.
-     */
-    fun getResultSet(query: String? = null): List<SettingsSearchItem> {
-        if (!query.isNullOrBlank()) {
-            return SettingsSearchHelper.getFilteredResults(query)
-                .map { SettingsSearchItem(it, null) }
-        }
-
-        return mutableListOf()
-    }
-
-    /**
-     * Add search result to adapter.
-     *
-     * @param searchResult result of search.
-     */
-    fun setItems(searchResult: List<SettingsSearchItem>) {
-        adapter?.updateDataSet(searchResult)
-    }
-
-    /**
-     * Opens a catalogue with the given search.
-     */
-    override fun onTitleClick(ctrl: SettingsController) {
-        searchView.query.let {
-            presenter.preferences.lastSearchQuerySearchSettings().set(it.toString())
-        }
-
-        router.pushController(ctrl)
+        searchView.setQuery(presenter.getLastSearchQuerySearchSettings(), true)
     }
 }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt

@@ -46,7 +46,7 @@ object SettingsSearchHelper {
      * Must be called to populate `prefSearchResultList`
      */
     @SuppressLint("RestrictedApi")
-    fun initPreferenceSearchResultCollection(context: Context) {
+    fun initPreferenceSearchResults(context: Context) {
         val preferenceManager = PreferenceManager(context)
         prefSearchResultList.clear()
 

+ 0 - 41
app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHolder.kt

@@ -1,41 +0,0 @@
-package eu.kanade.tachiyomi.ui.setting.search
-
-import android.view.View
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.databinding.SettingsSearchControllerCardBinding
-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) {
-
-    private val binding = SettingsSearchControllerCardBinding.bind(view)
-
-    init {
-        binding.titleWrapper.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) {
-        binding.searchResultPrefTitle.text = item.settingsSearchResult.title
-        binding.searchResultPrefSummary.text = item.settingsSearchResult.summary
-        binding.searchResultPrefBreadcrumb.text = item.settingsSearchResult.breadcrumb
-    }
-}

+ 0 - 57
app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchItem.kt

@@ -1,57 +0,0 @@
-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>?,
-) :
-    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()
-    }
-}

+ 25 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt

@@ -3,20 +3,39 @@ 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 kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
-class SettingsSearchPresenter : BasePresenter<SettingsSearchController>() {
+class SettingsSearchPresenter(
+    private val preferences: PreferencesHelper = Injekt.get(),
+) : BasePresenter<SettingsSearchController>() {
 
-    val preferences: PreferencesHelper = Injekt.get()
+    private val _state: MutableStateFlow<List<SettingsSearchHelper.SettingsSearchResult>> =
+        MutableStateFlow(emptyList())
+    val state: StateFlow<List<SettingsSearchHelper.SettingsSearchResult>> = _state.asStateFlow()
 
     override fun onCreate(savedState: Bundle?) {
         super.onCreate(savedState)
-        query = savedState?.getString(SettingsSearchPresenter::query.name) ?: "" // TODO - Some way to restore previous query?
+
+        SettingsSearchHelper.initPreferenceSearchResults(preferences.context)
+    }
+
+    fun getLastSearchQuerySearchSettings(): String {
+        return preferences.lastSearchQuerySearchSettings().get()
+    }
+
+    fun setLastSearchQuerySearchSettings(query: String) {
+        preferences.lastSearchQuerySearchSettings().set(query)
     }
 
-    override fun onSave(state: Bundle) {
-        state.putString(SettingsSearchPresenter::query.name, query)
-        super.onSave(state)
+    fun searchSettings(query: String?) {
+        _state.value = if (!query.isNullOrBlank()) {
+            SettingsSearchHelper.getFilteredResults(query)
+        } else {
+            emptyList()
+        }
     }
 }

+ 0 - 36
app/src/main/res/layout/settings_search_controller.xml

@@ -1,36 +0,0 @@
-<?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>

+ 0 - 37
app/src/main/res/layout/settings_search_controller_card.xml

@@ -1,37 +0,0 @@
-<?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"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAppearance="?android:attr/textAppearanceListItem"
-        tools:text="Title" />
-
-    <TextView
-        android:id="@+id/search_result_pref_summary"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAppearance="?attr/textAppearanceListItemSecondary"
-        android:textColor="?android:attr/textColorSecondary"
-        tools:text="Summary" />
-
-    <TextView
-        android:id="@+id/search_result_pref_breadcrumb"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:ellipsize="end"
-        android:maxLines="1"
-        android:textAppearance="?attr/textAppearanceBodySmall"
-        android:textColor="?android:attr/textColorPrimary"
-        tools:text="Location" />
-
-</LinearLayout>
-