diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml index 70129c6..ba7b339 100644 --- a/Android/app/src/main/AndroidManifest.xml +++ b/Android/app/src/main/AndroidManifest.xml @@ -5,19 +5,22 @@ + android:name=".MainActivity" + android:label="@string/app_name" + android:launchMode="singleTask"> + + + + + @@ -45,15 +48,8 @@ android:mimeType="text/plain" android:scheme="file" /> - - - - - - + diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ImportMonsterActivity.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ImportMonsterActivity.java deleted file mode 100644 index 030f8cc..0000000 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ImportMonsterActivity.java +++ /dev/null @@ -1,318 +0,0 @@ -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("%s %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("%s %s", title, text)); - } - root.setText(formatted); - } - - private void setupTraitList(@NonNull LinearLayout root, @NonNull List traits) { - setupTraitList(root, traits, null, null); - } - - private void setupTraitList(@NonNull LinearLayout root, @NonNull List 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); - } - } -} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java index 4532471..88ca743 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java @@ -1,26 +1,35 @@ package com.majinnaibu.monstercards; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.MenuItem; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.navigation.NavController; +import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.majinnaibu.monstercards.helpers.StringHelper; import com.majinnaibu.monstercards.init.AppCenterInitializer; import com.majinnaibu.monstercards.init.FlipperInitializer; 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.Objects; + public class MainActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == android.R.id.home) { - Logger.logDebug("Home selected (in MainActivity)"); getOnBackPressedDispatcher().onBackPressed(); return true; } @@ -50,5 +59,59 @@ public class MainActivity extends AppCompatActivity { }); NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); NavigationUI.setupWithNavController(navView, navController); + onNewIntent(getIntent()); } -} \ No newline at end of file + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + String json = readMonsterJSONFromIntent(intent); + if (!StringHelper.isNullOrEmpty(json)) { + NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment); + NavController navController = navHostFragment.getNavController(); + NavDirections action = MobileNavigationDirections.actionGlobalMonsterImportFragment(json); + navController.navigate(action); + } + } + + private String readMonsterJSONFromIntent(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 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(); + } +} 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 a0b0bed..a91a5f9 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 @@ -180,10 +180,6 @@ public class MonsterDetailViewModel extends ViewModel { return mMonsterId; } - public Monster getMonster() { - return mMonster; - } - public void setMonster(Monster monster) { mMonster = monster; mAbilities.setValue(mMonster.getAbilityDescriptions()); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java index a4912f7..edb973c 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java @@ -88,7 +88,7 @@ public class MonsterImportFragment extends MCFragment { return root; } - private void setupLabeledTextView(@NonNull TextView view, String text, int titleId) { + private void setupLabeledTextView(TextView view, String text, int titleId) { String title = getString(titleId); String fullText = String.format("%s %s", title, text); view.setText(Html.fromHtml(fullText)); @@ -184,7 +184,7 @@ public class MonsterImportFragment extends MCFragment { return super.onOptionsItemSelected(item); } - private void navigateToEditMonster(@NonNull UUID monsterId) { + private void navigateToEditMonster(UUID monsterId) { NavController navController = Navigation.findNavController(requireView()); NavDirections action; action = MonsterImportFragmentDirections.actionMonsterImportFragmentToNavigationLibrary(); @@ -234,7 +234,7 @@ public class MonsterImportFragment extends MCFragment { final TextView regionalEffects_label; final ImageView regionalEffects_divider; - ViewHolder(@NonNull View root) { + ViewHolder(View root) { this.root = root; name = root.findViewById(R.id.name); meta = root.findViewById(R.id.meta); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportViewModel.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportViewModel.java index ae13d15..31e1a80 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportViewModel.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportViewModel.java @@ -1,6 +1,5 @@ package com.majinnaibu.monstercards.ui.monster; -import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; @@ -184,7 +183,7 @@ public class MonsterImportViewModel extends ViewModel { return mMonster; } - public void setMonster(@NonNull Monster monster) { + public void setMonster(Monster monster) { mMonster = monster; mAbilities.setValue(mMonster.getAbilityDescriptions()); mActions.setValue(mMonster.getActionDescriptions()); diff --git a/Android/app/src/main/res/navigation/mobile_navigation.xml b/Android/app/src/main/res/navigation/mobile_navigation.xml index 893f689..71cb792 100644 --- a/Android/app/src/main/res/navigation/mobile_navigation.xml +++ b/Android/app/src/main/res/navigation/mobile_navigation.xml @@ -228,4 +228,21 @@ app:argType="string" /> + + + + + diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index e0e919e..dc5d036 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -121,6 +121,7 @@ Senses Skill Skills + Import Monster Library Search WIS