diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml
index b20232f..0250817 100644
--- a/Android/app/src/main/AndroidManifest.xml
+++ b/Android/app/src/main/AndroidManifest.xml
@@ -76,6 +76,16 @@
+
+
+
+
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 9158747..5dc7015 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
@@ -1,8 +1,16 @@
package com.majinnaibu.monstercards.ui.monster;
import android.content.Context;
+import android.content.Intent;
import android.content.res.Resources;
+import android.net.Uri;
import android.os.Bundle;
+
+import androidx.core.content.FileProvider;
+import com.majinnaibu.monstercards.exporters.Open5eExporter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.charset.StandardCharsets;
import android.text.Html;
import android.text.Spanned;
import android.util.DisplayMetrics;
@@ -184,10 +192,56 @@ public class MonsterDetailFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_add_to_dashboard) {
addCurrentMonsterToDashboard();
return true;
+ } else if (item.getItemId() == R.id.menu_action_share_monster) {
+ shareCurrentMonster();
+ return true;
}
return super.onOptionsItemSelected(item);
}
+ private void shareCurrentMonster() {
+ Monster monster = mViewModel.getMonster();
+ if (monster == null) {
+ return;
+ }
+
+ try {
+ Open5eExporter exporter = new Open5eExporter();
+ String jsonCard = exporter.exportMonster(monster);
+
+ Context context = requireContext();
+ String safeName = (monster.name != null && !monster.name.trim().isEmpty())
+ ? monster.name.replaceAll("[^a-zA-Z0-9._-]", "_")
+ : "monster";
+ File file = new File(context.getCacheDir(), safeName + ".card");
+
+ try (FileOutputStream out = new FileOutputStream(file)) {
+ out.write(jsonCard.getBytes(StandardCharsets.UTF_8));
+ }
+
+ Uri uri = FileProvider.getUriForFile(
+ context,
+ context.getPackageName() + ".fileprovider",
+ file
+ );
+
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.setType("application/octet-stream");
+ shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, jsonCard);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, monster.name);
+ shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.action_share_monster)));
+ } catch (Exception e) {
+ Logger.logError("Failed to share monster", e);
+ View view = getView();
+ if (view != null) {
+ Snackbar.make(view, R.string.failed_to_share_monster, Snackbar.LENGTH_LONG).show();
+ }
+ }
+ }
+
private void addCurrentMonsterToDashboard() {
UUID monsterId = mViewModel.getId().getValue();
if (monsterId == null) {
diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailViewModel.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailViewModel.java
index 38e5d13..27b0fb1 100644
--- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailViewModel.java
+++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailViewModel.java
@@ -181,6 +181,10 @@ public class MonsterDetailViewModel extends ViewModel {
return mMonsterId;
}
+ public Monster getMonster() {
+ return mMonster;
+ }
+
public void setMonster(@NonNull Monster monster) {
mMonster = monster;
mAbilities.setValue(mMonster.getAbilityDescriptions());
diff --git a/Android/app/src/main/res/drawable/ic_share_24.xml b/Android/app/src/main/res/drawable/ic_share_24.xml
new file mode 100644
index 0000000..9252e72
--- /dev/null
+++ b/Android/app/src/main/res/drawable/ic_share_24.xml
@@ -0,0 +1,10 @@
+
+
+
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 0dc658c..f084a1e 100644
--- a/Android/app/src/main/res/menu/monster_detail_menu.xml
+++ b/Android/app/src/main/res/menu/monster_detail_menu.xml
@@ -4,8 +4,15 @@
+ app:showAsAction="ifRoom" />
+
+
- Added collection %1$s to Dashboard
Dashboard cleared
No monsters available in library. Create one first!
- Import Entity / Character...
+ Import URL...
Import Entity or 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.
+ Share / Export Monster (.card)
+ Failed to share monster
diff --git a/Android/app/src/main/res/xml/file_paths.xml b/Android/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..62fb578
--- /dev/null
+++ b/Android/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Android/build.gradle b/Android/build.gradle
index 00d3989..12a758c 100644
--- a/Android/build.gradle
+++ b/Android/build.gradle
@@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:9.3.1'
+ classpath 'com.android.tools.build:gradle:9.3.2'
def nav_version = "2.9.8"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
diff --git a/docs/Import-Export.md b/docs/Import-Export.md
index 1bb5312..51bbcd1 100644
--- a/docs/Import-Export.md
+++ b/docs/Import-Export.md
@@ -250,7 +250,7 @@ Extend the generic sharing feature with specialized, direct sharing channels (to
- [x] **Step 3**: Import from D&D Beyond URL (`https://www.dndbeyond.com/characters/49074997` fetching from v5 character service endpoint).
- [x] **Step 4**: Export to internal format described by Open5e document (`Open5eExporter`).
- [x] **Step 5**: Import from internal format described by Open5e document (`Open5eImporter`).
-- [ ] **Step 6**: Generic Android Share button feature (`ACTION_SEND`).
+- [x] **Step 6**: Generic Android Share button feature (`ACTION_SEND`).
- [ ] **Step 7**: Specific share targets (7.1 NFC, 7.2 Bluetooth, 7.3 Embedded Web URL).
---
@@ -260,8 +260,8 @@ Extend the generic sharing feature with specialized, direct sharing channels (to
### 4.1 Schema Architecture Considerations
- **Unified Action Entity / Type Column**:
- Instead of maintaining separate `List` columns for each category (`actions`, `reactions`, `legendaryActions`, `lairActions`, `regionalActions`, etc.), a unified `monster_actions` table (or model list) with an `action_type` column (`ABILITY`, `ACTION`, `BONUS_ACTION`, `REACTION`, `LEGENDARY_ACTION`, `MYTHIC_ACTION`, `LAIR_ACTION`, `REGIONAL_EFFECT`) simplifies Room DB queries and enables a single reusable editor UI component.
-- **Section Intro & End Note Metadata**:
- - Store section-level intro text and end notes (e.g. `legendaryActionsDescription`, `lairActionsDescription`, `lairActionsEndNote`, `regionalActionsDescription`, `regionalActionsEndNote`, `mythicActionsDescription`) as dedicated metadata fields on the `Monster` entity rather than as artificial action traits.
+ - **Section Intro & End Note Metadata**:
+ - Store section-level intro text and end notes (e.g. `legendaryActionsDescription`, `lairActionsDescription`, `lairActionsEndNote`, `regionalActionsDescription`, `regionalActionsEndNote`, `mythicActionsDescription`) as dedicated metadata fields on the `Monster` entity rather than as artificial action traits.
### 4.2 Markdown & Content Formatting Support
- **Supported Markdown Syntax**: