From 0cbd03da69d3e7379a4b2d77b39d7b0e66e2c4e7 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Sun, 20 Sep 2026 16:29:13 -0700 Subject: [PATCH] Adds removing a monster from the dashboard via long press. --- .../majinnaibu/monstercards/MainActivity.java | 13 +- .../monstercards/data/DashboardDAO.java | 11 +- .../monstercards/data/MonsterRepository.java | 7 + .../models/DashboardMonsterWithMonster.java | 23 ++++ .../collections/CollectionDetailFragment.java | 31 +++-- .../ui/collections/CollectionsFragment.java | 9 +- .../ui/dashboard/DashboardFragment.java | 124 +++++++++++++++--- .../DashboardRecyclerViewAdapter.java | 46 +++++-- .../ui/dashboard/DashboardViewModel.java | 8 +- .../ui/editmonster/EditMonsterFragment.java | 5 +- .../ui/library/LibraryFragment.java | 4 +- .../ui/monster/MonsterDetailFragment.java | 14 +- .../ui/monster/MonsterImportFragment.java | 4 +- .../monstercards/ui/shared/MCFragment.java | 8 +- .../monstercards/utils/SnackbarHelper.java | 53 ++++++++ .../monstercards/utils/ToastHelper.java | 42 ++++++ .../app/src/main/res/menu/dashboard_menu.xml | 5 + Android/app/src/main/res/values/strings.xml | 10 ++ 18 files changed, 336 insertions(+), 81 deletions(-) create mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/models/DashboardMonsterWithMonster.java create mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/utils/SnackbarHelper.java create mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/utils/ToastHelper.java diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java index 96729d0..7cd00ee 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java @@ -21,8 +21,6 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; -import android.widget.Toast; - import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.gson.Gson; import com.majinnaibu.monstercards.helpers.MonsterImportHelper; @@ -31,6 +29,7 @@ import com.majinnaibu.monstercards.importers.BinderImporter; import com.majinnaibu.monstercards.importers.DnDBeyondImporter; import com.majinnaibu.monstercards.init.AppCenterInitializer; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.ToastHelper; import java.io.BufferedReader; import java.io.IOException; @@ -141,24 +140,24 @@ public class MainActivity extends AppCompatActivity { public void importMonsterFromInputAndNavigate(@NonNull String input) { BinderImporter binderImporter = new BinderImporter(); if (binderImporter.canImport(input)) { - Toast.makeText(this, R.string.toast_importing_url, Toast.LENGTH_SHORT).show(); + ToastHelper.showShort(this, R.string.toast_importing_url); Single.fromCallable(() -> binderImporter.parse(input)) .flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(() -> { - Toast.makeText(this, R.string.snackbar_import_binder_success, Toast.LENGTH_LONG).show(); + ToastHelper.showLong(this, R.string.snackbar_import_binder_success); NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment)); NavController navController = navHostFragment.getNavController(); navController.navigate(R.id.navigation_library); }, throwable -> { Logger.logError("Failed to import binder from input", throwable); - Toast.makeText(this, R.string.failed_to_import_url, Toast.LENGTH_LONG).show(); + ToastHelper.showLong(this, R.string.failed_to_import_url); }); return; } - Toast.makeText(this, R.string.toast_importing_url, Toast.LENGTH_SHORT).show(); + ToastHelper.showShort(this, R.string.toast_importing_url); Single.fromCallable(() -> MonsterImportHelper.fromJSON(input)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -170,7 +169,7 @@ public class MainActivity extends AppCompatActivity { navController.navigate(navAction); }, throwable -> { Logger.logError("Failed to import monster from input", throwable); - Toast.makeText(this, R.string.failed_to_import_url, Toast.LENGTH_LONG).show(); + ToastHelper.showLong(this, R.string.failed_to_import_url); }); } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/DashboardDAO.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/DashboardDAO.java index ce4fc6a..64126f5 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/DashboardDAO.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/DashboardDAO.java @@ -3,9 +3,11 @@ package com.majinnaibu.monstercards.data; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.Query; +import androidx.room.Transaction; import androidx.room.Update; import com.majinnaibu.monstercards.models.DashboardMonster; +import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster; import com.majinnaibu.monstercards.models.Monster; import java.util.List; @@ -19,6 +21,10 @@ public interface DashboardDAO { @Query("SELECT monsters.* FROM monsters INNER JOIN dashboard_monsters ON monsters.id = dashboard_monsters.monster_id ORDER BY dashboard_monsters.ordinal ASC, dashboard_monsters.id ASC") Flowable> getDashboardMonsters(); + @Transaction + @Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC") + Flowable> getDashboardMonstersWithMonster(); + @Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC") Flowable> getDashboardMonsterEntries(); @@ -28,9 +34,12 @@ public interface DashboardDAO { @Query("DELETE FROM dashboard_monsters WHERE id = :id") Completable removeDashboardMonsterById(long id); - @Query("DELETE FROM dashboard_monsters WHERE monster_id = :monsterId") + @Query("DELETE FROM dashboard_monsters WHERE id = (SELECT id FROM dashboard_monsters WHERE monster_id = :monsterId LIMIT 1)") Completable removeMonsterFromDashboard(String monsterId); + @Query("DELETE FROM dashboard_monsters WHERE monster_id = :monsterId") + Completable removeAllMonstersFromDashboard(String monsterId); + @Query("DELETE FROM dashboard_monsters") Completable clearDashboard(); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java index ced6e62..5acb685 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java @@ -208,6 +208,13 @@ public class MonsterRepository { .observeOn(AndroidSchedulers.mainThread()); } + public Flowable> getDashboardMonstersWithMonster() { + return m_db.dashboardDAO() + .getDashboardMonstersWithMonster() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + public Flowable> getDashboardMonsterEntries() { return m_db.dashboardDAO() .getDashboardMonsterEntries() diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/DashboardMonsterWithMonster.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/DashboardMonsterWithMonster.java new file mode 100644 index 0000000..86a5a72 --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/DashboardMonsterWithMonster.java @@ -0,0 +1,23 @@ +package com.majinnaibu.monstercards.models; + +import androidx.room.Embedded; +import androidx.room.Relation; + +public class DashboardMonsterWithMonster { + @Embedded + public DashboardMonster dashboardEntry; + + @Relation( + parentColumn = "monster_id", + entityColumn = "id" + ) + public Monster monster; + + public DashboardMonsterWithMonster() { + } + + public DashboardMonsterWithMonster(DashboardMonster dashboardEntry, Monster monster) { + this.dashboardEntry = dashboardEntry; + this.monster = monster; + } +} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java index 2e1e853..e3072df 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java @@ -21,15 +21,18 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.models.Collection; +import com.majinnaibu.monstercards.models.DashboardMonster; +import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.ui.dashboard.DashboardRecyclerViewAdapter; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.SnackbarHelper; +import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -80,7 +83,15 @@ public class CollectionDetailFragment extends MCFragment { mViewModel.getMonsters().observe(getViewLifecycleOwner(), monsters -> { if (mAdapter != null) { - mAdapter.submitList(monsters); + List items = new ArrayList<>(); + if (monsters != null) { + for (int i = 0; i < monsters.size(); i++) { + DashboardMonster dm = new DashboardMonster(monsters.get(i).id, i); + dm.id = i + 1; + items.add(new DashboardMonsterWithMonster(dm, monsters.get(i))); + } + } + mAdapter.submitList(items); } }); @@ -113,9 +124,9 @@ public class CollectionDetailFragment extends MCFragment { GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount); recyclerView.setLayoutManager(layoutManager); - mAdapter = new DashboardRecyclerViewAdapter(monster -> { - if (monster != null) { - navigateToMonsterDetail(monster.id); + mAdapter = new DashboardRecyclerViewAdapter(item -> { + if (item != null && item.monster != null) { + navigateToMonsterDetail(item.monster.id); } else { Logger.logError("Can't navigate to MonsterDetailFragment with a null monster"); } @@ -133,7 +144,7 @@ public class CollectionDetailFragment extends MCFragment { if (monsters.isEmpty()) { View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster)); } return; } @@ -162,11 +173,7 @@ public class CollectionDetailFragment extends MCFragment { if (view != null) { Collection collection = mViewModel.getCollection().getValue(); String collectionName = collection != null ? collection.name : ""; - Snackbar.make( - view, - getString(R.string.snackbar_monster_added_to_collection, monster.name, collectionName), - Snackbar.LENGTH_LONG) - .show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_monster_added_to_collection, monster.name, collectionName)); } }, Logger::logError)); } @@ -219,7 +226,7 @@ public class CollectionDetailFragment extends MCFragment { if (view != null) { Collection collection = mViewModel.getCollection().getValue(); String collectionName = collection != null ? collection.name : ""; - Snackbar.make(view, getString(R.string.snackbar_collection_added_to_dashboard, collectionName), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_collection_added_to_dashboard, collectionName)); } }, Logger::logError)); } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java index da17887..7560d50 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java @@ -20,13 +20,13 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.SnackbarHelper; import java.util.UUID; @@ -150,10 +150,7 @@ public class CollectionsFragment extends MCFragment { public void onComplete() { View view = getView(); if (view != null) { - Snackbar.make( - view, - getString(R.string.snackbar_collection_created, collection.name), - Snackbar.LENGTH_LONG) + SnackbarHelper.makeLong(view, getString(R.string.snackbar_collection_created, collection.name)) .setAction("View", v -> navigateToCollectionDetail(collection.id)) .show(); } @@ -164,7 +161,7 @@ public class CollectionsFragment extends MCFragment { Logger.logError("Error creating collection", e); View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.snackbar_failed_to_create_collection), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_collection)); } } }); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java index 6a1230a..4b86916 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java @@ -21,13 +21,13 @@ import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.SnackbarHelper; import java.util.ArrayList; import java.util.List; @@ -69,7 +69,7 @@ public class DashboardFragment extends MCFragment { private void loadDashboardMonsters() { MonsterRepository repository = getMonsterRepository(); - mDisposables.add(repository.getDashboardMonsters() + mDisposables.add(repository.getDashboardMonstersWithMonster() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError)); @@ -82,14 +82,21 @@ public class DashboardFragment extends MCFragment { GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount); recyclerView.setLayoutManager(layoutManager); - LiveData> monsterData = mViewModel.getMonsters(); - mAdapter = new DashboardRecyclerViewAdapter(monster -> { - if (monster != null) { - navigateToMonsterDetail(monster); - } else { - Logger.logError("Can't navigate to MonsterDetailFragment with a null monster"); - } - }); + LiveData> monsterData = mViewModel.getMonsters(); + mAdapter = new DashboardRecyclerViewAdapter( + item -> { + if (item != null && item.monster != null) { + navigateToMonsterDetail(item.monster); + } else { + Logger.logError("Can't navigate to MonsterDetailFragment with a null monster"); + } + }, + item -> { + if (item != null) { + showCardOptionsMenu(item); + } + } + ); if (monsterData != null) { monsterData.observe(getViewLifecycleOwner(), monsters -> { mAdapter.submitList(monsters); @@ -113,9 +120,9 @@ public class DashboardFragment extends MCFragment { int fromPos = viewHolder.getAdapterPosition(); int toPos = target.getAdapterPosition(); if (fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION) { - List currentList = new ArrayList<>(mAdapter.getCurrentList()); + List currentList = new ArrayList<>(mAdapter.getCurrentList()); if (fromPos < currentList.size() && toPos < currentList.size()) { - Monster moved = currentList.remove(fromPos); + com.majinnaibu.monstercards.models.DashboardMonsterWithMonster moved = currentList.remove(fromPos); currentList.add(toPos, moved); mAdapter.submitList(currentList); } @@ -127,10 +134,10 @@ public class DashboardFragment extends MCFragment { public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { int position = viewHolder.getAdapterPosition(); if (position != RecyclerView.NO_POSITION) { - List currentList = mAdapter.getCurrentList(); + List currentList = mAdapter.getCurrentList(); if (position < currentList.size()) { - Monster monster = currentList.get(position); - removeMonsterFromDashboard(monster); + com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item = currentList.get(position); + removeDashboardMonsterEntry(item); } } } @@ -142,6 +149,7 @@ public class DashboardFragment extends MCFragment { String[] options = new String[]{ getString(R.string.action_add_single_monster), getString(R.string.action_add_collection_option), + getString(R.string.action_remove_single_monster), getString(R.string.action_clear_dashboard) }; new AlertDialog.Builder(requireContext()) @@ -152,6 +160,8 @@ public class DashboardFragment extends MCFragment { } else if (which == 1) { showAddCollectionPicker(); } else if (which == 2) { + showRemoveMonsterPicker(); + } else if (which == 3) { clearDashboard(); } }) @@ -159,6 +169,68 @@ public class DashboardFragment extends MCFragment { .show(); } + private void showRemoveMonsterPicker() { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.getDashboardMonsters() + .firstOrError() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(monsters -> { + if (monsters.isEmpty()) { + View view = getView(); + if (view != null) { + SnackbarHelper.showLong(view, getString(R.string.snackbar_dashboard_already_empty)); + } + return; + } + String[] names = new String[monsters.size()]; + for (int i = 0; i < monsters.size(); i++) { + names[i] = monsters.get(i).name; + } + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.action_remove_single_monster) + .setItems(names, (dialog, which) -> { + Monster selected = monsters.get(which); + removeMonsterFromDashboard(selected); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + }, Logger::logError)); + } + + private void showCardOptionsMenu(@NonNull com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item) { + String[] options = new String[]{ + getString(R.string.action_view_details), + getString(R.string.action_remove_from_dashboard) + }; + new AlertDialog.Builder(requireContext()) + .setTitle(item.monster.name) + .setItems(options, (dialog, which) -> { + if (which == 0) { + navigateToMonsterDetail(item.monster); + } else if (which == 1) { + removeDashboardMonsterEntry(item); + } + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + } + + private void removeDashboardMonsterEntry(@NonNull com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item) { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.removeDashboardMonsterById(item.dashboardEntry.id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + SnackbarHelper.makeLong(view, getString(R.string.snackbar_removed_from_dashboard, item.monster.name)) + .setAction(R.string.action_undo, v -> addMonsterToDashboard(item.monster)) + .show(); + } + }, Logger::logError)); + } + private void showAddMonsterPicker() { MonsterRepository repository = getMonsterRepository(); mDisposables.add(repository.getMonsters() @@ -169,7 +241,7 @@ public class DashboardFragment extends MCFragment { if (monsters.isEmpty()) { View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.no_monsters_available), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.no_monsters_available)); } return; } @@ -198,7 +270,7 @@ public class DashboardFragment extends MCFragment { if (collections.isEmpty()) { View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.no_collections_available), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.no_collections_available)); } return; } @@ -225,7 +297,7 @@ public class DashboardFragment extends MCFragment { .subscribe(() -> { View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.snackbar_added_to_dashboard, monster.name), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_added_to_dashboard, monster.name)); } }, Logger::logError)); } @@ -238,7 +310,7 @@ public class DashboardFragment extends MCFragment { .subscribe(() -> { View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.snackbar_collection_added_to_dashboard, collection.name), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_collection_added_to_dashboard, collection.name)); } }, Logger::logError)); } @@ -248,7 +320,14 @@ public class DashboardFragment extends MCFragment { mDisposables.add(repository.removeMonsterFromDashboard(monster.id) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> {}, Logger::logError)); + .subscribe(() -> { + View view = getView(); + if (view != null) { + SnackbarHelper.makeLong(view, getString(R.string.snackbar_removed_from_dashboard, monster.name)) + .setAction(R.string.action_undo, v -> addMonsterToDashboard(monster)) + .show(); + } + }, Logger::logError)); } private void clearDashboard() { @@ -259,7 +338,7 @@ public class DashboardFragment extends MCFragment { .subscribe(() -> { View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.snackbar_dashboard_cleared), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_dashboard_cleared)); } }, Logger::logError)); } @@ -283,6 +362,9 @@ public class DashboardFragment extends MCFragment { } else if (item.getItemId() == R.id.menu_action_add_collection_option) { showAddCollectionPicker(); return true; + } else if (item.getItemId() == R.id.menu_action_remove_single_monster) { + showRemoveMonsterPicker(); + return true; } else if (item.getItemId() == R.id.menu_action_clear_dashboard) { clearDashboard(); return true; diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java index af372bd..e2693e0 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java @@ -18,29 +18,44 @@ import com.majinnaibu.monstercards.data.enums.ChallengeRating; import com.majinnaibu.monstercards.data.enums.ProficiencyType; import com.majinnaibu.monstercards.databinding.CardMonsterBinding; import com.majinnaibu.monstercards.helpers.CommonMarkHelper; +import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Trait; import com.majinnaibu.monstercards.utils.Logger; import java.util.Locale; -public class DashboardRecyclerViewAdapter extends ListAdapter { - private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { +public class DashboardRecyclerViewAdapter extends ListAdapter { + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override - public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) { - return oldItem.id.equals(newItem.id); + public boolean areItemsTheSame(@NonNull DashboardMonsterWithMonster oldItem, @NonNull DashboardMonsterWithMonster newItem) { + if (oldItem.dashboardEntry != null && newItem.dashboardEntry != null) { + return oldItem.dashboardEntry.id == newItem.dashboardEntry.id; + } + return oldItem.monster.id.equals(newItem.monster.id); } @Override - public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) { - return oldItem.equals(newItem); + public boolean areContentsTheSame(@NonNull DashboardMonsterWithMonster oldItem, @NonNull DashboardMonsterWithMonster newItem) { + if (oldItem.dashboardEntry != null && newItem.dashboardEntry != null) { + return oldItem.dashboardEntry.id == newItem.dashboardEntry.id + && oldItem.dashboardEntry.ordinal == newItem.dashboardEntry.ordinal + && (oldItem.monster != null && oldItem.monster.equals(newItem.monster)); + } + return (oldItem.monster != null && oldItem.monster.equals(newItem.monster)); } }; private final ItemCallback mOnClick; + private final ItemCallback mOnLongClick; - public DashboardRecyclerViewAdapter(ItemCallback onClick) { + public DashboardRecyclerViewAdapter(ItemCallback onClick, ItemCallback onLongClick) { super(DIFF_CALLBACK); mOnClick = onClick; + mOnLongClick = onLongClick; + } + + public DashboardRecyclerViewAdapter(ItemCallback onClick) { + this(onClick, null); } @NonNull @@ -52,7 +67,9 @@ public class DashboardRecyclerViewAdapter extends ListAdapter { if (mOnClick != null) { - mOnClick.onItemCallback(holder.monster); + mOnClick.onItemCallback(holder.item); } }); + + holder.itemView.setOnLongClickListener(v -> { + if (mOnLongClick != null) { + mOnLongClick.onItemCallback(holder.item); + return true; + } + return false; + }); } public interface ItemCallback { - void onItemCallback(Monster monster); + void onItemCallback(DashboardMonsterWithMonster item); } public static class ViewHolder extends RecyclerView.ViewHolder { + public DashboardMonsterWithMonster item; public final TextView name; public final TextView meta; public final View action1Group; diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java index 98b6711..761405b 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java @@ -4,23 +4,23 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; -import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster; import java.util.ArrayList; import java.util.List; public class DashboardViewModel extends ViewModel { - private final MutableLiveData> mMonsters; + private final MutableLiveData> mMonsters; public DashboardViewModel() { mMonsters = new MutableLiveData<>(new ArrayList<>()); } - public LiveData> getMonsters() { + public LiveData> getMonsters() { return mMonsters; } - public void setMonsters(List monsters) { + public void setMonsters(List monsters) { mMonsters.setValue(monsters); } } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java index 84d1a70..f83d873 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java @@ -18,7 +18,6 @@ import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; -import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.data.enums.StringType; @@ -26,6 +25,7 @@ import com.majinnaibu.monstercards.data.enums.TraitType; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.SnackbarHelper; import java.util.ArrayList; import java.util.List; @@ -124,8 +124,7 @@ public class EditMonsterFragment extends MCFragment { public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { Logger.logError("Error saving monster", e); assert view != null; - Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG) - .setAction("Action", null).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster)); } }); }); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java index b9695c1..1f716a6 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java @@ -24,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.MainActivity; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; @@ -35,6 +34,7 @@ import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentDirections; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.SnackbarHelper; import java.util.UUID; @@ -206,7 +206,7 @@ public class LibraryFragment extends MCFragment { Logger.logError("Error creating monster", e); View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster)); } } })); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java index a529740..3091cc1 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java @@ -31,7 +31,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavDirections; import androidx.navigation.Navigation; -import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.helpers.CommonMarkHelper; @@ -40,6 +39,7 @@ import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.SnackbarHelper; import java.util.List; import java.util.UUID; @@ -252,7 +252,7 @@ public class MonsterDetailFragment extends MCFragment { Logger.logError("Failed to share monster", e); View view = getView(); if (view != null) { - Snackbar.make(view, R.string.failed_to_share_monster, Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, R.string.failed_to_share_monster); } } } @@ -270,7 +270,7 @@ public class MonsterDetailFragment extends MCFragment { View view = getView(); if (view != null) { String monsterName = mViewModel.getName().getValue(); - Snackbar.make(view, getString(R.string.snackbar_added_to_dashboard, monsterName), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_added_to_dashboard, monsterName)); } }, Logger::logError); } @@ -291,7 +291,7 @@ public class MonsterDetailFragment extends MCFragment { if (collections.isEmpty()) { View view = getView(); if (view != null) { - Snackbar.make(view, getString(R.string.no_collections_available), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, getString(R.string.no_collections_available)); } dispose(); return; @@ -330,11 +330,7 @@ public class MonsterDetailFragment extends MCFragment { View view = getView(); if (view != null) { String monsterName = mViewModel.getName().getValue(); - Snackbar.make( - view, - getString(R.string.snackbar_monster_added_to_collection, monsterName, collection.name), - Snackbar.LENGTH_LONG) - .show(); + SnackbarHelper.showLong(view, getString(R.string.snackbar_monster_added_to_collection, monsterName, collection.name)); } } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java index 1a64b8a..7ebf82a 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java @@ -23,7 +23,6 @@ import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.Navigation; -import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.MonsterCardsApplication; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; @@ -34,6 +33,7 @@ import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.ui.library.LibraryFragmentDirections; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.SnackbarHelper; import java.util.List; import java.util.UUID; @@ -174,7 +174,7 @@ public class MonsterImportFragment extends MCFragment { @Override public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { Logger.logError("Error creating monster", e); - Snackbar.make(mHolder.root, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(mHolder.root, getString(R.string.snackbar_failed_to_create_monster)); } }); } else { diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java index 2ded79c..6835279 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java @@ -19,11 +19,11 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; -import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.MonsterCardsApplication; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.SnackbarHelper; import java.io.OutputStream; import java.nio.charset.StandardCharsets; @@ -87,7 +87,7 @@ public class MCFragment extends Fragment { Logger.logError("Failed to launch document creation picker", e); View view = getView(); if (view != null) { - Snackbar.make(view, R.string.snackbar_export_failed, Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, R.string.snackbar_export_failed); } } } @@ -103,14 +103,14 @@ public class MCFragment extends Fragment { if (view != null) { String fileName = getFileNameFromUri(context, uri); String message = getString(R.string.snackbar_export_success, fileName); - Snackbar.make(view, message, Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, message); } } } catch (Exception e) { Logger.logError("Failed to write export content to URI", e); View view = getView(); if (view != null) { - Snackbar.make(view, R.string.snackbar_export_failed, Snackbar.LENGTH_LONG).show(); + SnackbarHelper.showLong(view, R.string.snackbar_export_failed); } } } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/utils/SnackbarHelper.java b/Android/app/src/main/java/com/majinnaibu/monstercards/utils/SnackbarHelper.java new file mode 100644 index 0000000..592b5e2 --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/utils/SnackbarHelper.java @@ -0,0 +1,53 @@ +package com.majinnaibu.monstercards.utils; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import com.google.android.material.snackbar.Snackbar; + +/** + * Centralized helper for managing Snackbar notifications and durations across the app. + */ +public class SnackbarHelper { + // Configurable default durations in milliseconds + public static int DEFAULT_SHORT_DURATION = 2500; // 2.5 seconds + public static int DEFAULT_LONG_DURATION = 4000; // 4.0 seconds (e.g. for Undo or action messages) + + public static Snackbar makeShort(@NonNull View view, @StringRes int resId) { + return make(view, view.getContext().getString(resId), DEFAULT_SHORT_DURATION); + } + + public static Snackbar makeShort(@NonNull View view, @NonNull CharSequence message) { + return make(view, message, DEFAULT_SHORT_DURATION); + } + + public static Snackbar makeLong(@NonNull View view, @StringRes int resId) { + return make(view, view.getContext().getString(resId), DEFAULT_LONG_DURATION); + } + + public static Snackbar makeLong(@NonNull View view, @NonNull CharSequence message) { + return make(view, message, DEFAULT_LONG_DURATION); + } + + public static Snackbar make(@NonNull View view, @NonNull CharSequence message, int durationMs) { + return Snackbar.make(view, message, durationMs); + } + + public static void showShort(@NonNull View view, @StringRes int resId) { + makeShort(view, resId).show(); + } + + public static void showShort(@NonNull View view, @NonNull CharSequence message) { + makeShort(view, message).show(); + } + + public static void showLong(@NonNull View view, @StringRes int resId) { + makeLong(view, resId).show(); + } + + public static void showLong(@NonNull View view, @NonNull CharSequence message) { + makeLong(view, message).show(); + } +} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/utils/ToastHelper.java b/Android/app/src/main/java/com/majinnaibu/monstercards/utils/ToastHelper.java new file mode 100644 index 0000000..fac187e --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/utils/ToastHelper.java @@ -0,0 +1,42 @@ +package com.majinnaibu.monstercards.utils; + +import android.content.Context; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +/** + * Centralized helper for managing Toast notifications and durations across the app. + */ +public class ToastHelper { + // Configurable default durations + public static int DEFAULT_SHORT_DURATION = Toast.LENGTH_SHORT; // ~2.0 seconds + public static int DEFAULT_LONG_DURATION = Toast.LENGTH_LONG; // ~3.5 seconds + + private static Toast sCurrentToast; + + public static void showShort(@NonNull Context context, @StringRes int resId) { + show(context, context.getString(resId), DEFAULT_SHORT_DURATION); + } + + public static void showShort(@NonNull Context context, @NonNull String message) { + show(context, message, DEFAULT_SHORT_DURATION); + } + + public static void showLong(@NonNull Context context, @StringRes int resId) { + show(context, context.getString(resId), DEFAULT_LONG_DURATION); + } + + public static void showLong(@NonNull Context context, @NonNull String message) { + show(context, message, DEFAULT_LONG_DURATION); + } + + public static void show(@NonNull Context context, @NonNull String message, int duration) { + if (sCurrentToast != null) { + sCurrentToast.cancel(); + } + sCurrentToast = Toast.makeText(context.getApplicationContext(), message, duration); + sCurrentToast.show(); + } +} diff --git a/Android/app/src/main/res/menu/dashboard_menu.xml b/Android/app/src/main/res/menu/dashboard_menu.xml index 23180bc..8d6eacf 100644 --- a/Android/app/src/main/res/menu/dashboard_menu.xml +++ b/Android/app/src/main/res/menu/dashboard_menu.xml @@ -12,6 +12,11 @@ android:title="@string/action_add_collection_option" app:showAsAction="never" /> + + Your Library is Empty Create new monster cards or import them from D&D Beyond or Open5e. Monster Cards + + Remove Monster... + Remove + Undo + View Details + Remove from Dashboard + Removed %1$s from Dashboard + Dashboard is already empty + Remove %1$s? + Remove %1$s from your dashboard?