From 9d1d1710ebc94cf32b5d8fa2b0a4d83c2ed54fbf Mon Sep 17 00:00:00 2001 From: Viktor De Pasquale Date: Thu, 14 Nov 2019 18:56:03 +0100 Subject: [PATCH] Added new search functionality to module screen --- .../topjohnwu/magisk/data/database/Repo.kt | 34 ++++- .../topjohnwu/magisk/redesign/MainActivity.kt | 29 ++-- .../magisk/redesign/compat/CompatFragment.kt | 2 +- .../magisk/redesign/compat/Queryable.kt | 26 ++++ .../magisk/redesign/hide/HideViewModel.kt | 18 +-- .../magisk/redesign/module/ModuleFragment.kt | 35 ++++- .../magisk/redesign/module/ModuleViewModel.kt | 76 ++++++++-- app/src/main/res/drawable/bg_shadow.xml | 8 ++ .../main/res/layout/fragment_module_md2.xml | 72 +++++++--- .../main/res/layout/include_module_filter.xml | 131 ++++++++++++++++++ .../main/res/values-v21/styles_md2_impl.xml | 7 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/styles_md2.xml | 6 +- app/src/main/res/values/styles_md2_impl.xml | 6 + 14 files changed, 395 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/com/topjohnwu/magisk/redesign/compat/Queryable.kt create mode 100644 app/src/main/res/drawable/bg_shadow.xml create mode 100644 app/src/main/res/layout/include_module_filter.xml diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt b/app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt index 3c42c9d2e..457f969b3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt @@ -8,7 +8,12 @@ import com.topjohnwu.magisk.model.entity.module.Repo interface RepoBase { - fun getRepos(offset: Int, limit: Int = 10): List + fun getRepos(offset: Int, limit: Int = LIMIT): List + fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List + + companion object { + const val LIMIT = 10 + } } @@ -18,6 +23,19 @@ interface RepoByUpdatedDao : RepoBase { @Query("SELECT * FROM repos ORDER BY last_update DESC LIMIT :limit OFFSET :offset") override fun getRepos(offset: Int, limit: Int): List + @Query( + """SELECT * + FROM repos + WHERE + (author LIKE '%' || :query || '%') || + (name LIKE '%' || :query || '%') || + (description LIKE '%' || :query || '%') + ORDER BY last_update DESC + LIMIT :limit + OFFSET :offset""" + ) + override fun searchRepos(query: String, offset: Int, limit: Int): List + } @Dao @@ -26,4 +44,18 @@ interface RepoByNameDao : RepoBase { @Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset") override fun getRepos(offset: Int, limit: Int): List + @Query( + """SELECT * + FROM repos + WHERE + (author LIKE '%' || :query || '%') || + (name LIKE '%' || :query || '%') || + (description LIKE '%' || :query || '%') + ORDER BY name COLLATE NOCASE + LIMIT :limit + OFFSET :offset""" + ) + override fun searchRepos(query: String, offset: Int, limit: Int): List + + } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/MainActivity.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/MainActivity.kt index 75571b3e6..e448acdcc 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/MainActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/MainActivity.kt @@ -116,19 +116,7 @@ open class MainActivity : CompatActivity( transactionType: FragNavController.TransactionType ) { setDisplayHomeAsUpEnabled(!navigation.isRoot) - - val lapam = binding.mainBottomBar.layoutParams as ViewGroup.MarginLayoutParams - val height = binding.mainBottomBar.measuredHeight - val verticalMargin = lapam.let { it.topMargin + it.bottomMargin } - val maxTranslation = height + verticalMargin - val translation = if (navigation.isRoot) 0 else maxTranslation - - binding.mainBottomBar.animate() - .translationY(translation.toFloat()) - .setInterpolator(FastOutSlowInInterpolator()) - .withStartAction { if (navigation.isRoot) binding.mainBottomBar.isVisible = true } - .withEndAction { if (!navigation.isRoot) binding.mainBottomBar.isVisible = false } - .start() + requestNavigationHidden(!navigation.isRoot) } override fun peekSystemWindowInsets(insets: Insets) { @@ -143,4 +131,19 @@ open class MainActivity : CompatActivity( } } + internal fun requestNavigationHidden(hide: Boolean = true) { + val lapam = binding.mainBottomBar.layoutParams as ViewGroup.MarginLayoutParams + val height = binding.mainBottomBar.measuredHeight + val verticalMargin = lapam.let { it.topMargin + it.bottomMargin } + val maxTranslation = height + verticalMargin + val translation = if (!hide) 0 else maxTranslation + + binding.mainBottomBar.animate() + .translationY(translation.toFloat()) + .setInterpolator(FastOutSlowInInterpolator()) + .withStartAction { if (!hide) binding.mainBottomBar.isVisible = true } + .withEndAction { if (hide) binding.mainBottomBar.isVisible = false } + .start() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatFragment.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatFragment.kt index 6c9c90352..c4b942736 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatFragment.kt @@ -18,7 +18,7 @@ abstract class CompatFragment + protected val compatActivity get() = requireActivity() as CompatActivity<*, *> override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/Queryable.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/Queryable.kt new file mode 100644 index 000000000..68c785b61 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/Queryable.kt @@ -0,0 +1,26 @@ +package com.topjohnwu.magisk.redesign.compat + +import android.os.Handler +import android.os.Looper + +interface Queryable { + + val queryDelay: Long + val queryHandler: Handler + val queryRunnable: Runnable + + fun submitQuery() + + companion object { + fun impl(delay: Long = 1000L) = object : Queryable { + override val queryDelay = delay + override val queryHandler = Handler(Looper.getMainLooper()) + override val queryRunnable = Runnable { TODO() } + + override fun submitQuery() { + queryHandler.removeCallbacks(queryRunnable) + queryHandler.postDelayed(queryRunnable, queryDelay) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideViewModel.kt index 260d34313..9e7b43d20 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideViewModel.kt @@ -1,8 +1,6 @@ package com.topjohnwu.magisk.redesign.hide import android.content.pm.ApplicationInfo -import android.os.Handler -import android.os.Looper import androidx.databinding.Bindable import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.data.repository.MagiskRepository @@ -16,20 +14,18 @@ import com.topjohnwu.magisk.model.entity.StatefulProcess import com.topjohnwu.magisk.model.entity.recycler.HideItem import com.topjohnwu.magisk.model.entity.recycler.HideProcessItem import com.topjohnwu.magisk.redesign.compat.CompatViewModel +import com.topjohnwu.magisk.redesign.compat.Queryable import com.topjohnwu.magisk.redesign.home.itemBindingOf import com.topjohnwu.magisk.utils.DiffObservableList import com.topjohnwu.magisk.utils.FilterableDiffObservableList import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.magisk.utils.currentLocale -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers class HideViewModel( private val magiskRepo: MagiskRepository -) : CompatViewModel() { +) : CompatViewModel(), Queryable by Queryable.impl(1000) { - private val queryHandler = Handler(Looper.getMainLooper()) - private val queryRunnable = Runnable { query() } + override val queryRunnable = Runnable { query() } var isShowSystem = false @Bindable get @@ -67,7 +63,7 @@ class HideViewModel( .applyViewModel(this) .subscribeK { items.update(it.first, it.second) - query() + submitQuery() } // --- @@ -87,9 +83,9 @@ class HideViewModel( // --- - private fun submitQuery() { + override fun submitQuery() { queryHandler.removeCallbacks(queryRunnable) - queryHandler.postDelayed(queryRunnable, 1000) + queryHandler.postDelayed(queryRunnable, queryDelay) } private fun query( @@ -130,7 +126,7 @@ class HideViewModel( } -inline fun > filterableListOf( +inline fun > filterableListOf( vararg newItems: T ) = FilterableDiffObservableList(object : DiffObservableList.Callback { override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem) diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleFragment.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleFragment.kt index 5cb682805..181ca3d54 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleFragment.kt @@ -5,7 +5,10 @@ import android.os.Bundle import android.view.View import com.topjohnwu.magisk.R import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding +import com.topjohnwu.magisk.redesign.MainActivity import com.topjohnwu.magisk.redesign.compat.CompatFragment +import com.topjohnwu.magisk.redesign.compat.hideKeyboard +import com.topjohnwu.magisk.redesign.hide.MotionRevealHelper import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener import org.koin.androidx.viewmodel.ext.android.viewModel @@ -14,7 +17,7 @@ class ModuleFragment : CompatFragment override val layoutRes = R.layout.fragment_module_md2 override val viewModel by viewModel() - private lateinit var listener: EndlessRecyclerScrollListener + private val listeners = hashSetOf() override fun consumeSystemWindowInsets(insets: Insets) = insets @@ -26,21 +29,45 @@ class ModuleFragment : CompatFragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setEndlessScroller() + setEndlessSearch() + + binding.moduleFilterToggle.setOnClickListener { + (activity as? MainActivity)?.requestNavigationHidden() + MotionRevealHelper.withViews(binding.moduleFilter, binding.moduleFilterToggle, true) + } + binding.moduleFilterInclude.moduleFilterDone.setOnClickListener { + (activity as? MainActivity)?.requestNavigationHidden(false) + hideKeyboard() + MotionRevealHelper.withViews(binding.moduleFilter, binding.moduleFilterToggle, false) + } } override fun onDestroyView() { - if (this::listener.isInitialized) { - binding.moduleRemote.removeOnScrollListener(listener) + listeners.forEach { + binding.moduleRemote.removeOnScrollListener(it) + binding.moduleFilterInclude.moduleFilterList.removeOnScrollListener(it) } super.onDestroyView() } + override fun onPreBind(binding: FragmentModuleMd2Binding) = Unit + private fun setEndlessScroller() { val lama = binding.moduleRemote.layoutManager ?: return lama.isAutoMeasureEnabled = false - listener = EndlessRecyclerScrollListener(lama, viewModel::loadRemote) + val listener = EndlessRecyclerScrollListener(lama, viewModel::loadRemote) binding.moduleRemote.addOnScrollListener(listener) + listeners.add(listener) + } + + private fun setEndlessSearch() { + val lama = binding.moduleFilterInclude.moduleFilterList.layoutManager ?: return + lama.isAutoMeasureEnabled = false + + val listener = EndlessRecyclerScrollListener(lama, viewModel::loadMoreQuery) + binding.moduleFilterInclude.moduleFilterList.addOnScrollListener(listener) + listeners.add(listener) } } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleViewModel.kt index 155ab4a78..3c5595701 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleViewModel.kt @@ -1,6 +1,7 @@ package com.topjohnwu.magisk.redesign.module import androidx.annotation.WorkerThread +import androidx.databinding.Bindable import androidx.databinding.ViewDataBinding import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.Config @@ -19,11 +20,14 @@ import com.topjohnwu.magisk.model.entity.recycler.RepoItem import com.topjohnwu.magisk.model.entity.recycler.SectionTitle import com.topjohnwu.magisk.model.events.dialog.ModuleInstallDialog import com.topjohnwu.magisk.redesign.compat.CompatViewModel +import com.topjohnwu.magisk.redesign.compat.Queryable import com.topjohnwu.magisk.redesign.home.itemBindingOf import com.topjohnwu.magisk.redesign.superuser.diffListOf import com.topjohnwu.magisk.tasks.RepoUpdater +import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.magisk.utils.currentLocale import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter import timber.log.Timber @@ -33,7 +37,27 @@ class ModuleViewModel( private val repoName: RepoByNameDao, private val repoUpdated: RepoByUpdatedDao, private val repoUpdater: RepoUpdater -) : CompatViewModel() { +) : CompatViewModel(), Queryable by Queryable.impl(1000) { + + override val queryRunnable = Runnable { query() } + + var query = "" + @Bindable get + set(value) { + if (field == value) return + field = value + notifyPropertyChanged(BR.query) + submitQuery() + // Yes we do lie about the search being loaded + searchLoading.value = true + } + + private var queryJob: Disposable? = null + val searchLoading = KObservableField(false) + val itemsSearch = diffListOf() + val itemSearchBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) + } val adapter = adapterOf>() val items = diffListOf>() @@ -102,7 +126,7 @@ class ModuleViewModel( return } remoteJob = Single.fromCallable { itemsRemote.size } - .flatMap { loadRepos(offset = it) } + .flatMap { loadRemoteInternal(offset = it) } .map { it.map { RepoItem(it) } } .subscribeK(onError = { Timber.e(it) @@ -114,14 +138,49 @@ class ModuleViewModel( } } - private fun loadRepos( + // --- + + override fun submitQuery() { + queryHandler.removeCallbacks(queryRunnable) + queryHandler.postDelayed(queryRunnable, queryDelay) + } + + private fun queryInternal(query: String, offset: Int): Single> { + if (query.isBlank()) { + return Single.just(listOf()) + .doOnSubscribe { itemsSearch.clear() } + .subscribeOn(AndroidSchedulers.mainThread()) + } + return Single.fromCallable { dao.searchRepos(query, offset) } + .map { it.map { RepoItem(it) } } + } + + private fun query(query: String = this.query, offset: Int = 0) { + queryJob?.dispose() + queryJob = queryInternal(query, offset) + .map { it to itemsSearch.calculateDiff(it) } + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess { searchLoading.value = false } + .subscribeK { itemsSearch.update(it.first, it.second) } + } + + @Synchronized + fun loadMoreQuery() { + if (queryJob?.isDisposed == false) return + queryJob = queryInternal(query, itemsSearch.size) + .subscribeK { itemsSearch.addAll(it) } + } + + // --- + + private fun loadRemoteInternal( offset: Int = 0, downloadRepos: Boolean = offset == 0 ): Single> = Single.fromCallable { dao.getRepos(offset) }.flatMap { when { // in case we find result empty and offset is initial we need to refresh the repos. downloadRepos && it.isEmpty() && offset == 0 -> downloadRepos() - .andThen(loadRepos(downloadRepos = false)) + .andThen(loadRemoteInternal(downloadRepos = false)) else -> Single.just(it) } } @@ -137,10 +196,11 @@ class ModuleViewModel( .sortedBy { it.item.name.toLowerCase(currentLocale) } .toList() - private fun update(repo: Repo, progress: Int) = Single.fromCallable { itemsRemote } - .map { it.first { it.item.id == repo.id } } - .subscribeK { it.progress.value = progress } - .add() + private fun update(repo: Repo, progress: Int) = + Single.fromCallable { itemsRemote + itemsSearch } + .map { it.first { it.item.id == repo.id } } + .subscribeK { it.progress.value = progress } + .add() // --- diff --git a/app/src/main/res/drawable/bg_shadow.xml b/app/src/main/res/drawable/bg_shadow.xml new file mode 100644 index 000000000..4f7bd989b --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_module_md2.xml b/app/src/main/res/layout/fragment_module_md2.xml index e552171c9..814777139 100644 --- a/app/src/main/res/layout/fragment_module_md2.xml +++ b/app/src/main/res/layout/fragment_module_md2.xml @@ -15,23 +15,61 @@ - + android:layout_height="match_parent"> + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/include_module_filter.xml b/app/src/main/res/layout/include_module_filter.xml new file mode 100644 index 000000000..c1c3d2b57 --- /dev/null +++ b/app/src/main/res/layout/include_module_filter.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-v21/styles_md2_impl.xml b/app/src/main/res/values-v21/styles_md2_impl.xml index 89ef37cc0..ce1e57cba 100644 --- a/app/src/main/res/values-v21/styles_md2_impl.xml +++ b/app/src/main/res/values-v21/styles_md2_impl.xml @@ -8,4 +8,11 @@ ?colorPrimary + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index d22750e35..0e48fce00 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -50,6 +50,7 @@ + diff --git a/app/src/main/res/values/styles_md2.xml b/app/src/main/res/values/styles_md2.xml index b90f0c985..36263fc3b 100644 --- a/app/src/main/res/values/styles_md2.xml +++ b/app/src/main/res/values/styles_md2.xml @@ -56,7 +56,11 @@ @style/WidgetFoundation.RadioButton @style/WidgetFoundation.ProgressBar - @style/WidgetFoundation.ProgressBar.Indeterminate + + @style/WidgetFoundation.ProgressBar.Indeterminate + + + @style/WidgetFoundation.ProgressBar.Indeterminate.Circular diff --git a/app/src/main/res/values/styles_md2_impl.xml b/app/src/main/res/values/styles_md2_impl.xml index a1eda00ae..aa7e3e3fa 100644 --- a/app/src/main/res/values/styles_md2_impl.xml +++ b/app/src/main/res/values/styles_md2_impl.xml @@ -162,4 +162,10 @@ variant. Make sure to use style referenced by attribute defined it attrs.xml. 100dp + + \ No newline at end of file