From cfcac7a6047839c1709ce930766f73cf692ac847 Mon Sep 17 00:00:00 2001 From: Paul Spiesberger Date: Tue, 6 Jan 2026 19:18:32 +0530 Subject: [PATCH 1/2] Add Search to Crew and Cast - Add new menu xml entry for people - Add search menu to Toolbar - Migrate people adapter from ArrayAdapter to BaseAdapter - Implement search and filter - Handle empty state --- .../seriesguide/people/PeopleActivity.kt | 28 +++++++ .../seriesguide/people/PeopleAdapter.kt | 84 +++++++++++-------- .../seriesguide/people/PeopleFragment.kt | 24 +++++- app/src/main/res/menu/people_menu.xml | 13 +++ 4 files changed, 112 insertions(+), 37 deletions(-) create mode 100644 app/src/main/res/menu/people_menu.xml diff --git a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleActivity.kt index e84aed5bbb..e06f0f0b0f 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleActivity.kt @@ -5,7 +5,9 @@ package com.battlelancer.seriesguide.people import android.content.Intent import android.os.Bundle +import android.view.Menu import android.view.ViewGroup +import androidx.appcompat.widget.SearchView import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.databinding.ActivityPeopleBinding import com.battlelancer.seriesguide.people.PeopleFragment.OnShowPersonListener @@ -105,6 +107,32 @@ class PeopleActivity : BaseActivity(), OnShowPersonListener { } } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.people_menu, menu) + + val peopleType = PeopleType.valueOf( + intent.getStringExtra(InitBundle.PEOPLE_TYPE)!! + ) + + val searchItem = menu.findItem(R.id.menu_search) + val searchView = searchItem.actionView as SearchView + searchView.queryHint = "${getString(R.string.search)} ${if (peopleType == PeopleType.CAST) getString(R.string.movie_cast) else getString(R.string.movie_crew)}" + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + val peopleFragment = + supportFragmentManager.findFragmentById(R.id.containerPeople) as PeopleFragment + peopleFragment.search(newText) + return true + } + }) + + return super.onCreateOptionsMenu(menu) + } + override fun showPerson(tmdbId: Int) { if (isTwoPane) { // show inline diff --git a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleAdapter.kt b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleAdapter.kt index 75c39a6206..d6d6576cf6 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleAdapter.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleAdapter.kt @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2014-2025 Uwe Trottmann +// Apache-2.0 +// Copyright © 2026 Uwe Trottmann package com.battlelancer.seriesguide.people @@ -8,31 +8,52 @@ import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ArrayAdapter +import android.widget.BaseAdapter import android.widget.ImageView import android.widget.TextView -import androidx.appcompat.content.res.AppCompatResources import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.tmdbapi.TmdbTools import com.battlelancer.seriesguide.util.CircleTransformation import com.battlelancer.seriesguide.util.ImageTools import com.squareup.picasso.Transformation -/** - * Shows a list of people in rows with headshots, name and description. - */ -internal class PeopleAdapter(context: Context) : ArrayAdapter(context, LAYOUT) { +interface PeopleAdapterHost { + fun searchResultIsEmpty(isEmpty: Boolean) +} + +class PeopleAdapter( + private val host: PeopleAdapterHost, + private val placeholderDrawable: Drawable? +) : BaseAdapter() { private val personImageTransform = CircleTransformation() - private val placeholderDrawable = - AppCompatResources.getDrawable(context, R.drawable.ic_account_circle_black_24dp)!! - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + private var originalData = emptyList() + private var data = emptyList() + + fun setData(data: List) { + originalData = data + this.data = originalData + notifyDataSetChanged() + } + + override fun getCount() = data.count() + + override fun getItem(position: Int) = data.getOrNull(position) + + override fun getItemId(position: Int) = position.toLong() + + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup? + ): View { val view: View val viewHolder: ViewHolder + val context = parent?.context ?: throw IllegalStateException("Context missing") + if (convertView == null) { - view = LayoutInflater.from(parent.context) - .inflate(LAYOUT, parent, false) + view = LayoutInflater.from(context).inflate(R.layout.item_person, parent, false) viewHolder = ViewHolder(view, placeholderDrawable) view.tag = viewHolder } else { @@ -45,17 +66,9 @@ internal class PeopleAdapter(context: Context) : ArrayAdapter(context, L return view } - /** - * Replace the data in this [android.widget.ArrayAdapter] with the given list. - */ - fun setData(data: List) { - clear() - addAll(data) - } - class ViewHolder( view: View, - private val placeholderDrawable: Drawable + private val placeholderDrawable: Drawable? ) { private val name: TextView = view.findViewById(R.id.textViewPerson) private val description: TextView = view.findViewById(R.id.textViewPersonDescription) @@ -67,26 +80,31 @@ internal class PeopleAdapter(context: Context) : ArrayAdapter(context, L description.text = person?.description ?: "" // load profile picture - if (person != null) { + if (person != null && placeholderDrawable != null) { ImageTools.loadWithPicasso( - context, - TmdbTools.buildProfileImageUrl( - context, person.profilePath, - TmdbTools.ProfileImageSize.W185 + context, TmdbTools.buildProfileImageUrl( + context, person.profilePath, TmdbTools.ProfileImageSize.W185 ) ) // Note: dimensions should match placeholder drawable, see notes in its file .resizeDimen(R.dimen.person_headshot_size, R.dimen.person_headshot_size) - .centerCrop() - .transform(personImageTransform) + .centerCrop().transform(personImageTransform) .placeholder(placeholderDrawable) - .error(placeholderDrawable) - .into(picture) + .error(placeholderDrawable).into(picture) } } } - companion object { - private val LAYOUT = R.layout.item_person + fun filter(query: String) { + data = originalData.filter { person -> + if (person.description != null) { + person.name.contains(query, ignoreCase = true) || + person.description.contains(query, ignoreCase = true) + } else { + person.name.contains(query, ignoreCase = true) + } + } + host.searchResultIsEmpty(data.isEmpty()) + notifyDataSetChanged() } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleFragment.kt index 424faba504..40d3b94e13 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleFragment.kt @@ -12,6 +12,7 @@ import android.view.animation.AnimationUtils import android.widget.AdapterView import android.widget.ListView import android.widget.ProgressBar +import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.battlelancer.seriesguide.R @@ -22,7 +23,7 @@ import com.uwetrottmann.androidutils.AndroidUtils /** * A fragment loading and showing a list of cast or crew members. */ -class PeopleFragment : Fragment() { +class PeopleFragment : Fragment(), PeopleAdapterHost { private lateinit var listView: ListView private lateinit var emptyView: EmptyView @@ -80,7 +81,6 @@ class PeopleFragment : Fragment() { ListView.CHOICE_MODE_NONE } - return rootView } @@ -103,7 +103,10 @@ class PeopleFragment : Fragment() { emptyView.setButtonClickListener { refresh() } - adapter = PeopleAdapter(requireContext()) + adapter = PeopleAdapter( + this, + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_account_circle_black_24dp) + ) listView.adapter = adapter model.credits.observe(viewLifecycleOwner) { @@ -126,7 +129,6 @@ class PeopleFragment : Fragment() { override fun onDetach() { super.onDetach() - onShowPersonListener = sDummyListener } @@ -183,6 +185,20 @@ class PeopleFragment : Fragment() { emptyView.setContentVisibility(View.VISIBLE) } + fun search(query: String) { + adapter.filter(query) + } + + override fun searchResultIsEmpty(isEmpty: Boolean) { + if (isEmpty) { + emptyView.setMessage(R.string.empty_no_results) + emptyView.setButtonGone(true) + } else { + emptyView.setButtonGone(false) + setEmptyMessage() + } + } + companion object { val liftOnScrollTargetViewId = R.id.listViewPeople diff --git a/app/src/main/res/menu/people_menu.xml b/app/src/main/res/menu/people_menu.xml new file mode 100644 index 0000000000..98c17656f4 --- /dev/null +++ b/app/src/main/res/menu/people_menu.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file From 9298b1f36180833186ab92f14fb3dba900fb9b52 Mon Sep 17 00:00:00 2001 From: Paul Spiesberger Date: Sun, 15 Feb 2026 00:35:48 +0100 Subject: [PATCH 2/2] Move Search to ViewModel - Reset PeopleAdapter - Move search to ViewModel - Introduce MenuProvider - Add new hint Strings for search --- .../seriesguide/people/PeopleActivity.kt | 62 ++++++++------ .../seriesguide/people/PeopleAdapter.kt | 84 ++++++++----------- .../seriesguide/people/PeopleFragment.kt | 30 +++---- .../seriesguide/people/PeopleViewModel.kt | 19 ++++- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 103 insertions(+), 96 deletions(-) diff --git a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleActivity.kt index e06f0f0b0f..370e3afd18 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleActivity.kt @@ -6,8 +6,12 @@ package com.battlelancer.seriesguide.people import android.content.Intent import android.os.Bundle import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.ViewGroup import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuProvider +import androidx.lifecycle.Lifecycle import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.databinding.ActivityPeopleBinding import com.battlelancer.seriesguide.people.PeopleFragment.OnShowPersonListener @@ -17,7 +21,7 @@ import com.battlelancer.seriesguide.util.commitReorderingAllowed /** * Displays a list of people, and on wide enough screens person details. - * Otherwise lets [PersonActivity] show details. + * Otherwise, lets [PersonActivity] show details. */ class PeopleActivity : BaseActivity(), OnShowPersonListener { @@ -51,6 +55,7 @@ class PeopleActivity : BaseActivity(), OnShowPersonListener { setContentView(binding.root) ThemeUtils.configureForEdgeToEdge(binding.root as ViewGroup) setupActionBar() + setupMenu() binding.sgAppBarLayout.sgAppBarLayout.liftOnScrollTargetViewId = PeopleFragment.liftOnScrollTargetViewId @@ -107,30 +112,43 @@ class PeopleActivity : BaseActivity(), OnShowPersonListener { } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.people_menu, menu) + private fun setupMenu() { + addMenuProvider(object : MenuProvider { - val peopleType = PeopleType.valueOf( - intent.getStringExtra(InitBundle.PEOPLE_TYPE)!! - ) + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.people_menu, menu) - val searchItem = menu.findItem(R.id.menu_search) - val searchView = searchItem.actionView as SearchView - searchView.queryHint = "${getString(R.string.search)} ${if (peopleType == PeopleType.CAST) getString(R.string.movie_cast) else getString(R.string.movie_crew)}" - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - return true - } + val peopleType = PeopleType.valueOf( + intent.getStringExtra(InitBundle.PEOPLE_TYPE)!! + ) + + val searchItem = menu.findItem(R.id.menu_search) + val searchView = searchItem.actionView as SearchView - override fun onQueryTextChange(newText: String): Boolean { - val peopleFragment = - supportFragmentManager.findFragmentById(R.id.containerPeople) as PeopleFragment - peopleFragment.search(newText) - return true + searchView.queryHint = if (peopleType == PeopleType.CAST) { + getString(R.string.people_search_cast_hint) + } else { + getString(R.string.people_search_crew_hint) + } + + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + val peopleFragment = + supportFragmentManager.findFragmentById(R.id.containerPeople) as PeopleFragment? + peopleFragment?.search(newText) + return true + } + }) } - }) - return super.onCreateOptionsMenu(menu) + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return false // BaseActivity (BaseThemeActivity) handles the home button + } + }, this, Lifecycle.State.RESUMED) } override fun showPerson(tmdbId: Int) { @@ -148,7 +166,3 @@ class PeopleActivity : BaseActivity(), OnShowPersonListener { } } } - -interface PeopleActivityInterface { - val isTwoPane: Boolean -} diff --git a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleAdapter.kt b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleAdapter.kt index d6d6576cf6..e9ea2ef361 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleAdapter.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleAdapter.kt @@ -1,5 +1,5 @@ -// Apache-2.0 -// Copyright © 2026 Uwe Trottmann +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2014-2026 Uwe Trottmann package com.battlelancer.seriesguide.people @@ -8,52 +8,31 @@ import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.BaseAdapter +import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.tmdbapi.TmdbTools import com.battlelancer.seriesguide.util.CircleTransformation import com.battlelancer.seriesguide.util.ImageTools import com.squareup.picasso.Transformation -interface PeopleAdapterHost { - fun searchResultIsEmpty(isEmpty: Boolean) -} - -class PeopleAdapter( - private val host: PeopleAdapterHost, - private val placeholderDrawable: Drawable? -) : BaseAdapter() { +/** + * Shows a list of people in rows with headshots, name and description. + */ +internal class PeopleAdapter(context: Context) : ArrayAdapter(context, LAYOUT) { private val personImageTransform = CircleTransformation() + private val placeholderDrawable = + AppCompatResources.getDrawable(context, R.drawable.ic_account_circle_black_24dp)!! - private var originalData = emptyList() - private var data = emptyList() - - fun setData(data: List) { - originalData = data - this.data = originalData - notifyDataSetChanged() - } - - override fun getCount() = data.count() - - override fun getItem(position: Int) = data.getOrNull(position) - - override fun getItemId(position: Int) = position.toLong() - - override fun getView( - position: Int, - convertView: View?, - parent: ViewGroup? - ): View { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view: View val viewHolder: ViewHolder - val context = parent?.context ?: throw IllegalStateException("Context missing") - if (convertView == null) { - view = LayoutInflater.from(context).inflate(R.layout.item_person, parent, false) + view = LayoutInflater.from(parent.context) + .inflate(LAYOUT, parent, false) viewHolder = ViewHolder(view, placeholderDrawable) view.tag = viewHolder } else { @@ -66,9 +45,17 @@ class PeopleAdapter( return view } + /** + * Replace the data in this [android.widget.ArrayAdapter] with the given list. + */ + fun setData(data: List) { + clear() + addAll(data) + } + class ViewHolder( view: View, - private val placeholderDrawable: Drawable? + private val placeholderDrawable: Drawable ) { private val name: TextView = view.findViewById(R.id.textViewPerson) private val description: TextView = view.findViewById(R.id.textViewPersonDescription) @@ -80,31 +67,26 @@ class PeopleAdapter( description.text = person?.description ?: "" // load profile picture - if (person != null && placeholderDrawable != null) { + if (person != null) { ImageTools.loadWithPicasso( - context, TmdbTools.buildProfileImageUrl( - context, person.profilePath, TmdbTools.ProfileImageSize.W185 + context, + TmdbTools.buildProfileImageUrl( + context, person.profilePath, + TmdbTools.ProfileImageSize.W185 ) ) // Note: dimensions should match placeholder drawable, see notes in its file .resizeDimen(R.dimen.person_headshot_size, R.dimen.person_headshot_size) - .centerCrop().transform(personImageTransform) + .centerCrop() + .transform(personImageTransform) .placeholder(placeholderDrawable) - .error(placeholderDrawable).into(picture) + .error(placeholderDrawable) + .into(picture) } } } - fun filter(query: String) { - data = originalData.filter { person -> - if (person.description != null) { - person.name.contains(query, ignoreCase = true) || - person.description.contains(query, ignoreCase = true) - } else { - person.name.contains(query, ignoreCase = true) - } - } - host.searchResultIsEmpty(data.isEmpty()) - notifyDataSetChanged() + companion object { + private val LAYOUT = R.layout.item_person } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleFragment.kt index c4ad98d5e4..9dd9a742ff 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleFragment.kt @@ -12,7 +12,6 @@ import android.view.animation.AnimationUtils import android.widget.AdapterView import android.widget.ListView import android.widget.ProgressBar -import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.battlelancer.seriesguide.R @@ -23,7 +22,7 @@ import com.uwetrottmann.androidutils.AndroidUtils /** * A fragment loading and showing a list of cast or crew members. */ -class PeopleFragment : Fragment(), PeopleAdapterHost { +class PeopleFragment : Fragment() { private lateinit var listView: ListView private lateinit var emptyView: EmptyView @@ -111,10 +110,7 @@ class PeopleFragment : Fragment(), PeopleAdapterHost { emptyView.setButtonClickListener { refresh() } - adapter = PeopleAdapter( - this, - AppCompatResources.getDrawable(requireContext(), R.drawable.ic_account_circle_black_24dp) - ) + adapter = PeopleAdapter(requireContext()) listView.adapter = adapter model.credits.observe(viewLifecycleOwner) { @@ -188,8 +184,13 @@ class PeopleFragment : Fragment(), PeopleAdapterHost { } private fun setEmptyMessage() { - // display error message if we are offline - if (!AndroidUtils.isNetworkConnected(requireContext())) { + + emptyView.setContentVisibility(View.VISIBLE) + if (model.isSearchOngoing()) { + emptyView.setMessage(R.string.empty_no_results) + emptyView.setButtonGone(true) + } else if (!AndroidUtils.isNetworkConnected(requireContext())) { + // display error message if we are offline emptyView.setMessage(R.string.offline) } else { emptyView.setMessage( @@ -199,21 +200,10 @@ class PeopleFragment : Fragment(), PeopleAdapterHost { ) ) } - emptyView.setContentVisibility(View.VISIBLE) } fun search(query: String) { - adapter.filter(query) - } - - override fun searchResultIsEmpty(isEmpty: Boolean) { - if (isEmpty) { - emptyView.setMessage(R.string.empty_no_results) - emptyView.setButtonGone(true) - } else { - emptyView.setButtonGone(false) - setEmptyMessage() - } + model.filter(query) } companion object { diff --git a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleViewModel.kt b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleViewModel.kt index 9af8fea3df..917e6f4a33 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/people/PeopleViewModel.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/people/PeopleViewModel.kt @@ -20,6 +20,7 @@ class PeopleViewModel( private val peopleType: PeopleActivity.PeopleType ) : AndroidViewModel(application) { + private var originalCredits = emptyList() val credits = MutableLiveData>() init { @@ -40,10 +41,26 @@ class PeopleViewModel( newCredits?.crew } - credits.postValue(castOrCrewOrNull ?: emptyList()) + originalCredits = castOrCrewOrNull ?: emptyList() + credits.postValue(originalCredits) } } + fun filter(query: String) { + viewModelScope.launch(Dispatchers.IO) { + val filterResult = originalCredits.filter { person -> + if (person.description != null) { + person.name.contains(query, ignoreCase = true) || + person.description.contains(query, ignoreCase = true) + } else { + person.name.contains(query, ignoreCase = true) + } + } + credits.postValue(filterResult) + } + } + + fun isSearchOngoing() = credits.value?.size != originalCredits.size } class PeopleViewModelFactory( diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3de0aee35f..f2dcb4cd2a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -549,6 +549,8 @@ Nach Webseiten und Bildern suchen Biografie + Suche nach Crew + Suche nach Besetzung Gefällt Ihnen SeriesGuide? Ja! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3fb7d0153..5c5d75356d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -597,6 +597,8 @@ Biography + Search for crew + Search for cast Are you enjoying SeriesGuide?