diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml
index 0250817..13c7d91 100644
--- a/Android/app/src/main/AndroidManifest.xml
+++ b/Android/app/src/main/AndroidManifest.xml
@@ -48,6 +48,8 @@
+
+
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 8455dd9..f2e79b1 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java
@@ -22,6 +22,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.gson.Gson;
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
import com.majinnaibu.monstercards.helpers.StringHelper;
+import com.majinnaibu.monstercards.importers.BinderImporter;
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.init.AppCenterInitializer;
import com.majinnaibu.monstercards.utils.Logger;
@@ -110,6 +111,25 @@ 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();
+ 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();
+ 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();
+ });
+ return;
+ }
+
Toast.makeText(this, R.string.toast_importing_url, Toast.LENGTH_SHORT).show();
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input))
.subscribeOn(Schedulers.io())
@@ -143,7 +163,7 @@ public class MainActivity extends AppCompatActivity {
if (uri == null || !isMonsterFile(uri)) {
if (uri != null) {
- Logger.logError("Ignored file because extension is not supported (.monster, .card): " + uri);
+ Logger.logError("Ignored file because extension is not supported (.monster, .card, .binder): " + uri);
}
return null;
}
@@ -162,7 +182,8 @@ public class MainActivity extends AppCompatActivity {
}
String lowerName = fileName.toLowerCase(Locale.ROOT);
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt")
- || lowerName.endsWith(".card") || lowerName.endsWith(".card.txt");
+ || lowerName.endsWith(".card") || lowerName.endsWith(".card.txt")
+ || lowerName.endsWith(".binder") || lowerName.endsWith(".binder.txt");
}
@Nullable
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 ab1e81d..ced6e62 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
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.AppDatabase;
import com.majinnaibu.monstercards.helpers.StringHelper;
+import com.majinnaibu.monstercards.models.BinderExport;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.CollectionWithCount;
@@ -12,7 +13,9 @@ import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.SearchResultItem;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -260,6 +263,67 @@ public class MonsterRepository {
return result;
}
+ public Completable importBinder(@NonNull BinderExport binder) {
+ return Completable.fromAction(() -> {
+ if (binder.collections == null || binder.collections.isEmpty()) {
+ return;
+ }
+
+ for (BinderExport.CollectionExport colExport : binder.collections) {
+ if (colExport.cards == null || colExport.cards.isEmpty()) {
+ continue;
+ }
+
+ List monstersToSave = new ArrayList<>();
+ for (Monster card : colExport.cards) {
+ if (card.id == null) {
+ card.id = UUID.randomUUID();
+ }
+ monstersToSave.add(card);
+ }
+ m_db.monsterDAO().save(monstersToSave.toArray(new Monster[0])).blockingAwait();
+
+ String colName = colExport.name != null ? colExport.name.trim() : "";
+ if (!colName.isEmpty()) {
+ List existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet();
+ Collection targetCollection = null;
+ for (Collection existing : existingCols) {
+ if (existing.name != null && existing.name.equalsIgnoreCase(colName)) {
+ targetCollection = existing;
+ break;
+ }
+ }
+
+ UUID targetCollectionId;
+ if (targetCollection != null) {
+ targetCollectionId = targetCollection.id;
+ } else {
+ Collection newCol = new Collection();
+ newCol.id = UUID.randomUUID();
+ newCol.name = colName;
+ m_db.collectionDAO().save(newCol).blockingAwait();
+ targetCollectionId = newCol.id;
+ }
+
+ List colMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString())
+ .first(new ArrayList<>()).blockingGet();
+ Set existingMonsterIds = new HashSet<>();
+ for (Monster m : colMonsters) {
+ existingMonsterIds.add(m.id);
+ }
+
+ int ordinal = colMonsters.size();
+ for (Monster monster : monstersToSave) {
+ if (!existingMonsterIds.contains(monster.id)) {
+ m_db.collectionDAO().addMonsterToCollection(new CollectionMonster(targetCollectionId, monster.id, ordinal++)).blockingAwait();
+ existingMonsterIds.add(monster.id);
+ }
+ }
+ }
+ }
+ }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ }
+
private static class Helpers {
static boolean monsterMatchesSearch(Monster monster, String searchText) {
if (StringHelper.isNullOrEmpty(searchText)) {
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java
new file mode 100644
index 0000000..b988333
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java
@@ -0,0 +1,34 @@
+package com.majinnaibu.monstercards.exporters;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.majinnaibu.monstercards.models.BinderExport;
+import com.majinnaibu.monstercards.models.Monster;
+
+import java.util.Collections;
+import java.util.List;
+
+public class BinderExporter {
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+
+ @NonNull
+ public String exportBinder(String collectionName, List monsters) {
+ BinderExport export = new BinderExport();
+ export.schemaVersion = 1;
+
+ if (monsters != null) {
+ for (Monster monster : monsters) {
+ monster.schemaVersion = 1;
+ }
+ }
+
+ BinderExport.CollectionExport col = new BinderExport.CollectionExport(
+ collectionName != null ? collectionName : "",
+ monsters
+ );
+ export.collections = Collections.singletonList(col);
+ return GSON.toJson(export);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java
new file mode 100644
index 0000000..96bd982
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java
@@ -0,0 +1,17 @@
+package com.majinnaibu.monstercards.exporters;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.majinnaibu.monstercards.models.Monster;
+
+public class MonsterCardExporter {
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+
+ @NonNull
+ public String exportCard(@NonNull Monster monster) {
+ monster.schemaVersion = 1;
+ return GSON.toJson(monster);
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java
new file mode 100644
index 0000000..d2f97df
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java
@@ -0,0 +1,37 @@
+package com.majinnaibu.monstercards.importers;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.majinnaibu.monstercards.models.BinderExport;
+
+public class BinderImporter implements EntityImporter {
+
+ private static final Gson GSON = new Gson();
+
+ @Override
+ public boolean canImport(@NonNull String input) {
+ try {
+ JsonElement el = JsonParser.parseString(input);
+ if (el.isJsonObject()) {
+ JsonObject obj = el.getAsJsonObject();
+ return obj.has("collections") && obj.get("collections").isJsonArray();
+ }
+ } catch (Exception ignored) {
+ }
+ return false;
+ }
+
+ @NonNull
+ @Override
+ public BinderExport parse(@NonNull String input) throws Exception {
+ BinderExport binder = GSON.fromJson(input, BinderExport.class);
+ if (binder == null || binder.collections == null) {
+ throw new IllegalArgumentException("Failed to deserialize Binder JSON");
+ }
+ return binder;
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java
new file mode 100644
index 0000000..f9fcc00
--- /dev/null
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java
@@ -0,0 +1,30 @@
+package com.majinnaibu.monstercards.models;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BinderExport {
+ @SerializedName("schemaVersion")
+ public int schemaVersion = 1;
+
+ @SerializedName("collections")
+ public List collections = new ArrayList<>();
+
+ public static class CollectionExport {
+ @SerializedName("name")
+ public String name;
+
+ @SerializedName("cards")
+ public List cards = new ArrayList<>();
+
+ public CollectionExport() {
+ }
+
+ public CollectionExport(String name, List cards) {
+ this.name = name != null ? name : "";
+ this.cards = cards != null ? cards : new ArrayList<>();
+ }
+ }
+}
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java
index 2678c34..75c29bf 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
+import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
@@ -29,6 +30,9 @@ import java.util.UUID;
@SuppressWarnings("unused")
public class Monster {
+ @Ignore
+ public int schemaVersion = 1;
+
@PrimaryKey
@NonNull
public UUID id;
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 911696c..2e1e853 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
@@ -187,10 +187,28 @@ public class CollectionDetailFragment extends MCFragment {
if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) {
addCollectionToDashboard();
return true;
+ } else if (item.getItemId() == R.id.menu_action_export_collection) {
+ exportCollection();
+ return true;
}
return super.onOptionsItemSelected(item);
}
+ private void exportCollection() {
+ Collection collection = mViewModel.getCollection().getValue();
+ List monsters = mViewModel.getMonsters().getValue();
+ if (collection != null) {
+ String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder(
+ collection.name,
+ monsters != null ? monsters : new java.util.ArrayList<>()
+ );
+ String collectionName = (collection.name != null && !collection.name.trim().isEmpty())
+ ? collection.name.trim()
+ : "collection";
+ exportToFile(collectionName + ".binder", json);
+ }
+ }
+
private void addCollectionToDashboard() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.addCollectionToDashboard(mCollectionId)
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 67b7810..81be1bb 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
@@ -274,10 +274,25 @@ public class DashboardFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
clearDashboard();
return true;
+ } else if (item.getItemId() == R.id.menu_action_export_dashboard) {
+ exportDashboard();
+ return true;
}
return super.onOptionsItemSelected(item);
}
+ private void exportDashboard() {
+ mDisposables.add(getMonsterRepository().getDashboardMonsters()
+ .firstOrError()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(monsters -> {
+ String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder("", monsters);
+ String fileName = getString(R.string.default_filename_dashboard) + ".binder";
+ exportToFile(fileName, json);
+ }, Logger::logError));
+ }
+
@Override
public void onDestroyView() {
super.onDestroyView();
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 f0d1b7f..f3661e9 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
@@ -70,10 +70,25 @@ public class LibraryFragment extends MCFragment {
if (item.getItemId() == R.id.menu_action_import_from_url) {
showImportUrlDialog();
return true;
+ } else if (item.getItemId() == R.id.menu_action_export_library) {
+ exportLibrary();
+ return true;
}
return super.onOptionsItemSelected(item);
}
+ private void exportLibrary() {
+ getMonsterRepository().getMonsters()
+ .firstOrError()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(monsters -> {
+ String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder("", monsters);
+ String fileName = getString(R.string.default_filename_library) + ".binder";
+ exportToFile(fileName, json);
+ }, Logger::logError);
+ }
+
private void showImportUrlDialog() {
Context context = requireContext();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
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 5dc7015..a529740 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
@@ -195,10 +195,25 @@ public class MonsterDetailFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_share_monster) {
shareCurrentMonster();
return true;
+ } else if (item.getItemId() == R.id.menu_action_export_card) {
+ exportCurrentMonsterCard();
+ return true;
}
return super.onOptionsItemSelected(item);
}
+ private void exportCurrentMonsterCard() {
+ Monster monster = mViewModel.getMonster();
+ if (monster == null) {
+ return;
+ }
+ String jsonCard = new com.majinnaibu.monstercards.exporters.MonsterCardExporter().exportCard(monster);
+ String safeName = (monster.name != null && !monster.name.trim().isEmpty())
+ ? monster.name.trim()
+ : "monster";
+ exportToFile(safeName + ".card", jsonCard);
+ }
+
private void shareCurrentMonster() {
Monster monster = mViewModel.getMonster();
if (monster == null) {
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 45c3ec7..2ded79c 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
@@ -1,15 +1,53 @@
package com.majinnaibu.monstercards.ui.shared;
import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
+import android.provider.OpenableColumns;
+import android.view.View;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
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 java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
public class MCFragment extends Fragment {
+ private String mPendingExportContent;
+ private ActivityResultLauncher mCreateDocumentLauncher;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mCreateDocumentLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
+ Uri uri = result.getData().getData();
+ if (uri != null && mPendingExportContent != null) {
+ writeContentToUri(uri, mPendingExportContent);
+ }
+ }
+ mPendingExportContent = null;
+ });
+ }
+
public MonsterCardsApplication getApplication() {
return (MonsterCardsApplication) requireActivity().getApplication();
}
@@ -32,4 +70,76 @@ public class MCFragment extends Fragment {
}
}
}
+
+ protected void exportToFile(@NonNull String defaultFileName, @NonNull String content) {
+ mPendingExportContent = content;
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_TITLE, defaultFileName);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Uri downloadsUri = Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload");
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, downloadsUri);
+ }
+ try {
+ mCreateDocumentLauncher.launch(intent);
+ } catch (Exception e) {
+ 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();
+ }
+ }
+ }
+
+ private void writeContentToUri(@NonNull Uri uri, @NonNull String content) {
+ Context context = getContext();
+ if (context == null) return;
+ try (OutputStream out = context.getContentResolver().openOutputStream(uri)) {
+ if (out != null) {
+ out.write(content.getBytes(StandardCharsets.UTF_8));
+ out.flush();
+ View view = getView();
+ 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();
+ }
+ }
+ } 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();
+ }
+ }
+ }
+
+ private String getFileNameFromUri(@NonNull Context context, @NonNull Uri uri) {
+ String displayName = null;
+ if ("content".equals(uri.getScheme())) {
+ try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ if (nameIndex != -1) {
+ displayName = cursor.getString(nameIndex);
+ }
+ }
+ } catch (Exception e) {
+ Logger.logError("Error querying display name from content URI", e);
+ }
+ }
+ return displayName != null ? displayName : defaultFileNameFromUri(uri);
+ }
+
+ private String defaultFileNameFromUri(@NonNull Uri uri) {
+ String path = uri.getPath();
+ if (path != null) {
+ int lastSlash = path.lastIndexOf('/');
+ if (lastSlash >= 0 && lastSlash < path.length() - 1) {
+ return path.substring(lastSlash + 1);
+ }
+ }
+ return uri.toString();
+ }
}
diff --git a/Android/app/src/main/res/menu/collection_detail_menu.xml b/Android/app/src/main/res/menu/collection_detail_menu.xml
index 464f7b4..02cd759 100644
--- a/Android/app/src/main/res/menu/collection_detail_menu.xml
+++ b/Android/app/src/main/res/menu/collection_detail_menu.xml
@@ -6,4 +6,9 @@
android:id="@+id/menu_action_add_collection_to_dashboard"
android:title="@string/action_add_collection_to_dashboard"
app:showAsAction="never" />
+
+
diff --git a/Android/app/src/main/res/menu/dashboard_menu.xml b/Android/app/src/main/res/menu/dashboard_menu.xml
index 81b0839..23180bc 100644
--- a/Android/app/src/main/res/menu/dashboard_menu.xml
+++ b/Android/app/src/main/res/menu/dashboard_menu.xml
@@ -16,4 +16,9 @@
android:id="@+id/menu_action_clear_dashboard"
android:title="@string/action_clear_dashboard"
app:showAsAction="never" />
+
+
diff --git a/Android/app/src/main/res/menu/library_menu.xml b/Android/app/src/main/res/menu/library_menu.xml
index 4b0813a..f3b03a5 100644
--- a/Android/app/src/main/res/menu/library_menu.xml
+++ b/Android/app/src/main/res/menu/library_menu.xml
@@ -6,4 +6,9 @@
android:id="@+id/menu_action_import_from_url"
android:title="@string/action_import_from_url"
app:showAsAction="never" />
+
+
diff --git a/Android/app/src/main/res/menu/monster_detail_menu.xml b/Android/app/src/main/res/menu/monster_detail_menu.xml
index f084a1e..600a509 100644
--- a/Android/app/src/main/res/menu/monster_detail_menu.xml
+++ b/Android/app/src/main/res/menu/monster_detail_menu.xml
@@ -23,4 +23,9 @@
android:id="@+id/menu_action_add_to_dashboard"
android:title="@string/action_add_to_dashboard"
app:showAsAction="never" />
+
+
diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml
index a0ef60c..a4fef13 100644
--- a/Android/app/src/main/res/values/strings.xml
+++ b/Android/app/src/main/res/values/strings.xml
@@ -171,4 +171,13 @@
Failed to import entity. Please check the URL, ID, or JSON format.
Share / Export Monster (.card)
Failed to share monster
+ Export Card
+ Export Collection
+ Export Library
+ Export Dashboard
+ Exported to %1$s
+ Failed to export file
+ Imported binder successfully
+ MonsterCards Library
+ MonsterCards Dashboard
diff --git a/Android/app/src/test/java/com/majinnaibu/monstercards/exporters/BinderImportExportTest.java b/Android/app/src/test/java/com/majinnaibu/monstercards/exporters/BinderImportExportTest.java
new file mode 100644
index 0000000..378f186
--- /dev/null
+++ b/Android/app/src/test/java/com/majinnaibu/monstercards/exporters/BinderImportExportTest.java
@@ -0,0 +1,59 @@
+package com.majinnaibu.monstercards.exporters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.majinnaibu.monstercards.importers.BinderImporter;
+import com.majinnaibu.monstercards.models.BinderExport;
+import com.majinnaibu.monstercards.models.Monster;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.UUID;
+
+public class BinderImportExportTest {
+
+ @Test
+ public void testExportAndImportBinder() throws Exception {
+ Monster monster = new Monster();
+ monster.id = UUID.randomUUID();
+ monster.name = "Goblin Warrior";
+ monster.size = "Small";
+ monster.type = "humanoid";
+
+ BinderExporter exporter = new BinderExporter();
+ String json = exporter.exportBinder("Goblins", Collections.singletonList(monster));
+
+ BinderImporter importer = new BinderImporter();
+ assertTrue(importer.canImport(json));
+
+ BinderExport binder = importer.parse(json);
+ assertEquals(1, binder.schemaVersion);
+ assertNotNull(binder.collections);
+ assertEquals(1, binder.collections.size());
+ assertEquals("Goblins", binder.collections.get(0).name);
+ assertEquals(1, binder.collections.get(0).cards.size());
+ assertEquals("Goblin Warrior", binder.collections.get(0).cards.get(0).name);
+ assertEquals(1, binder.collections.get(0).cards.get(0).schemaVersion);
+ }
+
+ @Test
+ public void testExportSingleCard() {
+ Monster monster = new Monster();
+ monster.id = UUID.randomUUID();
+ monster.name = "Dragon";
+
+ MonsterCardExporter exporter = new MonsterCardExporter();
+ String json = exporter.exportCard(monster);
+
+ JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
+ assertTrue(obj.has("schemaVersion"));
+ assertEquals(1, obj.get("schemaVersion").getAsInt());
+ assertEquals("Dragon", obj.get("name").getAsString());
+ }
+}