Adds collections to search and disambiguates results.

This commit is contained in:
2026-09-07 15:21:19 -07:00
parent 51b0245cfe
commit e2a1fafaa9
6 changed files with 190 additions and 13 deletions

View File

@@ -8,6 +8,7 @@ import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster; import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.CollectionWithCount; import com.majinnaibu.monstercards.models.CollectionWithCount;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.SearchResultItem;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -50,6 +51,43 @@ public class MonsterRepository {
.observeOn(AndroidSchedulers.mainThread()); .observeOn(AndroidSchedulers.mainThread());
} }
public Flowable<List<SearchResultItem>> searchAll(String searchText) {
Flowable<List<Monster>> monstersFlowable = m_db.monsterDAO()
.getAll()
.map(monsters -> {
ArrayList<Monster> filteredMonsters = new ArrayList<>();
for (Monster monster : monsters) {
if (Helpers.monsterMatchesSearch(monster, searchText)) {
filteredMonsters.add(monster);
}
}
return (List<Monster>) filteredMonsters;
});
Flowable<List<Collection>> collectionsFlowable = m_db.collectionDAO()
.getAll()
.map(collections -> {
ArrayList<Collection> filteredCollections = new ArrayList<>();
for (Collection collection : collections) {
if (StringHelper.isNullOrEmpty(searchText) || StringHelper.containsCaseInsensitive(collection.name, searchText)) {
filteredCollections.add(collection);
}
}
return (List<Collection>) filteredCollections;
});
return Flowable.combineLatest(monstersFlowable, collectionsFlowable, (monsters, collections) -> {
ArrayList<SearchResultItem> results = new ArrayList<>();
for (Monster monster : monsters) {
results.add(new SearchResultItem(monster));
}
for (Collection collection : collections) {
results.add(new SearchResultItem(collection));
}
return (List<SearchResultItem>) results;
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
public Flowable<Monster> getMonster(@NonNull UUID monsterId) { public Flowable<Monster> getMonster(@NonNull UUID monsterId) {
return m_db.monsterDAO() return m_db.monsterDAO()
.loadAllByIds(new String[]{monsterId.toString()}) .loadAllByIds(new String[]{monsterId.toString()})

View File

@@ -0,0 +1,32 @@
package com.majinnaibu.monstercards.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SearchResultItem {
public enum Type {
MONSTER,
COLLECTION
}
@NonNull
public final Type type;
@Nullable
public final Monster monster;
@Nullable
public final Collection collection;
public SearchResultItem(@NonNull Monster monster) {
this.type = Type.MONSTER;
this.monster = monster;
this.collection = null;
}
public SearchResultItem(@NonNull Collection collection) {
this.type = Type.COLLECTION;
this.monster = null;
this.collection = collection;
}
}

View File

@@ -9,11 +9,14 @@ import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.SearchResultItem;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
public class SearchFragment extends MCFragment { public class SearchFragment extends MCFragment {
@@ -22,7 +25,17 @@ public class SearchFragment extends MCFragment {
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_search, container, false); View root = inflater.inflate(R.layout.fragment_search, container, false);
MonsterRepository repository = this.getMonsterRepository(); MonsterRepository repository = this.getMonsterRepository();
SearchResultsRecyclerViewAdapter adapter = new SearchResultsRecyclerViewAdapter(repository, null); SearchResultsRecyclerViewAdapter adapter = new SearchResultsRecyclerViewAdapter(repository, item -> {
if (item != null) {
if (item.type == SearchResultItem.Type.MONSTER && item.monster != null) {
NavDirections action = SearchFragmentDirections.actionNavigationSearchToNavigationMonster(item.monster.id.toString());
Navigation.findNavController(requireView()).navigate(action);
} else if (item.type == SearchResultItem.Type.COLLECTION && item.collection != null) {
NavDirections action = SearchFragmentDirections.actionNavigationSearchToCollectionDetailFragment(item.collection.id.toString());
Navigation.findNavController(requireView()).navigate(action);
}
}
});
final RecyclerView recyclerView = root.findViewById(R.id.monster_list); final RecyclerView recyclerView = root.findViewById(R.id.monster_list);
assert recyclerView != null; assert recyclerView != null;
setupRecyclerView(recyclerView, adapter); setupRecyclerView(recyclerView, adapter);

View File

@@ -3,6 +3,7 @@ package com.majinnaibu.monstercards.ui.search;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -10,7 +11,10 @@ import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.SearchResultItem;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
@@ -23,7 +27,7 @@ public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<Searc
private final MonsterRepository mRepository; private final MonsterRepository mRepository;
private final ItemCallback mOnClickHandler; private final ItemCallback mOnClickHandler;
private String mSearchText; private String mSearchText;
private List<Monster> mValues; private List<SearchResultItem> mValues;
private Disposable mSubscriptionHandler; private Disposable mSubscriptionHandler;
public SearchResultsRecyclerViewAdapter(MonsterRepository repository, public SearchResultsRecyclerViewAdapter(MonsterRepository repository,
@@ -42,9 +46,9 @@ public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<Searc
mSubscriptionHandler.dispose(); mSubscriptionHandler.dispose();
} }
mSearchText = searchText; mSearchText = searchText;
Flowable<List<Monster>> foundMonsters = mRepository.searchMonsters(mSearchText); Flowable<List<SearchResultItem>> resultsFlowable = mRepository.searchAll(mSearchText);
mSubscriptionHandler = foundMonsters.subscribe(monsters -> { mSubscriptionHandler = resultsFlowable.subscribe(results -> {
mValues = monsters; mValues = results;
notifyDataSetChanged(); notifyDataSetChanged();
}, },
throwable -> Logger.logError("Error performing search", throwable)); throwable -> Logger.logError("Error performing search", throwable));
@@ -54,18 +58,52 @@ public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<Searc
@Override @Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.monster_list_content, parent, false); .inflate(R.layout.search_result_list_item, parent, false);
return new ViewHolder(view); return new ViewHolder(view);
} }
@Override @Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
Monster monster = mValues.get(position); SearchResultItem item = mValues.get(position);
holder.mContentView.setText(monster.name); if (item.type == SearchResultItem.Type.MONSTER && item.monster != null) {
holder.itemView.setTag(monster); Monster monster = item.monster;
holder.mTitleView.setText(monster.name);
StringBuilder sb = new StringBuilder();
if (!StringHelper.isNullOrEmpty(monster.alignment)) {
sb.append(monster.alignment);
}
String cr = monster.getChallengeRatingDescription();
if (!StringHelper.isNullOrEmpty(cr)) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append("CR ").append(cr);
}
if (sb.length() > 0) {
holder.mSubtitleView.setText(sb.toString());
holder.mSubtitleView.setVisibility(View.VISIBLE);
} else {
holder.mSubtitleView.setVisibility(View.GONE);
}
holder.mIconView.setVisibility(View.GONE);
} else if (item.type == SearchResultItem.Type.COLLECTION && item.collection != null) {
Collection collection = item.collection;
holder.mTitleView.setText(collection.name);
if (!StringHelper.isNullOrEmpty(collection.description)) {
holder.mSubtitleView.setText(collection.description);
holder.mSubtitleView.setVisibility(View.VISIBLE);
} else {
holder.mSubtitleView.setVisibility(View.GONE);
}
holder.mIconView.setVisibility(View.VISIBLE);
}
holder.itemView.setTag(item);
holder.itemView.setOnClickListener(view -> { holder.itemView.setOnClickListener(view -> {
if (mOnClickHandler != null) { if (mOnClickHandler != null) {
mOnClickHandler.onItem(monster); mOnClickHandler.onItem(item);
} }
}); });
} }
@@ -76,15 +114,19 @@ public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<Searc
} }
public interface ItemCallback { public interface ItemCallback {
void onItem(Monster monster); void onItem(SearchResultItem item);
} }
public static class ViewHolder extends RecyclerView.ViewHolder { public static class ViewHolder extends RecyclerView.ViewHolder {
final TextView mContentView; final TextView mTitleView;
final TextView mSubtitleView;
final ImageView mIconView;
ViewHolder(View view) { ViewHolder(View view) {
super(view); super(view);
mContentView = view.findViewById(R.id.content); mTitleView = view.findViewById(R.id.title);
mSubtitleView = view.findViewById(R.id.subtitle);
mIconView = view.findViewById(R.id.icon);
} }
} }
} }

View File

@@ -0,0 +1,49 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp"
android:background="?attr/selectableItemBackground">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceListItem"
android:textSize="18sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end"
android:visibility="gone" />
</LinearLayout>
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:contentDescription="@string/title_collections"
android:src="@drawable/ic_collections_black_24dp"
app:tint="?android:attr/textColorSecondary"
android:visibility="gone" />
</LinearLayout>

View File

@@ -13,6 +13,9 @@
<action <action
android:id="@+id/action_navigation_search_to_navigation_monster" android:id="@+id/action_navigation_search_to_navigation_monster"
app:destination="@id/navigation_monster" /> app:destination="@id/navigation_monster" />
<action
android:id="@+id/action_navigation_search_to_collectionDetailFragment"
app:destination="@id/collectionDetailFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/navigation_dashboard" android:id="@+id/navigation_dashboard"