Adds import monster activity.

This commit is contained in:
Tom Hicks
2021-06-30 11:16:52 -07:00
parent efa4c2a299
commit af05c41b75
5 changed files with 376 additions and 36 deletions

View File

@@ -12,6 +12,40 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity
android:name=".ImportMonsterActivity"
android:icon="@mipmap/ic_launcher"
android:label="Import Monster"
android:launchMode="singleTask"
android:priority="50">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<action android:name="android.intent.action.PICK" />
<action android:name="android.intent.action.INSERT" />
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.ALTERNATIVE" />
<category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="text/plain"
android:scheme="content" />
<data
android:mimeType="application/octet-stream"
android:scheme="content" />
<data
android:mimeType="text/plain"
android:scheme="file" />
</intent-filter>
</activity>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name"> android:label="@string/app_name">
@@ -25,4 +59,5 @@
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity" android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" /> android:exported="true" />
</application> </application>
</manifest>
</manifest>

View File

@@ -0,0 +1,318 @@
package com.majinnaibu.monstercards;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.monster.MonsterDetailViewModel;
import com.majinnaibu.monstercards.utils.Logger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class ImportMonsterActivity extends AppCompatActivity {
private ViewHolder mHolder;
private MonsterDetailViewModel mViewModel;
@Override
protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_monster);
mHolder = new ViewHolder(findViewById(android.R.id.content));
mViewModel = new ViewModelProvider(this).get(MonsterDetailViewModel.class);
mViewModel.getName().observe(this, mHolder.name::setText);
mViewModel.getMeta().observe(this, mHolder.meta::setText);
mViewModel.getArmorClass().observe(this, armorText -> setupLabeledTextView(mHolder.armorClass, armorText, R.string.label_armor_class));
mViewModel.getHitPoints().observe(this, hitPoints -> setupLabeledTextView(mHolder.hitPoints, hitPoints, R.string.label_hit_points));
mViewModel.getSpeed().observe(this, speed -> setupLabeledTextView(mHolder.speed, speed, R.string.label_speed));
mViewModel.getStrength().observe(this, mHolder.strength::setText);
mViewModel.getDexterity().observe(this, mHolder.dexterity::setText);
mViewModel.getConstitution().observe(this, mHolder.constitution::setText);
mViewModel.getIntelligence().observe(this, mHolder.intelligence::setText);
mViewModel.getWisdom().observe(this, mHolder.wisdom::setText);
mViewModel.getCharisma().observe(this, mHolder.charisma::setText);
mViewModel.getSavingThrows().observe(this, savingThrows -> setupOptionalTextView(mHolder.savingThrows, savingThrows, R.string.label_saving_throws));
mViewModel.getSkills().observe(this, skills -> setupOptionalTextView(mHolder.skills, skills, R.string.label_skills));
mViewModel.getDamageVulnerabilities().observe(this, damageTypes -> setupOptionalTextView(mHolder.damageVulnerabilities, damageTypes, R.string.label_damage_vulnerabilities));
mViewModel.getDamageResistances().observe(this, damageTypes -> setupOptionalTextView(mHolder.damageResistances, damageTypes, R.string.label_damage_resistances));
mViewModel.getDamageImmunities().observe(this, damageTypes -> setupOptionalTextView(mHolder.damageImmunities, damageTypes, R.string.label_damage_immunities));
mViewModel.getConditionImmunities().observe(this, conditionImmunities -> setupOptionalTextView(mHolder.conditionImmunities, conditionImmunities, R.string.label_condition_immunities));
mViewModel.getSenses().observe(this, senses -> setupOptionalTextView(mHolder.senses, senses, R.string.label_senses));
mViewModel.getLanguages().observe(this, languages -> setupOptionalTextView(mHolder.languages, languages, R.string.label_languages));
mViewModel.getChallenge().observe(this, challengeRating -> setupLabeledTextView(mHolder.challenge, challengeRating, R.string.label_challenge_rating));
mViewModel.getAbilities().observe(this, abilities -> setupTraitList(mHolder.abilities, abilities));
mViewModel.getActions().observe(this, actions -> setupTraitList(mHolder.actions, actions, mHolder.actions_label, mHolder.actions_divider));
mViewModel.getReactions().observe(this, reactions -> setupTraitList(mHolder.reactions, reactions, mHolder.reactions_label, mHolder.reactions_divider));
mViewModel.getRegionalEffects().observe(this, regionalEffects -> setupTraitList(mHolder.regionalEffects, regionalEffects, mHolder.regionalEffects_label, mHolder.regionalEffects_divider));
mViewModel.getLairActions().observe(this, lairActions -> setupTraitList(mHolder.lairActions, lairActions, mHolder.lairActions_label, mHolder.lairActions_divider));
mViewModel.getLegendaryActions().observe(this, legendaryActions -> setupTraitList(mHolder.legendaryActions, legendaryActions, mHolder.legendaryActions_label, mHolder.legendaryActions_divider));
}
@Override
protected void onResume() {
super.onResume();
Logger.logDebug("onCreateView");
Monster monster = readMonsterFromIntent(getIntent());
if (monster != null) {
mViewModel.setMonster(monster);
}
}
private Monster readMonsterFromIntent(Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
String type = intent.getType();
String json;
Uri uri = null;
if ("android.intent.action.SEND".equals(action) && "text/plain".equals(type)) {
uri = extras.getParcelable("android.intent.extra.STREAM");
} else if ("android.intent.action.VIEW".equals(action) && ("text/plain".equals(type) || "application/octet-stream".equals(type))) {
uri = intent.getData();
} else {
Logger.logError(String.format("unexpected launch configuration action: %s, type: %s, uri: %s", action, type, uri));
}
if (uri == null) {
return null;
}
json = readContentsOfUri(uri);
if (StringHelper.isNullOrEmpty(json)) {
return null;
}
return MonsterImportHelper.fromJSON(json);
}
private String readContentsOfUri(Uri uri) {
StringBuilder builder = new StringBuilder();
try (InputStream inputStream =
getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(
new InputStreamReader(Objects.requireNonNull(inputStream)))) {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} catch (IOException e) {
Logger.logError("error reading file", e);
return null;
}
return builder.toString();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
}
private void setupLabeledTextView(TextView view, String text, int titleId) {
String title = getString(titleId);
String fullText = String.format("<b>%s</b> %s", title, text);
view.setText(Html.fromHtml(fullText));
}
private void setupOptionalTextView(TextView root, String text, int titleId) {
String title = getString(titleId);
if (StringHelper.isNullOrEmpty(text)) {
root.setVisibility(View.GONE);
} else {
root.setVisibility(View.VISIBLE);
}
Spanned formatted;
if (StringHelper.isNullOrEmpty(title)) {
formatted = Html.fromHtml(text);
} else {
formatted = Html.fromHtml(String.format("<b>%s</b> %s", title, text));
}
root.setText(formatted);
}
private void setupTraitList(@NonNull LinearLayout root, @NonNull List<String> traits) {
setupTraitList(root, traits, null, null);
}
private void setupTraitList(@NonNull LinearLayout root, @NonNull List<String> traits, View label, View divider) {
int visibility = traits.size() > 0 ? View.VISIBLE : View.GONE;
DisplayMetrics displayMetrics = null;
Resources resources = getResources();
if (resources != null) {
displayMetrics = resources.getDisplayMetrics();
}
root.removeAllViews();
for (String action : traits) {
TextView tvAction = new TextView(this);
// TODO: Handle multiline block quotes specially so they stay multiline.
// TODO: Replace QuoteSpans in the result of fromHtml with something like this https://stackoverflow.com/questions/7717567/how-to-style-blockquotes-in-android-textviews to make them indent as expected
tvAction.setText(Html.fromHtml(CommonMarkHelper.toHtml(action)));
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, displayMetrics);
tvAction.setLayoutParams(layoutParams);
root.addView(tvAction);
}
root.setVisibility(visibility);
if (label != null) {
label.setVisibility(visibility);
}
if (divider != null) {
divider.setVisibility(visibility);
}
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
getMenuInflater().inflate(R.menu.import_monster, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.menu_action_import_monster) {
Logger.logDebug("Menu Item Selected");
Monster monster = mViewModel.getMonster();
if (monster != null) {
monster.id = UUID.randomUUID();
MonsterCardsApplication application = (MonsterCardsApplication) getApplication();
MonsterRepository repository = application.getMonsterRepository();
repository.addMonster(monster).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
Snackbar.make(
mHolder.root,
getString(R.string.snackbar_monster_created, monster.name),
Snackbar.LENGTH_LONG)
.setAction("Action", (_view) -> navigateToEditMonster(monster.id))
.show();
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error creating monster", e);
Snackbar.make(mHolder.root, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show();
}
});
} else {
Logger.logWTF("monsterId cannot be null.");
}
return true;
}
return super.onOptionsItemSelected(item);
}
private void navigateToEditMonster(UUID monsterId) {
Logger.logUnimplementedFeature(String.format("navigate to editing the monster %s", monsterId));
}
private static class ViewHolder {
final View root;
final TextView name;
final TextView meta;
final TextView armorClass;
final TextView hitPoints;
final TextView speed;
final TextView strength;
final TextView dexterity;
final TextView constitution;
final TextView intelligence;
final TextView wisdom;
final TextView charisma;
final TextView savingThrows;
final TextView skills;
final TextView damageVulnerabilities;
final TextView damageResistances;
final TextView damageImmunities;
final TextView conditionImmunities;
final TextView senses;
final TextView languages;
final TextView challenge;
final LinearLayout abilities;
final LinearLayout actions;
final TextView actions_label;
final ImageView actions_divider;
final LinearLayout reactions;
final TextView reactions_label;
final ImageView reactions_divider;
final LinearLayout legendaryActions;
final TextView legendaryActions_label;
final ImageView legendaryActions_divider;
final LinearLayout lairActions;
final TextView lairActions_label;
final ImageView lairActions_divider;
final LinearLayout regionalEffects;
final TextView regionalEffects_label;
final ImageView regionalEffects_divider;
ViewHolder(View root) {
this.root = root;
name = root.findViewById(R.id.name);
meta = root.findViewById(R.id.meta);
armorClass = root.findViewById(R.id.armorClass);
hitPoints = root.findViewById(R.id.hitPoints);
speed = root.findViewById(R.id.speed);
strength = root.findViewById(R.id.strength);
dexterity = root.findViewById(R.id.dexterity);
constitution = root.findViewById(R.id.constitution);
intelligence = root.findViewById(R.id.intelligence);
wisdom = root.findViewById(R.id.wisdom);
charisma = root.findViewById(R.id.charisma);
savingThrows = root.findViewById(R.id.savingThrows);
skills = root.findViewById(R.id.skills);
damageVulnerabilities = root.findViewById(R.id.damageVulnerabilities);
damageResistances = root.findViewById(R.id.damageResistances);
damageImmunities = root.findViewById(R.id.damageImmunities);
conditionImmunities = root.findViewById(R.id.conditionImmunities);
senses = root.findViewById(R.id.senses);
languages = root.findViewById(R.id.languages);
challenge = root.findViewById(R.id.challenge);
abilities = root.findViewById(R.id.abilities);
actions = root.findViewById(R.id.actions);
actions_divider = root.findViewById(R.id.actions_divider);
actions_label = root.findViewById(R.id.actions_label);
reactions = root.findViewById(R.id.reactions);
reactions_divider = root.findViewById(R.id.reactions_divider);
reactions_label = root.findViewById(R.id.reactions_label);
legendaryActions = root.findViewById(R.id.legendaryActions);
legendaryActions_divider = root.findViewById(R.id.legendaryActions_divider);
legendaryActions_label = root.findViewById(R.id.legendaryActions_label);
lairActions = root.findViewById(R.id.lairActions);
lairActions_divider = root.findViewById(R.id.lairActions_divider);
lairActions_label = root.findViewById(R.id.lairActions_label);
regionalEffects = root.findViewById(R.id.regionalEffects);
regionalEffects_divider = root.findViewById(R.id.regionalEffects_divider);
regionalEffects_label = root.findViewById(R.id.regionalEffects_label);
}
}
}

View File

@@ -1,7 +1,5 @@
package com.majinnaibu.monstercards.helpers; package com.majinnaibu.monstercards.helpers;
import androidx.annotation.NonNull;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
@@ -17,16 +15,17 @@ import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Skill; import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.models.Trait; import com.majinnaibu.monstercards.models.Trait;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
public class MonsterImportHelper { public class MonsterImportHelper {
@NonNull
public static Monster fromJSON(String json) { public static Monster fromJSON(String json) {
JsonObject rootDict = JsonParser.parseString(json).getAsJsonObject(); JsonParser parser = new JsonParser();
JsonObject rootDict = parser.parse(json).getAsJsonObject();
Monster monster = new Monster(); Monster monster = new Monster();
monster.name = Helpers.getString(rootDict, "name"); monster.name = Helpers.getString(rootDict, "name");
@@ -111,7 +110,7 @@ public class MonsterImportHelper {
return getString(dict, name, ""); return getString(dict, name, "");
} }
public static String getString(@NonNull JsonObject dict, String name, String defaultValue) { public static String getString(@NotNull JsonObject dict, String name, String defaultValue) {
if (dict.has(name)) { if (dict.has(name)) {
return dict.get(name).getAsString(); return dict.get(name).getAsString();
} }
@@ -123,19 +122,13 @@ public class MonsterImportHelper {
return getInt(dict, name, 0); return getInt(dict, name, 0);
} }
public static int getInt(@NonNull JsonObject dict, String name, int defaultValue) { public static int getInt(@NotNull JsonObject dict, String name, int defaultValue) {
if (dict.has(name)) { if (dict.has(name)) {
JsonElement element = dict.get(name); JsonElement element = dict.get(name);
if (element.isJsonPrimitive()) { if (element.isJsonPrimitive()) {
JsonPrimitive rawValue = element.getAsJsonPrimitive(); JsonPrimitive rawValue = element.getAsJsonPrimitive();//dict.getAsJsonPrimitive(name);
if (rawValue.isNumber()) { if (rawValue.isNumber()) {
return rawValue.getAsInt(); return rawValue.getAsInt();
} else {
try {
return rawValue.getAsInt();
} catch (Exception ex) {
return defaultValue;
}
} }
} }
} }
@@ -146,29 +139,22 @@ public class MonsterImportHelper {
return getBool(dict, name, false); return getBool(dict, name, false);
} }
public static boolean getBool(@NonNull JsonObject dict, String name, boolean defaultValue) { public static boolean getBool(@NotNull JsonObject dict, String name, boolean defaultValue) {
if (dict.has(name)) { if (dict.has(name)) {
JsonElement element = dict.get(name); JsonElement element = dict.get(name);
if (element.isJsonPrimitive()) { if (element.isJsonPrimitive()) {
JsonPrimitive rawValue = element.getAsJsonPrimitive(); JsonPrimitive rawValue = element.getAsJsonPrimitive();
if (rawValue.isBoolean()) { if (rawValue.isBoolean()) {
return rawValue.getAsBoolean(); return rawValue.getAsBoolean();
} else {
try {
return rawValue.getAsBoolean();
} catch (Exception ex) {
return defaultValue;
}
} }
} }
} }
return defaultValue; return defaultValue;
} }
@NonNull @NotNull
public static String formatDistance(String name, int distance) { public static String formatDistance(String name, int distance) {
// TODO: consider moving this to a string resource so it can be localized return String.format("%s %d ft.", name, distance);
return String.format(Locale.getDefault(), "%s %d ft.", name, distance);
} }
public static void addSense(Monster monster, JsonObject root, String name) { public static void addSense(Monster monster, JsonObject root, String name) {
@@ -178,8 +164,8 @@ public class MonsterImportHelper {
} }
} }
@NonNull @NotNull
public static List<Trait> getListOfTraits(@NonNull JsonObject dict, String name) { public static List<Trait> getListOfTraits(@NotNull JsonObject dict, String name) {
ArrayList<Trait> traits = new ArrayList<>(); ArrayList<Trait> traits = new ArrayList<>();
if (dict.has(name)) { if (dict.has(name)) {
JsonElement arrayElement = dict.get(name); JsonElement arrayElement = dict.get(name);
@@ -191,7 +177,7 @@ public class MonsterImportHelper {
if (jsonElement.isJsonObject()) { if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject(); JsonObject jsonObject = jsonElement.getAsJsonObject();
String traitName = Helpers.getString(jsonObject, "name"); String traitName = Helpers.getString(jsonObject, "name");
String description = Helpers.getString(jsonObject, "desc"); String description = Helpers.getString(jsonObject, "description");
Trait trait = new Trait(traitName, description); Trait trait = new Trait(traitName, description);
traits.add(trait); traits.add(trait);
} }
@@ -201,7 +187,7 @@ public class MonsterImportHelper {
return traits; return traits;
} }
public static void addSavingThrows(Monster monster, @NonNull JsonObject root) { public static void addSavingThrows(Monster monster, JsonObject root) {
if (root.has("sthrows")) { if (root.has("sthrows")) {
JsonElement arrayElement = root.get("sthrows"); JsonElement arrayElement = root.get("sthrows");
if (arrayElement.isJsonArray()) { if (arrayElement.isJsonArray()) {
@@ -231,8 +217,7 @@ public class MonsterImportHelper {
} }
} }
@NonNull public static Set<Skill> getSetOfSkills(JsonObject root) {
public static Set<Skill> getSetOfSkills(@NonNull JsonObject root) {
HashSet<Skill> skills = new HashSet<>(); HashSet<Skill> skills = new HashSet<>();
if (root.has("skills")) { if (root.has("skills")) {
JsonElement arrayElement = root.get("skills"); JsonElement arrayElement = root.get("skills");
@@ -256,13 +241,11 @@ public class MonsterImportHelper {
return skills; return skills;
} }
@NonNull
public static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name) { public static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name) {
return getSetOfDamageTypes(rootDict, name, null); return getSetOfDamageTypes(rootDict, name, null);
} }
@NonNull public static Set<String> getSetOfDamageTypes(JsonObject root, String name, String type) {
public static Set<String> getSetOfDamageTypes(@NonNull JsonObject root, String name, String type) {
HashSet<String> damageTypes = new HashSet<>(); HashSet<String> damageTypes = new HashSet<>();
if (root.has(name)) { if (root.has(name)) {
JsonElement arrayElement = root.get(name); JsonElement arrayElement = root.get(name);
@@ -285,8 +268,7 @@ public class MonsterImportHelper {
return damageTypes; return damageTypes;
} }
@NonNull public static Set<Language> getSetOfLanguages(JsonObject root, String name) {
public static Set<Language> getSetOfLanguages(@NonNull JsonObject root, String name) {
HashSet<Language> languages = new HashSet<>(); HashSet<Language> languages = new HashSet<>();
if (root.has(name)) { if (root.has(name)) {
JsonElement arrayElement = root.get(name); JsonElement arrayElement = root.get(name);

View File

@@ -180,6 +180,10 @@ public class MonsterDetailViewModel extends ViewModel {
return mMonsterId; return mMonsterId;
} }
public Monster getMonster() {
return mMonster;
}
public void setMonster(Monster monster) { public void setMonster(Monster monster) {
mMonster = monster; mMonster = monster;
mAbilities.setValue(mMonster.getAbilityDescriptions()); mAbilities.setValue(mMonster.getAbilityDescriptions());

View File

@@ -9,6 +9,7 @@
<string name="action_add_skill">Add Skill</string> <string name="action_add_skill">Add Skill</string>
<string name="action_add_trait">Add Trait</string> <string name="action_add_trait">Add Trait</string>
<string name="action_edit">Edit</string> <string name="action_edit">Edit</string>
<string name="action_import_monster">Import Monster</string>
<string name="app_name">MonsterCards</string> <string name="app_name">MonsterCards</string>
<string name="charisma_abbreviation">CHA</string> <string name="charisma_abbreviation">CHA</string>
<string name="constitution_abbreviation">CON</string> <string name="constitution_abbreviation">CON</string>