Adds removing a monster from the dashboard via long press.

This commit is contained in:
2026-09-20 16:29:13 -07:00
parent c06b53ed6b
commit 0cbd03da69
18 changed files with 336 additions and 81 deletions

View File

@@ -21,8 +21,6 @@ import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI; import androidx.navigation.ui.NavigationUI;
import android.widget.Toast;
import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.majinnaibu.monstercards.helpers.MonsterImportHelper; 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.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.init.AppCenterInitializer; import com.majinnaibu.monstercards.init.AppCenterInitializer;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.ToastHelper;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
@@ -141,24 +140,24 @@ public class MainActivity extends AppCompatActivity {
public void importMonsterFromInputAndNavigate(@NonNull String input) { public void importMonsterFromInputAndNavigate(@NonNull String input) {
BinderImporter binderImporter = new BinderImporter(); BinderImporter binderImporter = new BinderImporter();
if (binderImporter.canImport(input)) { 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)) Single.fromCallable(() -> binderImporter.parse(input))
.flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder)) .flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> { .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)); NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
NavController navController = navHostFragment.getNavController(); NavController navController = navHostFragment.getNavController();
navController.navigate(R.id.navigation_library); navController.navigate(R.id.navigation_library);
}, throwable -> { }, throwable -> {
Logger.logError("Failed to import binder from input", 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; 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)) Single.fromCallable(() -> MonsterImportHelper.fromJSON(input))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@@ -170,7 +169,7 @@ public class MainActivity extends AppCompatActivity {
navController.navigate(navAction); navController.navigate(navAction);
}, throwable -> { }, throwable -> {
Logger.logError("Failed to import monster from input", 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);
}); });
} }

View File

@@ -3,9 +3,11 @@ package com.majinnaibu.monstercards.data;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update; import androidx.room.Update;
import com.majinnaibu.monstercards.models.DashboardMonster; import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import java.util.List; 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") @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<List<Monster>> getDashboardMonsters(); Flowable<List<Monster>> getDashboardMonsters();
@Transaction
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
Flowable<List<DashboardMonsterWithMonster>> getDashboardMonstersWithMonster();
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC") @Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
Flowable<List<DashboardMonster>> getDashboardMonsterEntries(); Flowable<List<DashboardMonster>> getDashboardMonsterEntries();
@@ -28,9 +34,12 @@ public interface DashboardDAO {
@Query("DELETE FROM dashboard_monsters WHERE id = :id") @Query("DELETE FROM dashboard_monsters WHERE id = :id")
Completable removeDashboardMonsterById(long 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); Completable removeMonsterFromDashboard(String monsterId);
@Query("DELETE FROM dashboard_monsters WHERE monster_id = :monsterId")
Completable removeAllMonstersFromDashboard(String monsterId);
@Query("DELETE FROM dashboard_monsters") @Query("DELETE FROM dashboard_monsters")
Completable clearDashboard(); Completable clearDashboard();

View File

@@ -208,6 +208,13 @@ public class MonsterRepository {
.observeOn(AndroidSchedulers.mainThread()); .observeOn(AndroidSchedulers.mainThread());
} }
public Flowable<List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster>> getDashboardMonstersWithMonster() {
return m_db.dashboardDAO()
.getDashboardMonstersWithMonster()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<DashboardMonster>> getDashboardMonsterEntries() { public Flowable<List<DashboardMonster>> getDashboardMonsterEntries() {
return m_db.dashboardDAO() return m_db.dashboardDAO()
.getDashboardMonsterEntries() .getDashboardMonsterEntries()

View File

@@ -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;
}
}

View File

@@ -21,15 +21,18 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
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.Collection; 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.models.Monster;
import com.majinnaibu.monstercards.ui.dashboard.DashboardRecyclerViewAdapter; import com.majinnaibu.monstercards.ui.dashboard.DashboardRecyclerViewAdapter;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -80,7 +83,15 @@ public class CollectionDetailFragment extends MCFragment {
mViewModel.getMonsters().observe(getViewLifecycleOwner(), monsters -> { mViewModel.getMonsters().observe(getViewLifecycleOwner(), monsters -> {
if (mAdapter != null) { if (mAdapter != null) {
mAdapter.submitList(monsters); List<DashboardMonsterWithMonster> 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); GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
recyclerView.setLayoutManager(layoutManager); recyclerView.setLayoutManager(layoutManager);
mAdapter = new DashboardRecyclerViewAdapter(monster -> { mAdapter = new DashboardRecyclerViewAdapter(item -> {
if (monster != null) { if (item != null && item.monster != null) {
navigateToMonsterDetail(monster.id); navigateToMonsterDetail(item.monster.id);
} else { } else {
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster"); Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
} }
@@ -133,7 +144,7 @@ public class CollectionDetailFragment extends MCFragment {
if (monsters.isEmpty()) { if (monsters.isEmpty()) {
View view = getView(); View view = getView();
if (view != null) { 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; return;
} }
@@ -162,11 +173,7 @@ public class CollectionDetailFragment extends MCFragment {
if (view != null) { if (view != null) {
Collection collection = mViewModel.getCollection().getValue(); Collection collection = mViewModel.getCollection().getValue();
String collectionName = collection != null ? collection.name : ""; String collectionName = collection != null ? collection.name : "";
Snackbar.make( SnackbarHelper.showLong(view, getString(R.string.snackbar_monster_added_to_collection, monster.name, collectionName));
view,
getString(R.string.snackbar_monster_added_to_collection, monster.name, collectionName),
Snackbar.LENGTH_LONG)
.show();
} }
}, Logger::logError)); }, Logger::logError));
} }
@@ -219,7 +226,7 @@ public class CollectionDetailFragment extends MCFragment {
if (view != null) { if (view != null) {
Collection collection = mViewModel.getCollection().getValue(); Collection collection = mViewModel.getCollection().getValue();
String collectionName = collection != null ? collection.name : ""; 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)); }, Logger::logError));
} }

View File

@@ -20,13 +20,13 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
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.Collection; import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.UUID; import java.util.UUID;
@@ -150,10 +150,7 @@ public class CollectionsFragment extends MCFragment {
public void onComplete() { public void onComplete() {
View view = getView(); View view = getView();
if (view != null) { if (view != null) {
Snackbar.make( SnackbarHelper.makeLong(view, getString(R.string.snackbar_collection_created, collection.name))
view,
getString(R.string.snackbar_collection_created, collection.name),
Snackbar.LENGTH_LONG)
.setAction("View", v -> navigateToCollectionDetail(collection.id)) .setAction("View", v -> navigateToCollectionDetail(collection.id))
.show(); .show();
} }
@@ -164,7 +161,7 @@ public class CollectionsFragment extends MCFragment {
Logger.logError("Error creating collection", e); Logger.logError("Error creating collection", e);
View view = getView(); View view = getView();
if (view != null) { 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));
} }
} }
}); });

View File

@@ -21,13 +21,13 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
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.Collection; import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -69,7 +69,7 @@ public class DashboardFragment extends MCFragment {
private void loadDashboardMonsters() { private void loadDashboardMonsters() {
MonsterRepository repository = getMonsterRepository(); MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getDashboardMonsters() mDisposables.add(repository.getDashboardMonstersWithMonster()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError)); .subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError));
@@ -82,14 +82,21 @@ public class DashboardFragment extends MCFragment {
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount); GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
recyclerView.setLayoutManager(layoutManager); recyclerView.setLayoutManager(layoutManager);
LiveData<List<Monster>> monsterData = mViewModel.getMonsters(); LiveData<List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster>> monsterData = mViewModel.getMonsters();
mAdapter = new DashboardRecyclerViewAdapter(monster -> { mAdapter = new DashboardRecyclerViewAdapter(
if (monster != null) { item -> {
navigateToMonsterDetail(monster); if (item != null && item.monster != null) {
} else { navigateToMonsterDetail(item.monster);
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster"); } else {
} Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
}); }
},
item -> {
if (item != null) {
showCardOptionsMenu(item);
}
}
);
if (monsterData != null) { if (monsterData != null) {
monsterData.observe(getViewLifecycleOwner(), monsters -> { monsterData.observe(getViewLifecycleOwner(), monsters -> {
mAdapter.submitList(monsters); mAdapter.submitList(monsters);
@@ -113,9 +120,9 @@ public class DashboardFragment extends MCFragment {
int fromPos = viewHolder.getAdapterPosition(); int fromPos = viewHolder.getAdapterPosition();
int toPos = target.getAdapterPosition(); int toPos = target.getAdapterPosition();
if (fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION) { if (fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION) {
List<Monster> currentList = new ArrayList<>(mAdapter.getCurrentList()); List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster> currentList = new ArrayList<>(mAdapter.getCurrentList());
if (fromPos < currentList.size() && toPos < currentList.size()) { 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); currentList.add(toPos, moved);
mAdapter.submitList(currentList); mAdapter.submitList(currentList);
} }
@@ -127,10 +134,10 @@ public class DashboardFragment extends MCFragment {
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition(); int position = viewHolder.getAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
List<Monster> currentList = mAdapter.getCurrentList(); List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster> currentList = mAdapter.getCurrentList();
if (position < currentList.size()) { if (position < currentList.size()) {
Monster monster = currentList.get(position); com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item = currentList.get(position);
removeMonsterFromDashboard(monster); removeDashboardMonsterEntry(item);
} }
} }
} }
@@ -142,6 +149,7 @@ public class DashboardFragment extends MCFragment {
String[] options = new String[]{ String[] options = new String[]{
getString(R.string.action_add_single_monster), getString(R.string.action_add_single_monster),
getString(R.string.action_add_collection_option), getString(R.string.action_add_collection_option),
getString(R.string.action_remove_single_monster),
getString(R.string.action_clear_dashboard) getString(R.string.action_clear_dashboard)
}; };
new AlertDialog.Builder(requireContext()) new AlertDialog.Builder(requireContext())
@@ -152,6 +160,8 @@ public class DashboardFragment extends MCFragment {
} else if (which == 1) { } else if (which == 1) {
showAddCollectionPicker(); showAddCollectionPicker();
} else if (which == 2) { } else if (which == 2) {
showRemoveMonsterPicker();
} else if (which == 3) {
clearDashboard(); clearDashboard();
} }
}) })
@@ -159,6 +169,68 @@ public class DashboardFragment extends MCFragment {
.show(); .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() { private void showAddMonsterPicker() {
MonsterRepository repository = getMonsterRepository(); MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getMonsters() mDisposables.add(repository.getMonsters()
@@ -169,7 +241,7 @@ public class DashboardFragment extends MCFragment {
if (monsters.isEmpty()) { if (monsters.isEmpty()) {
View view = getView(); View view = getView();
if (view != null) { 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; return;
} }
@@ -198,7 +270,7 @@ public class DashboardFragment extends MCFragment {
if (collections.isEmpty()) { if (collections.isEmpty()) {
View view = getView(); View view = getView();
if (view != null) { 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; return;
} }
@@ -225,7 +297,7 @@ public class DashboardFragment extends MCFragment {
.subscribe(() -> { .subscribe(() -> {
View view = getView(); View view = getView();
if (view != null) { 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)); }, Logger::logError));
} }
@@ -238,7 +310,7 @@ public class DashboardFragment extends MCFragment {
.subscribe(() -> { .subscribe(() -> {
View view = getView(); View view = getView();
if (view != null) { 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)); }, Logger::logError));
} }
@@ -248,7 +320,14 @@ public class DashboardFragment extends MCFragment {
mDisposables.add(repository.removeMonsterFromDashboard(monster.id) mDisposables.add(repository.removeMonsterFromDashboard(monster.id)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .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() { private void clearDashboard() {
@@ -259,7 +338,7 @@ public class DashboardFragment extends MCFragment {
.subscribe(() -> { .subscribe(() -> {
View view = getView(); View view = getView();
if (view != null) { 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)); }, Logger::logError));
} }
@@ -283,6 +362,9 @@ public class DashboardFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_add_collection_option) { } else if (item.getItemId() == R.id.menu_action_add_collection_option) {
showAddCollectionPicker(); showAddCollectionPicker();
return true; 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) { } else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
clearDashboard(); clearDashboard();
return true; return true;

View File

@@ -18,29 +18,44 @@ import com.majinnaibu.monstercards.data.enums.ChallengeRating;
import com.majinnaibu.monstercards.data.enums.ProficiencyType; import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.databinding.CardMonsterBinding; import com.majinnaibu.monstercards.databinding.CardMonsterBinding;
import com.majinnaibu.monstercards.helpers.CommonMarkHelper; import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Trait; import com.majinnaibu.monstercards.models.Trait;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import java.util.Locale; import java.util.Locale;
public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, DashboardRecyclerViewAdapter.ViewHolder> { public class DashboardRecyclerViewAdapter extends ListAdapter<DashboardMonsterWithMonster, DashboardRecyclerViewAdapter.ViewHolder> {
private static final DiffUtil.ItemCallback<Monster> DIFF_CALLBACK = new DiffUtil.ItemCallback<Monster>() { private static final DiffUtil.ItemCallback<DashboardMonsterWithMonster> DIFF_CALLBACK = new DiffUtil.ItemCallback<DashboardMonsterWithMonster>() {
@Override @Override
public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) { public boolean areItemsTheSame(@NonNull DashboardMonsterWithMonster oldItem, @NonNull DashboardMonsterWithMonster newItem) {
return oldItem.id.equals(newItem.id); if (oldItem.dashboardEntry != null && newItem.dashboardEntry != null) {
return oldItem.dashboardEntry.id == newItem.dashboardEntry.id;
}
return oldItem.monster.id.equals(newItem.monster.id);
} }
@Override @Override
public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) { public boolean areContentsTheSame(@NonNull DashboardMonsterWithMonster oldItem, @NonNull DashboardMonsterWithMonster newItem) {
return oldItem.equals(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 mOnClick;
private final ItemCallback mOnLongClick;
public DashboardRecyclerViewAdapter(ItemCallback onClick) { public DashboardRecyclerViewAdapter(ItemCallback onClick, ItemCallback onLongClick) {
super(DIFF_CALLBACK); super(DIFF_CALLBACK);
mOnClick = onClick; mOnClick = onClick;
mOnLongClick = onLongClick;
}
public DashboardRecyclerViewAdapter(ItemCallback onClick) {
this(onClick, null);
} }
@NonNull @NonNull
@@ -52,7 +67,9 @@ public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, Dashboard
@Override @Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) { public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Logger.logUnimplementedMethod(); Logger.logUnimplementedMethod();
Monster monster = getItem(position); DashboardMonsterWithMonster item = getItem(position);
Monster monster = item.monster;
holder.item = item;
holder.monster = monster; holder.monster = monster;
holder.name.setText(monster.name); holder.name.setText(monster.name);
holder.meta.setText(monster.getMeta()); holder.meta.setText(monster.getMeta());
@@ -120,16 +137,25 @@ public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, Dashboard
holder.itemView.setOnClickListener(v -> { holder.itemView.setOnClickListener(v -> {
if (mOnClick != null) { 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 { public interface ItemCallback {
void onItemCallback(Monster monster); void onItemCallback(DashboardMonsterWithMonster item);
} }
public static class ViewHolder extends RecyclerView.ViewHolder { public static class ViewHolder extends RecyclerView.ViewHolder {
public DashboardMonsterWithMonster item;
public final TextView name; public final TextView name;
public final TextView meta; public final TextView meta;
public final View action1Group; public final View action1Group;

View File

@@ -4,23 +4,23 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class DashboardViewModel extends ViewModel { public class DashboardViewModel extends ViewModel {
private final MutableLiveData<List<Monster>> mMonsters; private final MutableLiveData<List<DashboardMonsterWithMonster>> mMonsters;
public DashboardViewModel() { public DashboardViewModel() {
mMonsters = new MutableLiveData<>(new ArrayList<>()); mMonsters = new MutableLiveData<>(new ArrayList<>());
} }
public LiveData<List<Monster>> getMonsters() { public LiveData<List<DashboardMonsterWithMonster>> getMonsters() {
return mMonsters; return mMonsters;
} }
public void setMonsters(List<Monster> monsters) { public void setMonsters(List<DashboardMonsterWithMonster> monsters) {
mMonsters.setValue(monsters); mMonsters.setValue(monsters);
} }
} }

View File

@@ -18,7 +18,6 @@ import androidx.navigation.NavController;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.snackbar.Snackbar;
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.data.enums.StringType; 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.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -124,8 +124,7 @@ public class EditMonsterFragment extends MCFragment {
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error saving monster", e); Logger.logError("Error saving monster", e);
assert view != null; assert view != null;
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG) SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster));
.setAction("Action", null).show();
} }
}); });
}); });

View File

@@ -24,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.MainActivity; import com.majinnaibu.monstercards.MainActivity;
import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository; 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.MCFragment;
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.UUID; import java.util.UUID;
@@ -206,7 +206,7 @@ public class LibraryFragment extends MCFragment {
Logger.logError("Error creating monster", e); Logger.logError("Error creating monster", e);
View view = getView(); View view = getView();
if (view != null) { 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));
} }
} }
})); }));

View File

@@ -31,7 +31,6 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
import androidx.navigation.Navigation; import androidx.navigation.Navigation;
import com.google.android.material.snackbar.Snackbar;
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.CommonMarkHelper; 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.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -252,7 +252,7 @@ public class MonsterDetailFragment extends MCFragment {
Logger.logError("Failed to share monster", e); Logger.logError("Failed to share monster", e);
View view = getView(); View view = getView();
if (view != null) { 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(); View view = getView();
if (view != null) { if (view != null) {
String monsterName = mViewModel.getName().getValue(); 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); }, Logger::logError);
} }
@@ -291,7 +291,7 @@ public class MonsterDetailFragment extends MCFragment {
if (collections.isEmpty()) { if (collections.isEmpty()) {
View view = getView(); View view = getView();
if (view != null) { 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(); dispose();
return; return;
@@ -330,11 +330,7 @@ public class MonsterDetailFragment extends MCFragment {
View view = getView(); View view = getView();
if (view != null) { if (view != null) {
String monsterName = mViewModel.getName().getValue(); String monsterName = mViewModel.getName().getValue();
Snackbar.make( SnackbarHelper.showLong(view, getString(R.string.snackbar_monster_added_to_collection, monsterName, collection.name));
view,
getString(R.string.snackbar_monster_added_to_collection, monsterName, collection.name),
Snackbar.LENGTH_LONG)
.show();
} }
} }

View File

@@ -23,7 +23,6 @@ import androidx.navigation.NavController;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
import androidx.navigation.Navigation; import androidx.navigation.Navigation;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.MonsterCardsApplication; import com.majinnaibu.monstercards.MonsterCardsApplication;
import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository; 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.library.LibraryFragmentDirections;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -174,7 +174,7 @@ public class MonsterImportFragment extends MCFragment {
@Override @Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error creating monster", 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 { } else {

View File

@@ -19,11 +19,11 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.MonsterCardsApplication; import com.majinnaibu.monstercards.MonsterCardsApplication;
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.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -87,7 +87,7 @@ public class MCFragment extends Fragment {
Logger.logError("Failed to launch document creation picker", e); Logger.logError("Failed to launch document creation picker", e);
View view = getView(); View view = getView();
if (view != null) { 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) { if (view != null) {
String fileName = getFileNameFromUri(context, uri); String fileName = getFileNameFromUri(context, uri);
String message = getString(R.string.snackbar_export_success, fileName); String message = getString(R.string.snackbar_export_success, fileName);
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show(); SnackbarHelper.showLong(view, message);
} }
} }
} catch (Exception e) { } catch (Exception e) {
Logger.logError("Failed to write export content to URI", e); Logger.logError("Failed to write export content to URI", e);
View view = getView(); View view = getView();
if (view != null) { if (view != null) {
Snackbar.make(view, R.string.snackbar_export_failed, Snackbar.LENGTH_LONG).show(); SnackbarHelper.showLong(view, R.string.snackbar_export_failed);
} }
} }
} }

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -12,6 +12,11 @@
android:title="@string/action_add_collection_option" android:title="@string/action_add_collection_option"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_action_remove_single_monster"
android:title="@string/action_remove_single_monster"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_action_clear_dashboard" android:id="@+id/menu_action_clear_dashboard"
android:title="@string/action_clear_dashboard" android:title="@string/action_clear_dashboard"

View File

@@ -188,4 +188,14 @@
<string name="empty_library_title">Your Library is Empty</string> <string name="empty_library_title">Your Library is Empty</string>
<string name="empty_library_subtitle">Create new monster cards or import them from D&amp;D Beyond or Open5e.</string> <string name="empty_library_subtitle">Create new monster cards or import them from D&amp;D Beyond or Open5e.</string>
<string name="app_title_splash">Monster Cards</string> <string name="app_title_splash">Monster Cards</string>
<string name="action_remove_single_monster">Remove Monster...</string>
<string name="action_remove">Remove</string>
<string name="action_undo">Undo</string>
<string name="action_view_details">View Details</string>
<string name="action_remove_from_dashboard">Remove from Dashboard</string>
<string name="snackbar_removed_from_dashboard">Removed %1$s from Dashboard</string>
<string name="snackbar_dashboard_already_empty">Dashboard is already empty</string>
<string name="dialog_remove_from_dashboard_title">Remove %1$s?</string>
<string name="dialog_remove_from_dashboard_message">Remove %1$s from your dashboard?</string>
</resources> </resources>