diff --git a/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json b/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json new file mode 100644 index 0000000..fada53b --- /dev/null +++ b/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json @@ -0,0 +1,499 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "6f1e7a2b2ab96fc4be4da1657a7a0138", + "entities": [ + { + "tableName": "monsters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `blindsight_range` INTEGER NOT NULL DEFAULT 0, `is_blind_beyond_blindsight_range` INTEGER NOT NULL DEFAULT false, `darkvision_range` INTEGER NOT NULL DEFAULT 0, `tremorsense_range` INTEGER NOT NULL DEFAULT 0, `truesight_range` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `skills` TEXT, `damage_immunities` TEXT, `damage_resistances` TEXT, `damage_vulnerabilities` TEXT, `condition_immunities` TEXT, `languages` TEXT, `abilities` TEXT, `actions` TEXT, `reactions` TEXT, `lair_actions` TEXT, `legendary_actions` TEXT, `regional_actions` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "strengthScore", + "columnName": "strength_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "strengthSavingThrowAdvantage", + "columnName": "strength_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "strengthSavingThrowProficiency", + "columnName": "strength_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "dexterityScore", + "columnName": "dexterity_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "dexteritySavingThrowAdvantage", + "columnName": "dexterity_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "dexteritySavingThrowProficiency", + "columnName": "dexterity_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionScore", + "columnName": "constitution_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "constitutionSavingThrowAdvantage", + "columnName": "constitution_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionSavingThrowProficiency", + "columnName": "constitution_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceScore", + "columnName": "intelligence_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "intelligenceSavingThrowAdvantage", + "columnName": "intelligence_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceSavingThrowProficiency", + "columnName": "intelligence_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomScore", + "columnName": "wisdom_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "wisdomSavingThrowAdvantage", + "columnName": "wisdom_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomSavingThrowProficiency", + "columnName": "wisdom_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaScore", + "columnName": "charisma_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "charismaSavingThrowAdvantage", + "columnName": "charisma_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaSavingThrowProficiency", + "columnName": "charisma_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "armorType", + "columnName": "armor_type", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "shieldBonus", + "columnName": "shield_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "naturalArmorBonus", + "columnName": "natural_armor_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "otherArmorDescription", + "columnName": "other_armor_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "hitDice", + "columnName": "hit_dice", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "hasCustomHP", + "columnName": "has_custom_hit_points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customHPDescription", + "columnName": "custom_hit_points_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "walkSpeed", + "columnName": "walk_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "burrowSpeed", + "columnName": "burrow_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "climbSpeed", + "columnName": "climb_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "flySpeed", + "columnName": "fly_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "canHover", + "columnName": "can_hover", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "swimSpeed", + "columnName": "swim_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasCustomSpeed", + "columnName": "has_custom_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "customSpeedDescription", + "columnName": "custom_speed_description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "challengeRating", + "columnName": "challenge_rating", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'1'" + }, + { + "fieldPath": "customChallengeRatingDescription", + "columnName": "custom_challenge_rating_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "customProficiencyBonus", + "columnName": "custom_proficiency_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blindsightRange", + "columnName": "blindsight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isBlindBeyondBlindsightRange", + "columnName": "is_blind_beyond_blindsight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "darkvisionRange", + "columnName": "darkvision_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "tremorsenseRange", + "columnName": "tremorsense_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "truesightRange", + "columnName": "truesight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "telepathyRange", + "columnName": "telepathy_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "understandsButDescription", + "columnName": "understands_but_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "skills", + "columnName": "skills", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageImmunities", + "columnName": "damage_immunities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageResistances", + "columnName": "damage_resistances", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageVulnerabilities", + "columnName": "damage_vulnerabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conditionImmunities", + "columnName": "condition_immunities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "languages", + "columnName": "languages", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "abilities", + "columnName": "abilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lairActions", + "columnName": "lair_actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "legendaryActions", + "columnName": "legendary_actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "regionalActions", + "columnName": "regional_actions", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "monsters", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END" + ], + "tableName": "monsters_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6f1e7a2b2ab96fc4be4da1657a7a0138')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java b/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java index 3d21f3f..1d50723 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java +++ b/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java @@ -14,9 +14,10 @@ import com.majinnaibu.monstercards.data.converters.SetOfStringConverter; import com.majinnaibu.monstercards.data.converters.SetOfTraitConverter; import com.majinnaibu.monstercards.data.converters.UUIDConverter; import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.models.MonsterFTS; @SuppressWarnings("unused") -@Database(entities = {Monster.class}, version = 1) +@Database(entities = {Monster.class, MonsterFTS.class}, version = 2) @TypeConverters({ ArmorTypeConverter.class, ChallengeRatingConverter.class, diff --git a/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java b/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java index c8da1fb..eea208e 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java +++ b/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java @@ -4,7 +4,10 @@ import android.app.Application; import android.content.Context; import android.content.res.Configuration; +import androidx.annotation.NonNull; import androidx.room.Room; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; import com.facebook.flipper.android.AndroidFlipperClient; import com.facebook.flipper.android.utils.FlipperUtils; @@ -50,6 +53,7 @@ public class MonsterCardsApplication extends Application { } m_db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "monsters") + .addMigrations(MIGRATION_1_2) .fallbackToDestructiveMigrationOnDowngrade() .build(); m_monsterLibraryRepository = new MonsterRepository(m_db); @@ -69,4 +73,17 @@ public class MonsterCardsApplication extends Application { public void onLowMemory() { super.onLowMemory(); } + + private static final Migration MIGRATION_1_2 = new Migration(1, 2) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // rename table monster to monsters + database.execSQL("ALTER TABLE monster RENAME TO monsters"); + // create the fts view + database.execSQL("CREATE VIRTUAL TABLE IF NOT EXISTS `monsters_fts` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)"); + // build the initial full text search index + database.execSQL("INSERT INTO monsters_fts(monsters_fts) VALUES('rebuild')"); + + } + }; } diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java b/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java index 5295e92..a622683 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java +++ b/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java @@ -15,15 +15,18 @@ import io.reactivex.rxjava3.core.Flowable; @Dao public interface MonsterDAO { - @Query("SELECT * FROM monster") + @Query("SELECT * FROM monsters") Flowable> getAll(); - @Query("SELECT * FROM monster WHERE id IN (:monsterIds)") + @Query("SELECT * FROM monsters WHERE id IN (:monsterIds)") Flowable> loadAllByIds(String[] monsterIds); - @Query("SELECT * FROM monster WHERE name LIKE :name LIMIT 1") + @Query("SELECT * FROM monsters WHERE name LIKE :name LIMIT 1") Flowable findByName(String name); + @Query("SELECT monsters.* FROM monsters JOIN monsters_fts ON monsters.oid = monsters_fts.docid WHERE monsters_fts MATCH :searchText") + Flowable> search(String searchText); + @Insert Completable insertAll(Monster... monsters); diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java b/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java index 83a1660..56383bf 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java +++ b/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java @@ -29,6 +29,13 @@ public class MonsterRepository { .observeOn(AndroidSchedulers.mainThread()); } + public Flowable> searchMonsters(String searchText) { + return m_db.monsterDAO() + .search(searchText) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + public Flowable getMonster(UUID monsterId) { return m_db.monsterDAO() .loadAllByIds(new String[]{monsterId.toString()}) diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java b/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java index 7333266..101ffac 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java +++ b/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Set; import java.util.UUID; -@Entity +@Entity(tableName = "monsters") @SuppressLint("DefaultLocale") @SuppressWarnings("unused") public class Monster { diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java b/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java new file mode 100644 index 0000000..7d6c110 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java @@ -0,0 +1,14 @@ +package com.majinnaibu.monstercards.models; + +import androidx.room.Entity; +import androidx.room.Fts4; + +@Entity(tableName = "monsters_fts") +@Fts4(contentEntity = Monster.class) +public class MonsterFTS { + public String name; + public String size; + public String type; + public String subtype; + public String alignment; +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchFragment.java index 69e2fd3..b25276c 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchFragment.java +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchFragment.java @@ -1,18 +1,19 @@ package com.majinnaibu.monstercards.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.widget.Button; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.ui.MCFragment; public class SearchFragment extends MCFragment { @@ -21,22 +22,34 @@ public class SearchFragment extends MCFragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - searchViewModel = new ViewModelProvider(this).get(SearchViewModel.class); View root = inflater.inflate(R.layout.fragment_search, container, false); + MonsterRepository repository = this.getMonsterRepository(); + SearchResultsRecyclerViewAdapter adapter = new SearchResultsRecyclerViewAdapter(repository, null); + final RecyclerView recyclerView = root.findViewById(R.id.monster_list); + assert recyclerView != null; + setupRecyclerView(recyclerView, adapter); + final TextView textView = root.findViewById(R.id.search_query); - searchViewModel.getSearchQuery().observe(getViewLifecycleOwner(), new Observer() { + textView.addTextChangedListener(new TextWatcher() { @Override - public void onChanged(@Nullable String s) { - textView.setText(s); + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void afterTextChanged(Editable editable) { + adapter.doSearch(textView.getText().toString()); } }); - final Button btnSearch = root.findViewById(R.id.button_search); - btnSearch.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - } - }); return root; } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView, @NonNull SearchResultsRecyclerViewAdapter adapter) { + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + } } diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchResultsRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchResultsRecyclerViewAdapter.java new file mode 100644 index 0000000..c437486 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchResultsRecyclerViewAdapter.java @@ -0,0 +1,92 @@ +package com.majinnaibu.monstercards.ui.search; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.Disposable; + +public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter { + public interface ItemCallback { + void onItem(Monster monster); + } + + private final MonsterRepository mRepository; + private String mSearchText; + private List mValues; + private Disposable mSubscriptionHandler; + private final ItemCallback mOnClickHandler; + + public SearchResultsRecyclerViewAdapter(MonsterRepository repository, + ItemCallback onClick) { + mRepository = repository; + mSearchText = ""; + mValues = new ArrayList<>(); + mOnClickHandler = onClick; + mSubscriptionHandler = null; + + doSearch(mSearchText); + } + + public void doSearch(String searchText) { + if (mSubscriptionHandler != null && !mSubscriptionHandler.isDisposed()) { + mSubscriptionHandler.dispose(); + } + mSearchText = searchText; + Flowable> foundMonsters = mRepository.searchMonsters(mSearchText); + mSubscriptionHandler = foundMonsters.subscribe(monsters -> { + mValues = monsters; + notifyDataSetChanged(); + }, + throwable -> Logger.logError("Error performing search", throwable)); + } + + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.monster_list_content, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + Monster monster = mValues.get(position); + holder.mIdView.setText(monster.id.toString().substring(0, 6)); + holder.mContentView.setText(monster.name); + holder.itemView.setTag(monster); + holder.itemView.setOnClickListener(view -> { + if (mOnClickHandler != null) { + mOnClickHandler.onItem(monster); + } + }); + } + + @Override + public int getItemCount() { + return mValues.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + final TextView mIdView; + final TextView mContentView; + + ViewHolder(View view) { + super(view); + mIdView = view.findViewById(R.id.id_text); + mContentView = view.findViewById(R.id.content); + } + } +} diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index c757e53..3afd8c1 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -7,15 +7,6 @@ tools:context=".ui.search.SearchFragment" tools:targetApi="o"> -