diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file 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 7cd00ee..fc0f333 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java @@ -28,6 +28,8 @@ 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.models.BinderExport; +import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.ToastHelper; @@ -35,6 +37,8 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.Objects; @@ -131,46 +135,161 @@ public class MainActivity extends AppCompatActivity { } } + Uri intentUri = getUriFromIntent(intent); + String fileName = intentUri != null ? getFileNameFromUri(intentUri) : null; String json = readMonsterJSONFromIntent(intent); if (!StringHelper.isNullOrEmpty(json)) { - importMonsterFromInputAndNavigate(json); + importMonsterFromInputAndNavigate(json, fileName); } } + @Nullable + private Uri getUriFromIntent(@NonNull Intent intent) { + String action = intent.getAction(); + Bundle extras = intent.getExtras(); + if ("android.intent.action.SEND".equals(action)) { + if (extras != null) { + return extras.getParcelable(Intent.EXTRA_STREAM); + } + } else if ("android.intent.action.VIEW".equals(action) || "android.intent.action.EDIT".equals(action)) { + return intent.getData(); + } + return null; + } + public void importMonsterFromInputAndNavigate(@NonNull String input) { - BinderImporter binderImporter = new BinderImporter(); - if (binderImporter.canImport(input)) { + importMonsterFromInputAndNavigate(input, null); + } + + public void importMonsterFromInputAndNavigate(@NonNull String input, @Nullable String fileName) { + try { + BinderImporter binderImporter = new BinderImporter(); + if (binderImporter.canImport(input)) { + 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(() -> { + 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: " + fileName, throwable); + ToastHelper.showLong(this, R.string.failed_to_import_url); + }); + return; + } + ToastHelper.showShort(this, R.string.toast_importing_url); - Single.fromCallable(() -> binderImporter.parse(input)) - .flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder)) + Single.fromCallable(() -> MonsterImportHelper.fromJSON(input, fileName)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - ToastHelper.showLong(this, R.string.snackbar_import_binder_success); + .subscribe(monster -> { + String serializedJson = new Gson().toJson(monster); NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment)); NavController navController = navHostFragment.getNavController(); - navController.navigate(R.id.navigation_library); + NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson); + navController.navigate(navAction); }, throwable -> { - Logger.logError("Failed to import binder from input", throwable); + Logger.logError("Failed to import monster from input: " + fileName, throwable); ToastHelper.showLong(this, R.string.failed_to_import_url); }); + } catch (Exception e) { + Logger.logError("Failed to process import input: " + fileName, e); + ToastHelper.showLong(this, R.string.failed_to_import_url); + } + } + + public void importMultipleFilesFromUris(@NonNull List uris) { + if (uris.isEmpty()) return; + if (uris.size() == 1) { + Uri singleUri = uris.get(0); + String fileName = getFileNameFromUri(singleUri); + String content = readContentsOfUri(singleUri); + if (content != null && !content.trim().isEmpty()) { + importMonsterFromInputAndNavigate(content, fileName); + } else { + ToastHelper.showLong(this, R.string.failed_to_import_url); + } return; } ToastHelper.showShort(this, R.string.toast_importing_url); - Single.fromCallable(() -> MonsterImportHelper.fromJSON(input)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(monster -> { - String serializedJson = new Gson().toJson(monster); - NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment)); - NavController navController = navHostFragment.getNavController(); - NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson); - navController.navigate(navAction); - }, throwable -> { - Logger.logError("Failed to import monster from input", throwable); - ToastHelper.showLong(this, R.string.failed_to_import_url); - }); + Single.fromCallable(() -> { + int monstersImported = 0; + int bindersImported = 0; + + for (Uri uri : uris) { + try { + String fileName = getFileNameFromUri(uri); + String content = readContentsOfUri(uri); + if (content == null || content.trim().isEmpty()) { + continue; + } + + BinderImporter binderImporter = new BinderImporter(); + if (binderImporter.canImport(content)) { + BinderExport binder = binderImporter.parse(content); + ((MonsterCardsApplication) getApplication()).getMonsterRepository() + .importBinder(binder) + .blockingAwait(); + bindersImported++; + } else { + Monster monster = MonsterImportHelper.fromJSON(content, fileName); + ((MonsterCardsApplication) getApplication()).getMonsterRepository() + .saveMonster(monster) + .blockingAwait(); + monstersImported++; + } + } catch (Exception e) { + Logger.logError("Failed to import file URI: " + uri, e); + } + } + + return new BatchImportResult(monstersImported, bindersImported); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result.bindersImported > 0 || result.monstersImported > 0) { + String message = buildImportSummaryMessage(result.monstersImported, result.bindersImported); + ToastHelper.showLong(this, message); + NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment)); + NavController navController = navHostFragment.getNavController(); + navController.navigate(R.id.navigation_library); + } else { + ToastHelper.showLong(this, R.string.failed_to_import_url); + } + }, throwable -> { + Logger.logError("Failed to execute batch file import", throwable); + ToastHelper.showLong(this, R.string.failed_to_import_url); + }); + } + + private String buildImportSummaryMessage(int monstersCount, int bindersCount) { + StringBuilder sb = new StringBuilder("Successfully imported "); + if (bindersCount > 0 && monstersCount > 0) { + sb.append(bindersCount).append(bindersCount == 1 ? " collection" : " collections") + .append(" and ") + .append(monstersCount).append(monstersCount == 1 ? " monster" : " monsters"); + } else if (bindersCount > 0) { + sb.append(bindersCount).append(bindersCount == 1 ? " collection" : " collections"); + } else { + sb.append(monstersCount).append(monstersCount == 1 ? " monster" : " monsters"); + } + return sb.toString(); + } + + private static class BatchImportResult { + final int monstersImported; + final int bindersImported; + + BatchImportResult(int monstersImported, int bindersImported) { + this.monstersImported = monstersImported; + this.bindersImported = bindersImported; + } } @Nullable diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/CollectionDAO.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/CollectionDAO.java index c687649..fd95fbc 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/CollectionDAO.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/CollectionDAO.java @@ -47,9 +47,18 @@ public interface CollectionDAO { @Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId AND monster_id = :monsterId") Completable removeMonsterFromCollection(String collectionId, String monsterId); + @Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId") + Completable removeAllMonstersFromCollection(String collectionId); + @Query("DELETE FROM collection_monsters WHERE id = :id") Completable removeCollectionMonsterById(long id); + @Query("DELETE FROM collection_monsters") + Completable deleteAllCollectionMonsters(); + + @Query("DELETE FROM collections") + Completable deleteAllCollections(); + @Update Completable updateCollectionMonsters(List collectionMonsters); } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java index 9671790..6ccba4c 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java @@ -36,4 +36,7 @@ public interface MonsterDAO { @Delete Completable delete(Monster monster); + + @Query("DELETE FROM monsters") + Completable deleteAllMonsters(); } 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 5acb685..b58a161 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 @@ -9,18 +9,22 @@ import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.CollectionMonster; import com.majinnaibu.monstercards.models.CollectionWithCount; import com.majinnaibu.monstercards.models.DashboardMonster; +import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.SearchResultItem; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; @SuppressWarnings("ResultOfMethodCallIgnored") @@ -264,6 +268,91 @@ public class MonsterRepository { return result; } + public Completable clearAllData() { + return Completable.fromAction(() -> { + m_db.dashboardDAO().clearDashboard().blockingAwait(); + m_db.collectionDAO().deleteAllCollectionMonsters().blockingAwait(); + m_db.collectionDAO().deleteAllCollections().blockingAwait(); + m_db.monsterDAO().deleteAllMonsters().blockingAwait(); + }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + } + + public Completable removeAllCollections() { + return Completable.fromAction(() -> { + m_db.collectionDAO().deleteAllCollectionMonsters().blockingAwait(); + m_db.collectionDAO().deleteAllCollections().blockingAwait(); + }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + } + + public Completable removeAllMonstersFromCollection(@NonNull UUID collectionId) { + Completable result = m_db.collectionDAO().removeAllMonstersFromCollection(collectionId.toString()); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + + public Single exportCollections() { + return m_db.collectionDAO().getAll().first(new ArrayList<>()) + .map(collections -> { + List colExports = new ArrayList<>(); + for (Collection col : collections) { + List colMonsters = m_db.collectionDAO().getMonstersForCollection(col.id.toString()) + .first(new ArrayList<>()).blockingGet(); + colExports.add(new BinderExport.CollectionExport( + col.id.toString(), + col.name, + col.description, + colMonsters + )); + } + return new com.majinnaibu.monstercards.exporters.BinderExporter().exportFullBackup(colExports, null); + }) + .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + } + + public Single exportEverything() { + return Single.zip( + m_db.collectionDAO().getAll().first(new ArrayList<>()), + m_db.monsterDAO().getAll().first(new ArrayList<>()), + m_db.dashboardDAO().getDashboardMonsters().first(new ArrayList<>()), + (collections, allMonsters, dashboardMonsters) -> { + List colExports = new ArrayList<>(); + Set monstersInCollections = new HashSet<>(); + + for (Collection col : collections) { + List colMonsters = m_db.collectionDAO().getMonstersForCollection(col.id.toString()) + .first(new ArrayList<>()).blockingGet(); + for (Monster m : colMonsters) { + monstersInCollections.add(m.id); + } + colExports.add(new BinderExport.CollectionExport( + col.id.toString(), + col.name, + col.description, + colMonsters + )); + } + + List uncategorized = new ArrayList<>(); + for (Monster m : allMonsters) { + if (!monstersInCollections.contains(m.id)) { + uncategorized.add(m); + } + } + + if (!uncategorized.isEmpty() || colExports.isEmpty()) { + colExports.add(new BinderExport.CollectionExport( + UUID.randomUUID().toString(), + colExports.isEmpty() ? "All Monsters" : "Uncategorized", + "", + colExports.isEmpty() ? allMonsters : uncategorized + )); + } + + return new com.majinnaibu.monstercards.exporters.BinderExporter().exportFullBackup(colExports, dashboardMonsters); + } + ).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + } + public Completable updateDashboardMonsters(List items) { Completable result = m_db.dashboardDAO().updateDashboardMonsters(items); result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); @@ -272,65 +361,164 @@ public class MonsterRepository { public Completable importBinder(@NonNull BinderExport binder) { return Completable.fromAction(() -> { - if (binder.collections == null || binder.collections.isEmpty()) { - return; - } + // 1. Process Collections & Cards in Binder + if (binder.collections != null && !binder.collections.isEmpty()) { + List existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet(); - 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(); + for (BinderExport.CollectionExport colExport : binder.collections) { + if (colExport == null) { + continue; + } + + String colName = colExport.name != null ? colExport.name.trim() : ""; + String colDesc = colExport.description != null ? colExport.description.trim() : ""; + + UUID colExportId = null; + if (colExport.id != null && !colExport.id.trim().isEmpty()) { + try { + colExportId = UUID.fromString(colExport.id.trim()); + } catch (Exception ignored) { + } } - 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; + if (colExportId != null) { + for (Collection existing : existingCols) { + if (existing.id.equals(colExportId)) { + targetCollection = existing; + break; + } + } + } + if (targetCollection == null && !colName.isEmpty()) { + for (Collection existing : existingCols) { + if (existing.name != null && existing.name.equalsIgnoreCase(colName)) { + targetCollection = existing; + break; + } } } UUID targetCollectionId; if (targetCollection != null) { targetCollectionId = targetCollection.id; + if (!colDesc.isEmpty() && (targetCollection.description == null || targetCollection.description.isEmpty())) { + targetCollection.description = colDesc; + m_db.collectionDAO().save(targetCollection).blockingAwait(); + } } else { Collection newCol = new Collection(); - newCol.id = UUID.randomUUID(); - newCol.name = colName; + newCol.id = colExportId != null ? colExportId : UUID.randomUUID(); + newCol.name = !colName.isEmpty() ? colName : "Imported Collection"; + newCol.description = colDesc; m_db.collectionDAO().save(newCol).blockingAwait(); targetCollectionId = newCol.id; + existingCols.add(newCol); } - List colMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString()) - .first(new ArrayList<>()).blockingGet(); - Set existingMonsterIds = new HashSet<>(); - for (Monster m : colMonsters) { - existingMonsterIds.add(m.id); - } + if (colExport.cards != null && !colExport.cards.isEmpty()) { + List monstersToSave = new ArrayList<>(); + Map importedCardCounts = new HashMap<>(); - 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); + for (Monster card : colExport.cards) { + if (card != null) { + if (card.id == null) { + card.id = UUID.randomUUID(); + } + monstersToSave.add(card); + importedCardCounts.put(card.id, getCount(importedCardCounts, card.id) + 1); + } + } + + if (!monstersToSave.isEmpty()) { + m_db.monsterDAO().save(monstersToSave.toArray(new Monster[0])).blockingAwait(); + + List existingColMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString()) + .first(new ArrayList<>()).blockingGet(); + Map existingCardCounts = new HashMap<>(); + for (Monster m : existingColMonsters) { + existingCardCounts.put(m.id, getCount(existingCardCounts, m.id) + 1); + } + + int ordinal = existingColMonsters.size(); + for (Map.Entry entry : importedCardCounts.entrySet()) { + UUID monsterId = entry.getKey(); + int importedCount = entry.getValue(); + int existingCount = getCount(existingCardCounts, monsterId); + + if (importedCount > existingCount) { + int extraCopiesNeeded = importedCount - existingCount; + for (int k = 0; k < extraCopiesNeeded; k++) { + m_db.collectionDAO().addMonsterToCollection(new CollectionMonster(targetCollectionId, monsterId, ordinal++)).blockingAwait(); + } + } + } } } } } + + // 2. Process Dashboard entries in Binder if present + if (binder.dashboard != null && !binder.dashboard.isEmpty()) { + List dashboardMonstersToSave = new ArrayList<>(); + Map importedDashboardCounts = new HashMap<>(); + + for (Monster card : binder.dashboard) { + if (card != null) { + if (card.id == null) { + card.id = UUID.randomUUID(); + } + dashboardMonstersToSave.add(card); + importedDashboardCounts.put(card.id, getCount(importedDashboardCounts, card.id) + 1); + } + } + + if (!dashboardMonstersToSave.isEmpty()) { + m_db.monsterDAO().save(dashboardMonstersToSave.toArray(new Monster[0])).blockingAwait(); + + List existingDashboard = m_db.dashboardDAO().getDashboardMonstersWithMonster() + .first(new ArrayList<>()).blockingGet(); + Map existingDashboardCounts = new HashMap<>(); + int currentMaxOrdinal = 0; + for (DashboardMonsterWithMonster entry : existingDashboard) { + if (entry.monster != null) { + existingDashboardCounts.put(entry.monster.id, getCount(existingDashboardCounts, entry.monster.id) + 1); + } + if (entry.dashboardEntry != null && entry.dashboardEntry.ordinal > currentMaxOrdinal) { + currentMaxOrdinal = entry.dashboardEntry.ordinal; + } + } + + int ordinal = currentMaxOrdinal + 1; + List extraEntriesToAdd = new ArrayList<>(); + + for (Map.Entry entry : importedDashboardCounts.entrySet()) { + UUID monsterId = entry.getKey(); + int importedCount = entry.getValue(); + int existingCount = getCount(existingDashboardCounts, monsterId); + + if (importedCount > existingCount) { + int extraNeeded = importedCount - existingCount; + for (int k = 0; k < extraNeeded; k++) { + extraEntriesToAdd.add(new DashboardMonster(monsterId, ordinal++)); + } + } + } + + if (!extraEntriesToAdd.isEmpty()) { + m_db.dashboardDAO().addMonsterToDashboard(extraEntriesToAdd.toArray(new DashboardMonster[0])).blockingAwait(); + } + } + } }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); } + private static int getCount(Map map, UUID key) { + if (map == null || key == null) return 0; + Integer val = map.get(key); + return val != null ? val : 0; + } + 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/data/enums/AbilityScore.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java index 5a0b24e..d43b91f 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java @@ -21,11 +21,23 @@ public enum AbilityScore { } public static AbilityScore valueOfString(String string) { + if (string == null) { + return AbilityScore.STRENGTH; + } + String lower = string.trim().toLowerCase(java.util.Locale.ROOT); for (AbilityScore abilityScore : values()) { - if (abilityScore.stringValue.equals(string)) { + if (abilityScore.stringValue.equalsIgnoreCase(lower) + || abilityScore.shortDisplayName.equalsIgnoreCase(lower) + || abilityScore.name().equalsIgnoreCase(lower)) { return abilityScore; } } + if (lower.startsWith("str")) return STRENGTH; + if (lower.startsWith("dex")) return DEXTERITY; + if (lower.startsWith("con")) return CONSTITUTION; + if (lower.startsWith("int")) return INTELLIGENCE; + if (lower.startsWith("wis")) return WISDOM; + if (lower.startsWith("cha")) return CHARISMA; return AbilityScore.STRENGTH; } } 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 index b988333..28feb97 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/BinderExporter.java @@ -1,34 +1,94 @@ package com.majinnaibu.monstercards.exporters; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.majinnaibu.monstercards.models.BinderExport; +import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.Monster; import java.util.Collections; import java.util.List; +import java.util.UUID; public class BinderExporter { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); @NonNull public String exportBinder(String collectionName, List monsters) { + return exportBinder(new Collection(collectionName != null ? collectionName : "", ""), monsters, null); + } + + @NonNull + public String exportBinder(@Nullable Collection collection, List monsters) { + return exportBinder(collection, monsters, null); + } + + @NonNull + public String exportBinder(@Nullable Collection collection, @Nullable List monsters, @Nullable List dashboardMonsters) { BinderExport export = new BinderExport(); + export.schema = "https://majinnaibu.com/schemas/binder.schema.json"; export.schemaVersion = 1; if (monsters != null) { for (Monster monster : monsters) { + monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json"; monster.schemaVersion = 1; } } - BinderExport.CollectionExport col = new BinderExport.CollectionExport( - collectionName != null ? collectionName : "", - monsters - ); - export.collections = Collections.singletonList(col); + if (dashboardMonsters != null && !dashboardMonsters.isEmpty()) { + for (Monster monster : dashboardMonsters) { + monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json"; + monster.schemaVersion = 1; + } + export.dashboard = dashboardMonsters; + } + + if (collection != null) { + String colId = collection.id.toString(); + String colName = collection.name; + String colDesc = collection.description; + + BinderExport.CollectionExport col = new BinderExport.CollectionExport( + colId, + colName, + colDesc, + monsters + ); + export.collections = Collections.singletonList(col); + } + return GSON.toJson(export); + } + + @NonNull + public String exportFullBackup(@Nullable List collections, @Nullable List dashboardMonsters) { + BinderExport export = new BinderExport(); + export.schema = "https://majinnaibu.com/schemas/binder.schema.json"; + export.schemaVersion = 1; + + if (collections != null) { + for (BinderExport.CollectionExport col : collections) { + if (col.cards != null) { + for (Monster m : col.cards) { + m.schema = "https://majinnaibu.com/schemas/monster-card.schema.json"; + m.schemaVersion = 1; + } + } + } + export.collections = collections; + } + + if (dashboardMonsters != null && !dashboardMonsters.isEmpty()) { + for (Monster m : dashboardMonsters) { + m.schema = "https://majinnaibu.com/schemas/monster-card.schema.json"; + m.schemaVersion = 1; + } + export.dashboard = dashboardMonsters; + } + 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 index 96bd982..660a5db 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/exporters/MonsterCardExporter.java @@ -11,6 +11,7 @@ public class MonsterCardExporter { @NonNull public String exportCard(@NonNull Monster monster) { + monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json"; monster.schemaVersion = 1; return GSON.toJson(monster); } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java index 1d3c080..ae6fd02 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java @@ -1,39 +1,85 @@ package com.majinnaibu.monstercards.helpers; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.majinnaibu.monstercards.importers.DnDBeyondImporter; import com.majinnaibu.monstercards.importers.EntityImporter; import com.majinnaibu.monstercards.importers.MonsterJsonImporter; import com.majinnaibu.monstercards.importers.Open5eImporter; import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter; import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.utils.Logger; import java.util.ArrayList; import java.util.List; +import java.util.Locale; public class MonsterImportHelper { private static final List> IMPORTERS = new ArrayList<>(); static { + IMPORTERS.add(new TetraCubeMonsterImporter()); IMPORTERS.add(new MonsterJsonImporter()); IMPORTERS.add(new Open5eImporter()); - IMPORTERS.add(new TetraCubeMonsterImporter()); IMPORTERS.add(new DnDBeyondImporter()); } @NonNull public static Monster fromJSON(String json) { - if (json != null) { - for (EntityImporter importer : IMPORTERS) { - if (importer.canImport(json)) { - try { - return importer.parse(json); - } catch (Exception e) { - throw new IllegalArgumentException("Failed to parse monster JSON", e); + return fromJSON(json, null); + } + + @NonNull + public static Monster fromJSON(String json, @Nullable String fileName) { + if (json == null || json.trim().isEmpty()) { + throw new IllegalArgumentException("JSON payload is null or empty"); + } + + // 1. Check for explicit $schema reference matching monster-card or tetracube schema + try { + JsonElement el = JsonParser.parseString(json); + if (el.isJsonObject()) { + JsonObject obj = el.getAsJsonObject(); + if (obj.has("$schema")) { + String schemaVal = obj.get("$schema").getAsString(); + if (schemaVal.contains("monster-card.schema.json")) { + return new MonsterJsonImporter().parse(json); + } else if (schemaVal.contains("tetracube-monster.schema.json")) { + return new TetraCubeMonsterImporter().parse(json); } } } + } catch (Exception e) { + Logger.logError("Error checking $schema reference tag", e); + } + + // 2. If filename ends in .monster, try TetraCube importer first + String lowerFileName = fileName != null ? fileName.toLowerCase(Locale.ROOT) : ""; + boolean isMonsterFile = lowerFileName.endsWith(".monster") || lowerFileName.endsWith(".monster.txt"); + if (isMonsterFile) { + TetraCubeMonsterImporter tetraImporter = new TetraCubeMonsterImporter(); + if (tetraImporter.canImport(json)) { + try { + return tetraImporter.parse(json); + } catch (Exception e) { + Logger.logError("TetraCube importer failed for .monster file: " + fileName, e); + } + } + } + + // 3. Fallback to current importer detection chain + for (EntityImporter importer : IMPORTERS) { + if (importer.canImport(json)) { + try { + return importer.parse(json); + } catch (Exception e) { + Logger.logError("Importer " + importer.getClass().getSimpleName() + " failed", e); + } + } } throw new IllegalArgumentException("No importer found capable of parsing the given JSON payload."); } 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 index d2f97df..3d49526 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/BinderImporter.java @@ -18,6 +18,12 @@ public class BinderImporter implements EntityImporter { JsonElement el = JsonParser.parseString(input); if (el.isJsonObject()) { JsonObject obj = el.getAsJsonObject(); + if (obj.has("$schema")) { + String schemaVal = obj.get("$schema").getAsString(); + if (schemaVal.contains("binder.schema.json")) { + return true; + } + } return obj.has("collections") && obj.get("collections").isJsonArray(); } } catch (Exception ignored) { diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/MonsterJsonImporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/MonsterJsonImporter.java index adc00a9..beb7284 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/MonsterJsonImporter.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/MonsterJsonImporter.java @@ -18,7 +18,13 @@ public class MonsterJsonImporter implements EntityImporter { JsonElement el = JsonParser.parseString(input); if (el.isJsonObject()) { JsonObject obj = el.getAsJsonObject(); - return obj.has("name") && (obj.has("strengthScore") || obj.has("hitDice") || obj.has("size")); + if (obj.has("$schema")) { + String schemaVal = obj.get("$schema").getAsString(); + if (schemaVal.contains("monster-card.schema.json")) { + return true; + } + } + return obj.has("name") && (obj.has("strengthScore") || obj.has("strengthSavingThrowProficiency")); } } catch (Exception ignored) { } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java index 967bee2..63dc39b 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/TetraCubeMonsterImporter.java @@ -36,6 +36,12 @@ public class TetraCubeMonsterImporter implements EntityImporter { return false; } JsonObject rootDict = element.getAsJsonObject(); + if (rootDict.has("$schema")) { + String schemaVal = rootDict.get("$schema").getAsString(); + if (schemaVal.contains("tetracube-monster.schema.json")) { + return true; + } + } return rootDict.has("armorName") || rootDict.has("strPoints") || rootDict.has("isLegendary") || rootDict.has("isMythic") || rootDict.has("legendariesDescription"); 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 index f9fcc00..df9c36e 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/BinderExport.java @@ -4,27 +4,46 @@ import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class BinderExport { + @SerializedName("$schema") + public String schema = "https://majinnaibu.com/schemas/binder.schema.json"; + @SerializedName("schemaVersion") public int schemaVersion = 1; @SerializedName("collections") public List collections = new ArrayList<>(); + @SerializedName("dashboard") + public List dashboard = new ArrayList<>(); + public static class CollectionExport { + @SerializedName("id") + public String id; + @SerializedName("name") public String name; + @SerializedName("description") + public String description; + @SerializedName("cards") public List cards = new ArrayList<>(); public CollectionExport() { } - public CollectionExport(String name, List cards) { + public CollectionExport(String id, String name, String description, List cards) { + this.id = id != null ? id : UUID.randomUUID().toString(); this.name = name != null ? name : ""; + this.description = description != null ? description : ""; this.cards = cards != null ? cards : new ArrayList<>(); } + + public CollectionExport(String name, List cards) { + this(UUID.randomUUID().toString(), name, "", cards); + } } } 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 75c29bf..326bb54 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 @@ -9,6 +9,7 @@ import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; +import com.google.gson.annotations.SerializedName; import com.majinnaibu.monstercards.data.enums.AbilityScore; import com.majinnaibu.monstercards.data.enums.AdvantageType; import com.majinnaibu.monstercards.data.enums.ArmorType; @@ -30,11 +31,16 @@ import java.util.UUID; @SuppressWarnings("unused") public class Monster { + @SerializedName("$schema") + @Ignore + public String schema = "https://majinnaibu.com/schemas/monster-card.schema.json"; + @Ignore public int schemaVersion = 1; @PrimaryKey @NonNull + @SerializedName("id") public UUID id; @NonNull @@ -438,7 +444,10 @@ public class Monster { } } - public int getAbilityModifier(@NonNull AbilityScore abilityScore) { + public int getAbilityModifier(@Nullable AbilityScore abilityScore) { + if (abilityScore == null) { + return 0; + } switch (abilityScore) { case STRENGTH: return getStrengthModifier(); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java index 9730cc5..5e202f4 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java @@ -34,13 +34,25 @@ public class Skill implements Comparator, Comparable { this.proficiencyType = proficiencyType; } + public AbilityScore getAbilityScore() { + if (abilityScore != null) { + return abilityScore; + } + if (name != null) { + return AbilityScore.valueOfString(name); + } + return AbilityScore.STRENGTH; + } + public int getSkillBonus(Monster monster) { - int modifier = monster.getAbilityModifier(abilityScore); - switch (proficiencyType) { + AbilityScore score = getAbilityScore(); + int modifier = monster != null ? monster.getAbilityModifier(score) : 0; + ProficiencyType prof = proficiencyType != null ? proficiencyType : ProficiencyType.PROFICIENT; + switch (prof) { case PROFICIENT: - return modifier + monster.getProficiencyBonus(); + return modifier + (monster != null ? monster.getProficiencyBonus() : 0); case EXPERTISE: - return modifier + monster.getProficiencyBonus() * 2; + return modifier + (monster != null ? monster.getProficiencyBonus() * 2 : 0); case NONE: default: return modifier; 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 c8cc731..d73e0d6 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 @@ -221,10 +221,32 @@ public class CollectionDetailFragment extends MCFragment { } else if (item.getItemId() == R.id.menu_action_export_collection) { exportCollection(); return true; + } else if (item.getItemId() == R.id.menu_action_remove_all_monsters_from_collection) { + showRemoveAllMonstersConfirmationDialog(); + return true; } return super.onOptionsItemSelected(item); } + private void showRemoveAllMonstersConfirmationDialog() { + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_remove_all_from_collection_title) + .setMessage(R.string.dialog_remove_all_from_collection_message) + .setPositiveButton(R.string.action_remove_all, (dialog, which) -> { + mDisposables.add(getMonsterRepository().removeAllMonstersFromCollection(mCollectionId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + SnackbarHelper.showLong(view, R.string.snackbar_collection_cleared); + } + }, throwable -> Logger.logError("Failed to remove all monsters from collection", throwable))); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + } + private void exportCollection() { Collection collection = mViewModel.getCollection().getValue(); List monsters = mViewModel.getMonsters().getValue(); 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 4cfae01..3262ae9 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 @@ -4,6 +4,9 @@ import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; @@ -43,6 +46,7 @@ public class CollectionsFragment extends MCFragment { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + setHasOptionsMenu(true); View root = inflater.inflate(R.layout.fragment_collections, container, false); FloatingActionButton fab = root.findViewById(R.id.fab_add_collection); @@ -190,6 +194,51 @@ public class CollectionsFragment extends MCFragment { Navigation.findNavController(requireView()).navigate(action); } + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.collections_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.menu_action_export_collections) { + exportCollections(); + return true; + } else if (item.getItemId() == R.id.menu_action_remove_all_collections) { + showRemoveAllCollectionsConfirmationDialog(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void exportCollections() { + mDisposables.add(getMonsterRepository().exportCollections() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(json -> exportToFile("collections.binder", json), + throwable -> Logger.logError("Failed to export collections", throwable))); + } + + private void showRemoveAllCollectionsConfirmationDialog() { + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_remove_all_collections_title) + .setMessage(R.string.dialog_remove_all_collections_message) + .setPositiveButton(R.string.action_remove_all, (dialog, which) -> { + mDisposables.add(getMonsterRepository().removeAllCollections() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + SnackbarHelper.showLong(view, R.string.snackbar_all_collections_removed); + } + }, throwable -> Logger.logError("Failed to remove all collections", throwable))); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + } + @Override public void onDestroyView() { super.onDestroyView(); 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 153ed3f..64b01c2 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 @@ -24,6 +24,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.models.Collection; +import com.majinnaibu.monstercards.models.CollectionWithCount; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.utils.Logger; @@ -265,7 +266,7 @@ public class DashboardFragment extends MCFragment { private void showAddCollectionPicker() { MonsterRepository repository = getMonsterRepository(); - mDisposables.add(repository.getCollections() + mDisposables.add(repository.getCollectionsWithCount() .firstOrError() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -279,12 +280,16 @@ public class DashboardFragment extends MCFragment { } String[] names = new String[collections.size()]; for (int i = 0; i < collections.size(); i++) { - names[i] = collections.get(i).name; + CollectionWithCount item = collections.get(i); + int count = item.monsterCount; + String countText = count == 1 ? "1 monster" : count + " monsters"; + String colName = item.collection != null && item.collection.name != null ? item.collection.name : ""; + names[i] = colName + " (" + countText + ")"; } new AlertDialog.Builder(requireContext()) .setTitle(R.string.action_add_collection_option) .setItems(names, (dialog, which) -> { - Collection selected = collections.get(which); + Collection selected = collections.get(which).collection; addCollectionToDashboard(selected); }) .setNegativeButton(R.string.dialog_cancel, null) @@ -365,9 +370,6 @@ 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/library/LibraryFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java index 6c6166d..a10b683 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 @@ -85,10 +85,35 @@ public class LibraryFragment extends MCFragment { } else if (item.getItemId() == R.id.menu_action_export_library) { exportLibrary(); return true; + } else if (item.getItemId() == R.id.menu_action_export_everything) { + exportEverything(); + return true; + } else if (item.getItemId() == R.id.menu_action_clear_all_data) { + showClearAllDataConfirmationDialog(); + return true; } return super.onOptionsItemSelected(item); } + private void showClearAllDataConfirmationDialog() { + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_clear_all_data_title) + .setMessage(R.string.dialog_clear_all_data_message) + .setPositiveButton(R.string.action_clear, (dialog, which) -> { + mDisposables.add(getMonsterRepository().clearAllData() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + SnackbarHelper.showLong(view, R.string.snackbar_all_data_cleared); + } + }, throwable -> Logger.logError("Failed to clear all data", throwable))); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + } + private void exportLibrary() { getMonsterRepository().getMonsters() .firstOrError() @@ -101,6 +126,16 @@ public class LibraryFragment extends MCFragment { }, Logger::logError); } + private void exportEverything() { + mDisposables.add(getMonsterRepository().exportEverything() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(json -> { + String fileName = getString(R.string.default_filename_export_everything) + ".binder"; + exportToFile(fileName, json); + }, throwable -> Logger.logError("Failed to export everything", throwable))); + } + private void setupRecyclerView(@NonNull RecyclerView recyclerView, @Nullable View emptyState) { Context context = requireContext(); MonsterRepository repository = this.getMonsterRepository(); 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 bd032cf..b65b202 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 @@ -40,6 +40,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.UUID; @@ -71,17 +73,22 @@ public class MCFragment extends Fragment { new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - Uri uri = result.getData().getData(); - if (uri != null && getActivity() instanceof MainActivity) { - String content = readContentsFromUri(uri); - if (content != null && !content.trim().isEmpty()) { - ((MainActivity) getActivity()).importMonsterFromInputAndNavigate(content); - } else { - View view = getView(); - if (view != null) { - SnackbarHelper.showLong(view, R.string.failed_to_import_url); + Intent data = result.getData(); + List uris = new ArrayList<>(); + if (data.getClipData() != null) { + ClipData clipData = data.getClipData(); + for (int i = 0; i < clipData.getItemCount(); i++) { + Uri uri = clipData.getItemAt(i).getUri(); + if (uri != null) { + uris.add(uri); } } + } else if (data.getData() != null) { + uris.add(data.getData()); + } + + if (!uris.isEmpty() && getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).importMultipleFilesFromUris(uris); } } }); @@ -91,6 +98,7 @@ public class MCFragment extends Fragment { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); try { mOpenDocumentLauncher.launch(intent); } catch (Exception e) { 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 02cd759..48376a0 100644 --- a/Android/app/src/main/res/menu/collection_detail_menu.xml +++ b/Android/app/src/main/res/menu/collection_detail_menu.xml @@ -11,4 +11,9 @@ android:id="@+id/menu_action_export_collection" android:title="@string/action_export_collection" app:showAsAction="never" /> + + diff --git a/Android/app/src/main/res/menu/collections_menu.xml b/Android/app/src/main/res/menu/collections_menu.xml new file mode 100644 index 0000000..a312811 --- /dev/null +++ b/Android/app/src/main/res/menu/collections_menu.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/Android/app/src/main/res/menu/dashboard_menu.xml b/Android/app/src/main/res/menu/dashboard_menu.xml index 8d6eacf..23180bc 100644 --- a/Android/app/src/main/res/menu/dashboard_menu.xml +++ b/Android/app/src/main/res/menu/dashboard_menu.xml @@ -12,11 +12,6 @@ android:title="@string/action_add_collection_option" app:showAsAction="never" /> - - + + + + diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index 3c343c8..04e2523 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -11,7 +11,7 @@ Add Skill Add Trait Edit - Import Monster + Import File MonsterCards CHA CON @@ -133,7 +133,7 @@ Strings Trait Traits - Import Monster + Import File Library Monster Details %1$s Details @@ -156,25 +156,25 @@ Add Collection to Dashboard Add Monster Add Collection - Clear Dashboard + Clear Add to Dashboard Added %1$s to Dashboard Added collection %1$s to Dashboard Dashboard cleared No monsters available in library. Create one first! - Import URL... - Import Entity or Character + Import URL + Import Character Enter or paste a D&D Beyond URL, Character ID, or Open5e JSON payload: e.g. D&D Beyond URL or Open5e JSON Import - Importing entity... - Failed to import entity. Please check the URL, ID, or JSON format. + Importing… + Failed to import URL. Please check the URL, ID, or JSON format. Share / Export Monster (.card) Failed to share monster Export Card Export Collection Export Library - Export Dashboard + Export Exported to %1$s Failed to export file Imported binder successfully @@ -189,7 +189,7 @@ Create new monster cards or import them from D&D Beyond or Open5e. Monster Cards - Remove Monster... + Remove Monster Remove Undo View Details @@ -200,10 +200,29 @@ Remove %1$s from your dashboard? Create Monster - Import Monster from Url - Import Monster from File - Import Collection + Import URL + Import File + Import File Library Actions Collection Actions Dashboard Actions + + Clear + Clear All Data + Clear All Data? + This will permanently delete all monsters, collections, and clear your dashboard. Are you sure? + All data cleared + + Export All Data + monster_cards_backup + + Export + Remove All + Remove All Collections? + Are you sure you want to remove all collections? Monsters in your library will not be deleted. + All collections removed + + Remove All Monsters? + Are you sure you want to remove all monsters from this collection? + Removed all monsters from collection diff --git a/monsters/Unnamed Monster.card b/monsters/Unnamed Monster.card new file mode 100644 index 0000000..ae2c145 --- /dev/null +++ b/monsters/Unnamed Monster.card @@ -0,0 +1,84 @@ +{ + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 +} \ No newline at end of file diff --git a/monsters/baddies (1).binder b/monsters/baddies (1).binder new file mode 100644 index 0000000..e4f724a --- /dev/null +++ b/monsters/baddies (1).binder @@ -0,0 +1,514 @@ +{ + "collections": [ + { + "cards": [ + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + } + ], + "name": "baddies" + } + ], + "schemaVersion": 1 +} \ No newline at end of file diff --git a/monsters/baddies.binder b/monsters/baddies.binder new file mode 100644 index 0000000..e4f724a --- /dev/null +++ b/monsters/baddies.binder @@ -0,0 +1,514 @@ +{ + "collections": [ + { + "cards": [ + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + }, + { + "abilities": [], + "actions": [], + "age": "", + "alignment": "", + "alliesAndOrganizations": "", + "appearance": "", + "armorType": "NONE", + "background": "", + "backstory": "", + "bonds": "", + "bonusActions": [], + "burrowSpeed": 0, + "canHover": false, + "challengeRating": "ONE", + "charismaSavingThrowAdvantage": "NONE", + "charismaSavingThrowProficiency": "NONE", + "charismaScore": 10, + "climbSpeed": 0, + "conditionImmunities": [], + "constitutionSavingThrowAdvantage": "NONE", + "constitutionSavingThrowProficiency": "NONE", + "constitutionScore": 10, + "customChallengeRatingDescription": "", + "customHPDescription": "", + "customProficiencyBonus": 0, + "customSpeedDescription": "", + "damageImmunities": [], + "damageResistances": [], + "damageVulnerabilities": [], + "dexteritySavingThrowAdvantage": "NONE", + "dexteritySavingThrowProficiency": "NONE", + "dexterityScore": 10, + "eyes": "", + "flaws": "", + "flySpeed": 0, + "hair": "", + "hasCustomHP": false, + "hasCustomSpeed": false, + "height": "", + "hitDice": 1, + "id": "dc7d4d51-86f7-41c6-9e4d-fb187cfa047b", + "ideals": "", + "intelligenceSavingThrowAdvantage": "NONE", + "intelligenceSavingThrowProficiency": "NONE", + "intelligenceScore": 10, + "lairActions": [], + "lairActionsDescription": "", + "lairActionsEndNote": "", + "languages": [], + "legendaryActions": [], + "legendaryActionsDescription": "", + "mythicActions": [], + "mythicActionsDescription": "", + "name": "Unnamed Monster", + "naturalArmorBonus": 0, + "otherArmorDescription": "", + "personalityTraits": "", + "playerName": "", + "reactions": [], + "regionalActions": [], + "regionalActionsDescription": "", + "regionalActionsEndNote": "", + "schemaVersion": 1, + "senses": [], + "shieldBonus": 0, + "size": "", + "skills": [], + "skin": "", + "sourceUrl": "", + "strengthSavingThrowAdvantage": "NONE", + "strengthSavingThrowProficiency": "NONE", + "strengthScore": 10, + "subtype": "", + "swimSpeed": 0, + "telepathyRange": 0, + "type": "", + "understandsButDescription": "", + "walkSpeed": 0, + "weight": "", + "wisdomSavingThrowAdvantage": "NONE", + "wisdomSavingThrowProficiency": "NONE", + "wisdomScore": 10 + } + ], + "name": "baddies" + } + ], + "schemaVersion": 1 +} \ No newline at end of file diff --git a/schemas/binder.schema.json b/schemas/binder.schema.json new file mode 100644 index 0000000..02ef68c --- /dev/null +++ b/schemas/binder.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://majinnaibu.com/schemas/binder.schema.json", + "title": "BinderExport", + "description": "JSON Schema for MonsterCards .binder collection files", + "type": "object", + "required": [ + "collections", + "schemaVersion" + ], + "properties": { + "$schema": { + "type": "string", + "format": "uri" + }, + "schemaVersion": { + "type": "integer", + "minimum": 1 + }, + "collections": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "cards" + ], + "properties": { + "name": { + "type": "string" + }, + "cards": { + "type": "array" + } + } + } + }, + "dashboard": { + "type": "array" + } + } +} diff --git a/schemas/monster-card.schema.json b/schemas/monster-card.schema.json new file mode 100644 index 0000000..af01e44 --- /dev/null +++ b/schemas/monster-card.schema.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://majinnaibu.com/schemas/monster-card.schema.json", + "title": "MonsterCard", + "description": "JSON Schema for MonsterCards .card files", + "type": "object", + "required": [ + "name", + "schemaVersion" + ], + "properties": { + "$schema": { + "type": "string", + "format": "uri" + }, + "schemaVersion": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "size": { + "type": "string" + }, + "type": { + "type": "string" + }, + "subtype": { + "type": "string" + }, + "alignment": { + "type": "string" + }, + "strengthScore": { + "type": "integer" + }, + "dexterityScore": { + "type": "integer" + }, + "constitutionScore": { + "type": "integer" + }, + "intelligenceScore": { + "type": "integer" + }, + "wisdomScore": { + "type": "integer" + }, + "charismaScore": { + "type": "integer" + }, + "hitDice": { + "type": "integer" + }, + "walkSpeed": { + "type": "integer" + }, + "burrowSpeed": { + "type": "integer" + }, + "climbSpeed": { + "type": "integer" + }, + "flySpeed": { + "type": "integer" + }, + "swimSpeed": { + "type": "integer" + }, + "abilities": { + "type": "array" + }, + "actions": { + "type": "array" + }, + "reactions": { + "type": "array" + }, + "legendaryActions": { + "type": "array" + } + } +} diff --git a/schemas/tetracube-monster.schema.json b/schemas/tetracube-monster.schema.json new file mode 100644 index 0000000..cfb855b --- /dev/null +++ b/schemas/tetracube-monster.schema.json @@ -0,0 +1,257 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://majinnaibu.com/schemas/tetracube-monster.schema.json", + "title": "TetraCubeMonster", + "description": "JSON Schema for TetraCube .monster files based on all_the_fields.monster", + "type": "object", + "required": [ + "name", + "size", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "size": { + "type": "string" + }, + "type": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "race": { + "type": "string" + }, + "class": { + "type": "string" + }, + "alignment": { + "type": "string" + }, + "armorName": { + "type": "string" + }, + "strPoints": { + "type": [ + "string", + "integer" + ] + }, + "dexPoints": { + "type": [ + "string", + "integer" + ] + }, + "conPoints": { + "type": [ + "string", + "integer" + ] + }, + "intPoints": { + "type": [ + "string", + "integer" + ] + }, + "wisPoints": { + "type": [ + "string", + "integer" + ] + }, + "chaPoints": { + "type": [ + "string", + "integer" + ] + }, + "hitDice": { + "type": [ + "string", + "integer" + ] + }, + "speed": { + "type": [ + "string", + "integer" + ] + }, + "burrowSpeed": { + "type": [ + "string", + "integer" + ] + }, + "climbSpeed": { + "type": [ + "string", + "integer" + ] + }, + "flySpeed": { + "type": [ + "string", + "integer" + ] + }, + "swimSpeed": { + "type": [ + "string", + "integer" + ] + }, + "cr": { + "type": "string" + }, + "customCr": { + "type": "string" + }, + "customProf": { + "type": "integer" + }, + "shieldBonus": { + "type": "integer" + }, + "natArmorBonus": { + "type": [ + "integer", + "null" + ] + }, + "otherArmorDesc": { + "type": "string" + }, + "hpText": { + "type": "string" + }, + "speedDesc": { + "type": "string" + }, + "customHP": { + "type": "boolean" + }, + "customSpeed": { + "type": "boolean" + }, + "hover": { + "type": "boolean" + }, + "blind": { + "type": "boolean" + }, + "blindsight": { + "type": [ + "string", + "integer" + ] + }, + "darkvision": { + "type": [ + "string", + "integer" + ] + }, + "tremorsense": { + "type": [ + "string", + "integer" + ] + }, + "truesight": { + "type": [ + "string", + "integer" + ] + }, + "telepathy": { + "type": "integer" + }, + "understandsBut": { + "type": "string" + }, + "shortName": { + "type": "string" + }, + "pluralName": { + "type": "string" + }, + "isLegendary": { + "type": "boolean" + }, + "legendariesDescription": { + "type": "string" + }, + "isMythic": { + "type": "boolean" + }, + "mythicDescription": { + "type": "string" + }, + "isLair": { + "type": "boolean" + }, + "lairDescription": { + "type": "string" + }, + "lairDescriptionEnd": { + "type": "string" + }, + "isRegional": { + "type": "boolean" + }, + "regionalDescription": { + "type": "string" + }, + "regionalDescriptionEnd": { + "type": "string" + }, + "abilities": { + "type": "array" + }, + "actions": { + "type": "array" + }, + "bonusActions": { + "type": "array" + }, + "reactions": { + "type": "array" + }, + "legendaries": { + "type": "array" + }, + "mythics": { + "type": "array" + }, + "lairs": { + "type": "array" + }, + "regionals": { + "type": "array" + }, + "skills": { + "type": "array" + }, + "sthrows": { + "type": "array" + }, + "languages": { + "type": "array" + }, + "conditions": { + "type": "array" + }, + "damagetypes": { + "type": "array" + }, + "specialdamage": { + "type": "array" + } + } +}