Adding class stubs for settings search, UI elements.

This commit is contained in:
mpm11011 2020-08-16 19:08:09 -04:00
parent 64bdfabbd8
commit 22518f173f
11 changed files with 617 additions and 0 deletions

View File

@ -201,6 +201,8 @@ dependencies {
// Preferences // Preferences
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0' implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
implementation 'com.github.ByteHamster:SearchPreference:v1.0.3'
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// Model View Presenter // Model View Presenter
final nucleus_version = '3.0.0' final nucleus_version = '3.0.0'

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.ui.setting
import android.content.Context
import com.bytehamster.lib.preferencesearch.SearchPreference
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsControllerFactory(context: Context) {
var searchablePrefs = Keys::class.members.map { member -> SearchPreference(context).key = member.name }
companion object Factory {
var controllers: List<SettingsController>? = null
}
}

View File

@ -1,14 +1,23 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.view.Menu
import android.view.MenuInflater
import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.iconRes
import eu.kanade.tachiyomi.util.preference.iconTint import eu.kanade.tachiyomi.util.preference.iconTint
import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
class SettingsMainController : SettingsController() { class SettingsMainController : SettingsController() {
@ -82,4 +91,29 @@ class SettingsMainController : SettingsController() {
private fun navigateTo(controller: SettingsController) { private fun navigateTo(controller: SettingsController) {
router.pushController(controller.withFadeTransaction()) 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_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { performSettingsSearch(it.queryText.toString()) }
.launchIn(scope)
}
private fun performSettingsSearch(query: String) {
router.pushController(
SettingsSearchController(query).withFadeTransaction()
)
}
} }

View File

@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import androidx.preference.Preference
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
/**
* 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}"
val holderState = bundle.getSparseParcelableArray<Parcelable>(key)
if (holderState != null) {
holder.itemView.restoreHierarchyState(holderState)
bundle.remove(key)
}
}
interface OnTitleClickListener {
fun onTitleClick(pref: Preference)
}
private companion object {
const val HOLDER_BUNDLE_KEY = "holder_bundle"
}
}

View File

@ -0,0 +1,177 @@
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.preference.Preference
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.SettingsControllerFactory
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
/**
* This controller shows and manages the different search result in settings search.
* This controller should only handle UI actions, IO actions should be done by [SettingsSearchPresenter]
* [SettingsSearchAdapter.WhatListener] called when preference is clicked in settings search
*/
open class SettingsSearchController(
protected val initialQuery: String? = null,
protected val extensionFilter: String? = null
) : NucleusController<SettingsSearchControllerBinding, SettingsSearchPresenter>(),
SettingsSearchAdapter.OnTitleClickListener {
/**
* Adapter containing search results grouped by lang.
*/
protected var adapter: SettingsSearchAdapter? = null
protected var controllers = SettingsControllerFactory.controllers
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(initialQuery, extensionFilter)
}
/**
* 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)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
})
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach {
presenter.search(it.queryText.toString())
searchItem.collapseActionView()
setTitle() // Update toolbar title
}
.launchIn(scope)
}
/**
* Called when the view is created
*
* @param view view of controller
*/
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
}
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 the view holder for the given preference.
*
* @param pref used to find holder containing source
* @return the holder of the preference or null if it's not bound.
*/
private fun getHolder(pref: Preference): SettingsSearchHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.bindingAdapterPosition)
if (item != null && pref.key == item.pref.key) {
return holder as SettingsSearchHolder
}
}
return null
}
/**
* 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(pref: Preference) {
// TODO - These asserts will be the death of me, fix them.
for (ctrl in this!!.controllers!!) {
if (ctrl.findPreference(pref.key) != null) {
router.pushController(ctrl.withFadeTransaction())
}
}
}
}

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.view.View
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.settings_search_controller_card.setting
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) {
/**
* Adapter containing preference from search results.
*/
private val settingsAdapter = SettingsSearchAdapter(adapter.controller)
private var lastBoundResults: List<SettingsSearchItem>? = null
init {
title_wrapper.setOnClickListener {
adapter.getItem(bindingAdapterPosition)?.let {
adapter.titleClickListener.onTitleClick(it.pref)
}
}
}
/**
* Show the loading of source search result.
*
* @param item item of card.
*/
fun bind(item: SettingsSearchItem) {
val preference = item.pref
val results = item.results
val titlePrefix = if (item.highlighted) "" else ""
// Set Title with country code if available.
setting.text = titlePrefix + preference.key
if (results !== lastBoundResults) {
settingsAdapter.updateDataSet(results)
lastBoundResults = results
}
}
}

View File

@ -0,0 +1,71 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.view.View
import androidx.preference.Preference
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.
* @param highlighted whether this search item should be highlighted/marked in the catalogue search view.
*/
class SettingsSearchItem(val pref: Preference, val results: List<SettingsSearchItem>?, val highlighted: Boolean = false) :
AbstractFlexibleItem<SettingsSearchHolder>() {
/**
* Set view.
*
* @return id of view
*/
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)
}
/**
* Bind item to view.
*/
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: SettingsSearchHolder,
position: Int,
payloads: List<Any?>?
) {
holder.bind(this)
}
/**
* Used to check if two items are equal.
*
* @return items are equal?
*/
override fun equals(other: Any?): Boolean {
if (other is SettingsSearchItem) {
return pref.key == other.pref.key
}
return false
}
/**
* Return hash code of item.
*
* @return hashcode
*/
override fun hashCode(): Int {
return pref.key.toInt()
}
}

View File

@ -0,0 +1,83 @@
package eu.kanade.tachiyomi.ui.setting.settingssearch
import android.os.Bundle
import androidx.preference.Preference
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import rx.Subscription
import rx.subjects.PublishSubject
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
/**
* Presenter of [SettingsSearchController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param db manages the database calls.
* @param preferences manages the preference calls.
*/
open class SettingsSearchPresenter(
val initialQuery: String? = "",
val initialExtensionFilter: String? = null,
val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<SettingsSearchController>() {
/**
* Query from the view.
*/
var query = ""
private set
/**
* Fetches the different sources by user settings.
*/
private var fetchSourcesSubscription: Subscription? = null
/**
* Subject which fetches image of given manga.
*/
private val fetchImageSubject = PublishSubject.create<Pair<List<Preference>, Source>>()
/**
* Subscription for fetching images of manga.
*/
private var fetchImageSubscription: Subscription? = null
private val extensionManager by injectLazy<ExtensionManager>()
private var extensionFilter: String? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
extensionFilter = savedState?.getString(SettingsSearchPresenter::extensionFilter.name)
?: initialExtensionFilter
// TODO - Perform a search with previous or initial state
}
override fun onDestroy() {
fetchSourcesSubscription?.unsubscribe()
fetchImageSubscription?.unsubscribe()
super.onDestroy()
}
override fun onSave(state: Bundle) {
state.putString(BrowseSourcePresenter::query.name, query)
state.putString(SettingsSearchPresenter::extensionFilter.name, extensionFilter)
super.onSave(state)
}
fun search(toString: String) {
// TODO - My ignorance of kotlin pattern is showing here... why would the search logic take place in the Presenter?
}
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="4dp"
android:paddingBottom="4dp"
tools:listitem="@layout/settings_search_controller_card" />
<FrameLayout
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:alpha="0.75" />
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>
</FrameLayout>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/title_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/setting"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
app:layout_constraintBottom_toTopOf="@+id/location"
app:layout_constraintEnd_toStartOf="@+id/goto_icon"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
tools:text="Title" />
<TextView
android:id="@+id/location"
style="@style/TextAppearance.Regular.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
app:layout_constraintEnd_toStartOf="@+id/goto_icon"
app:layout_constraintStart_toStartOf="@+id/setting"
app:layout_constraintTop_toBottomOf="@+id/setting"
tools:text="Location" />
<ImageView
android:id="@+id/goto_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/label_more"
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_forward_24dp"
app:tint="?android:attr/textColorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -0,0 +1,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/action_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="collapseActionView|ifRoom" />
</menu>