Makes monsters shareable.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
10
Android/app/src/main/res/drawable/ic_share_24.xml
Normal file
10
Android/app/src/main/res/drawable/ic_share_24.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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&D Beyond URL, Character ID, or Open5e JSON payload:</string>
|
||||
<string name="dialog_import_url_hint">e.g. D&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>
|
||||
|
||||
5
Android/app/src/main/res/xml/file_paths.xml
Normal file
5
Android/app/src/main/res/xml/file_paths.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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**:
|
||||
|
||||
Reference in New Issue
Block a user