Makes monsters shareable.
This commit is contained in:
@@ -76,6 +76,16 @@
|
|||||||
|
|
||||||
<nav-graph android:value="@navigation/mobile_navigation" />
|
<nav-graph android:value="@navigation/mobile_navigation" />
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
package com.majinnaibu.monstercards.ui.monster;
|
package com.majinnaibu.monstercards.ui.monster;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
@@ -184,10 +192,56 @@ public class MonsterDetailFragment extends MCFragment {
|
|||||||
} else if (item.getItemId() == R.id.menu_action_add_to_dashboard) {
|
} else if (item.getItemId() == R.id.menu_action_add_to_dashboard) {
|
||||||
addCurrentMonsterToDashboard();
|
addCurrentMonsterToDashboard();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_share_monster) {
|
||||||
|
shareCurrentMonster();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
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() {
|
private void addCurrentMonsterToDashboard() {
|
||||||
UUID monsterId = mViewModel.getId().getValue();
|
UUID monsterId = mViewModel.getId().getValue();
|
||||||
if (monsterId == null) {
|
if (monsterId == null) {
|
||||||
|
|||||||
@@ -181,6 +181,10 @@ public class MonsterDetailViewModel extends ViewModel {
|
|||||||
return mMonsterId;
|
return mMonsterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Monster getMonster() {
|
||||||
|
return mMonster;
|
||||||
|
}
|
||||||
|
|
||||||
public void setMonster(@NonNull Monster monster) {
|
public void setMonster(@NonNull Monster monster) {
|
||||||
mMonster = monster;
|
mMonster = monster;
|
||||||
mAbilities.setValue(mMonster.getAbilityDescriptions());
|
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
|
<item
|
||||||
android:id="@+id/menu_action_edit_monster"
|
android:id="@+id/menu_action_edit_monster"
|
||||||
|
android:icon="@drawable/ic_edit_24"
|
||||||
android:title="@string/action_edit"
|
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
|
<item
|
||||||
android:id="@+id/menu_action_add_to_collection"
|
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_collection_added_to_dashboard">Added collection %1$s to Dashboard</string>
|
||||||
<string name="snackbar_dashboard_cleared">Dashboard cleared</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="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_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_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_url_hint">e.g. D&D Beyond URL or Open5e JSON</string>
|
||||||
<string name="dialog_import">Import</string>
|
<string name="dialog_import">Import</string>
|
||||||
<string name="toast_importing_url">Importing entity...</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="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>
|
</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()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
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"
|
def nav_version = "2.9.8"
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
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 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 4**: Export to internal format described by Open5e document (`Open5eExporter`).
|
||||||
- [x] **Step 5**: Import from internal format described by Open5e document (`Open5eImporter`).
|
- [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).
|
- [ ] **Step 7**: Specific share targets (7.1 NFC, 7.2 Bluetooth, 7.3 Embedded Web URL).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -260,7 +260,7 @@ Extend the generic sharing feature with specialized, direct sharing channels (to
|
|||||||
### 4.1 Schema Architecture Considerations
|
### 4.1 Schema Architecture Considerations
|
||||||
- **Unified Action Entity / Type Column**:
|
- **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.
|
- 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**:
|
- **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.
|
- 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
|
### 4.2 Markdown & Content Formatting Support
|
||||||
|
|||||||
Reference in New Issue
Block a user