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**: