Drop leanback implementation of search

This commit is contained in:
Niels van Velzen
2025-04-20 22:08:54 +02:00
committed by Niels van Velzen
parent 2a32babc8e
commit fa83fc1b30
4 changed files with 110 additions and 188 deletions

View File

@ -1,43 +0,0 @@
package org.jellyfin.androidtv.ui.search
import android.os.Bundle
import android.view.View
import androidx.leanback.app.SearchSupportFragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class LeanbackSearchFragment : SearchSupportFragment(), SearchSupportFragment.SearchResultProvider {
private val viewModel: SearchViewModel by viewModel()
private val searchFragmentDelegate: SearchFragmentDelegate by inject {
parametersOf(requireContext())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSearchResultProvider(this)
setOnItemViewClickedListener(searchFragmentDelegate.onItemViewClickedListener)
setOnItemViewSelectedListener(searchFragmentDelegate.onItemViewSelectedListener)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.searchResultsFlow
.onEach { searchFragmentDelegate.showResults(it) }
.launchIn(lifecycleScope)
val query = arguments?.getString(SearchFragment.EXTRA_QUERY)
if (!query.isNullOrBlank()) setSearchQuery(query, true)
}
override fun getResultsAdapter() = searchFragmentDelegate.rowsAdapter
override fun onQueryTextChange(query: String): Boolean = viewModel.searchDebounced(query)
override fun onQueryTextSubmit(query: String): Boolean = viewModel.searchImmediately(query)
}

View File

@ -1,39 +1,129 @@
package org.jellyfin.androidtv.ui.search
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.speech.SpeechRecognizer
import androidx.core.content.ContextCompat
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import org.jellyfin.androidtv.R
import androidx.leanback.app.RowsSupportFragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.jellyfin.androidtv.databinding.FragmentSearchBinding
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class SearchFragment : Fragment(R.layout.fragment_content_view) {
class SearchFragment : Fragment() {
companion object {
const val EXTRA_QUERY = "query"
}
private val isSpeechEnabled by lazy {
SpeechRecognizer.isRecognitionAvailable(requireContext())
&& ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_DENIED
private val viewModel: SearchViewModel by viewModel()
private var _binding: FragmentSearchBinding? = null
private val binding get() = _binding!!
private val searchFragmentDelegate: SearchFragmentDelegate by inject {
parametersOf(requireContext())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSearchBinding.inflate(inflater, container, false)
// Determine fragment to use
val searchFragment = when {
isSpeechEnabled -> LeanbackSearchFragment::class.java
else -> TextSearchFragment::class.java
binding.searchBar.apply {
onTextChanged { viewModel.searchDebounced(it) }
onSubmit { viewModel.searchImmediately(it) }
}
val rowsSupportFragment = RowsSupportFragment().apply {
adapter = searchFragmentDelegate.rowsAdapter
onItemViewClickedListener = searchFragmentDelegate.onItemViewClickedListener
onItemViewSelectedListener = searchFragmentDelegate.onItemViewSelectedListener
}
// Add fragment
childFragmentManager.commit {
replace(R.id.content_view, searchFragment, arguments)
replace(binding.resultsFrame.id, rowsSupportFragment)
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.searchResultsFlow
.onEach { searchFragmentDelegate.showResults(it) }
.launchIn(lifecycleScope)
val query = arguments?.getString(SearchFragment.EXTRA_QUERY)
if (!query.isNullOrBlank()) {
binding.searchBar.setText(query)
viewModel.searchImmediately(query)
binding.resultsFrame.requestFocus()
} else {
binding.searchBar.requestFocus()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun EditText.onSubmit(onSubmit: (String) -> Unit) {
setOnEditorActionListener { view, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE,
EditorInfo.IME_ACTION_SEARCH,
EditorInfo.IME_ACTION_PREVIOUS -> {
onSubmit(text.toString())
// Manually close IME to workaround focus issue with Fire TV
context.getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(view.windowToken, 0)
// Focus on search results
binding.resultsFrame.requestFocus()
true
}
else -> false
}
}
}
private fun EditText.onTextChanged(onTextChanged: (String) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(
s: Editable,
) = onTextChanged(s.toString())
override fun beforeTextChanged(
s: CharSequence,
start: Int,
count: Int,
after: Int,
) = Unit
override fun onTextChanged(
s: CharSequence,
start: Int,
before: Int,
count: Int,
) = Unit
})
}
}

View File

@ -1,125 +0,0 @@
package org.jellyfin.androidtv.ui.search
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.leanback.app.RowsSupportFragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.jellyfin.androidtv.databinding.FragmentSearchTextBinding
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class TextSearchFragment : Fragment() {
private val viewModel: SearchViewModel by viewModel()
private var _binding: FragmentSearchTextBinding? = null
private val binding get() = _binding!!
private val searchFragmentDelegate: SearchFragmentDelegate by inject {
parametersOf(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSearchTextBinding.inflate(inflater, container, false)
binding.searchBar.apply {
onTextChanged { viewModel.searchDebounced(it) }
onSubmit { viewModel.searchImmediately(it) }
}
val rowsSupportFragment = RowsSupportFragment().apply {
adapter = searchFragmentDelegate.rowsAdapter
onItemViewClickedListener = searchFragmentDelegate.onItemViewClickedListener
onItemViewSelectedListener = searchFragmentDelegate.onItemViewSelectedListener
}
childFragmentManager.commit {
replace(binding.resultsFrame.id, rowsSupportFragment)
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.searchResultsFlow
.onEach { searchFragmentDelegate.showResults(it) }
.launchIn(lifecycleScope)
val query = arguments?.getString(SearchFragment.EXTRA_QUERY)
if (!query.isNullOrBlank()) {
binding.searchBar.setText(query)
viewModel.searchImmediately(query)
binding.resultsFrame.requestFocus()
} else {
binding.searchBar.requestFocus()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun EditText.onSubmit(onSubmit: (String) -> Unit) {
setOnEditorActionListener { view, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE,
EditorInfo.IME_ACTION_SEARCH,
EditorInfo.IME_ACTION_PREVIOUS -> {
onSubmit(text.toString())
// Manually close IME to workaround focus issue with Fire TV
context.getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(view.windowToken, 0)
// Focus on search results
binding.resultsFrame.requestFocus()
true
}
else -> false
}
}
}
private fun EditText.onTextChanged(onTextChanged: (String) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(
s: Editable,
) = onTextChanged(s.toString())
override fun beforeTextChanged(
s: CharSequence,
start: Int,
count: Int,
after: Int,
) = Unit
override fun onTextChanged(
s: CharSequence,
start: Int,
before: Int,
count: Int,
) = Unit
})
}
}