Makes monsters shareable.

This commit is contained in:
2026-09-08 13:58:35 -07:00
parent ccc9fadef9
commit ccb06960a2
9 changed files with 98 additions and 6 deletions

View File

@@ -76,6 +76,16 @@
<nav-graph android:value="@navigation/mobile_navigation" />
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.majinnaibu.monstercards.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -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) {

View File

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

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.47 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.23 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -4,8 +4,15 @@
<item
android:id="@+id/menu_action_edit_monster"
android:icon="@drawable/ic_edit_24"
android:title="@string/action_edit"
app:showAsAction="never" />
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_action_share_monster"
android:icon="@drawable/ic_share_24"
android:title="@string/action_share_monster"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_action_add_to_collection"

View File

@@ -162,11 +162,13 @@
<string name="snackbar_collection_added_to_dashboard">Added collection %1$s to Dashboard</string>
<string name="snackbar_dashboard_cleared">Dashboard cleared</string>
<string name="no_monsters_available">No monsters available in library. Create one first!</string>
<string name="action_import_from_url">Import Entity / Character...</string>
<string name="action_import_from_url">Import URL...</string>
<string name="dialog_import_url_title">Import Entity or Character</string>
<string name="dialog_import_url_message">Enter or paste a D&amp;D Beyond URL, Character ID, or Open5e JSON payload:</string>
<string name="dialog_import_url_hint">e.g. D&amp;D Beyond URL or Open5e JSON</string>
<string name="dialog_import">Import</string>
<string name="toast_importing_url">Importing entity...</string>
<string name="failed_to_import_url">Failed to import entity. Please check the URL, ID, or JSON format.</string>
<string name="action_share_monster">Share / Export Monster (.card)</string>
<string name="failed_to_share_monster">Failed to share monster</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="shared_cards" path="." />
<external-cache-path name="shared_cards_ext" path="." />
</paths>

View File

@@ -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"

View File

@@ -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<Trait>` 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**: