From d1e3c3f5f313057e5a81a4333906ef5d79adea83 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Mon, 30 Jun 2025 12:23:51 -0700 Subject: [PATCH] Squashed 'Android/' content from commit 7a63a11 git-subtree-dir: Android git-subtree-split: 7a63a11e93810b4d7f49c5edd5596d1c7f23430b --- .gitignore | 17 + .idea/.name | 1 + .idea/codeStyles/Project.xml | 116 ++ .idea/compiler.xml | 6 + .idea/gradle.xml | 22 + .idea/inspectionProfiles/Project_Default.xml | 7 + .idea/jarRepositories.xml | 30 + .idea/misc.xml | 52 + .idea/runConfigurations.xml | 13 + .idea/saveactions_settings.xml | 17 + .idea/vcs.xml | 6 + README.md | 4 + app/.gitignore | 1 + app/build.gradle | 109 ++ app/proguard-rules.pro | 32 + .../1.json | 440 +++++++ .../2.json | 499 ++++++++ .../3.json | 483 ++++++++ .../schema-notes.md | 73 ++ .../monstercards/ExampleInstrumentedTest.java | 26 + .../init/AppCenterInitializer.java | 22 + .../monstercards/init/FlipperInitializer.java | 42 + app/src/main/AndroidManifest.xml | 59 + .../majinnaibu/monstercards/AppDatabase.java | 30 + .../majinnaibu/monstercards/MainActivity.java | 118 ++ .../monstercards/MonsterCardsApplication.java | 82 ++ .../monstercards/data/DevContent.java | 113 ++ .../monstercards/data/MonsterDAO.java | 39 + .../monstercards/data/MonsterRepository.java | 112 ++ .../data/converters/ArmorTypeConverter.java | 19 + .../converters/ChallengeRatingConverter.java | 19 + .../converters/ListOfTraitsConverter.java | 27 + .../converters/SetOfLanguageConverter.java | 28 + .../data/converters/SetOfSkillConverter.java | 28 + .../data/converters/SetOfStringConverter.java | 27 + .../data/converters/UUIDConverter.java | 20 + .../monstercards/data/enums/AbilityScore.java | 31 + .../data/enums/AdvantageType.java | 27 + .../monstercards/data/enums/ArmorType.java | 41 + .../data/enums/ChallengeRating.java | 60 + .../data/enums/ProficiencyType.java | 27 + .../monstercards/data/enums/StringType.java | 9 + .../monstercards/data/enums/TraitType.java | 10 + .../monstercards/helpers/ArrayHelper.java | 17 + .../helpers/CommonMarkHelper.java | 27 + .../helpers/MonsterImportHelper.java | 311 +++++ .../monstercards/helpers/StringHelper.java | 79 ++ .../monstercards/models/Language.java | 74 ++ .../monstercards/models/Monster.java | 1070 ++++++++++++++++ .../monstercards/models/MonsterFTS.java | 14 + .../majinnaibu/monstercards/models/Skill.java | 95 ++ .../majinnaibu/monstercards/models/Trait.java | 49 + .../ui/collections/CollectionsFragment.java | 27 + .../ui/collections/CollectionsViewModel.java | 19 + .../ui/components/AbilityScorePicker.java | 133 ++ .../ui/components/AdvantagePicker.java | 98 ++ .../ui/components/ProficiencyPicker.java | 98 ++ .../monstercards/ui/components/Stepper.java | 149 +++ .../ui/dashboard/DashboardFragment.java | 84 ++ .../DashboardRecyclerViewAdapter.java | 353 ++++++ .../ui/dashboard/DashboardViewModel.java | 26 + .../EditAbilityScoresFragment.java | 82 ++ .../ui/editmonster/EditArmorFragment.java | 104 ++ .../ui/editmonster/EditBasicInfoFragment.java | 88 ++ .../EditChallengeRatingFragment.java | 91 ++ .../ui/editmonster/EditLanguageFragment.java | 88 ++ .../ui/editmonster/EditLanguageViewModel.java | 68 + .../ui/editmonster/EditLanguagesFragment.java | 101 ++ .../EditLanguagesRecyclerViewAdapter.java | 114 ++ .../ui/editmonster/EditMonsterFragment.java | 272 ++++ .../ui/editmonster/EditMonsterViewModel.java | 1092 +++++++++++++++++ .../editmonster/EditSavingThrowsFragment.java | 94 ++ .../ui/editmonster/EditSkillFragment.java | 99 ++ .../ui/editmonster/EditSkillViewModel.java | 81 ++ .../ui/editmonster/EditSkillsFragment.java | 92 ++ .../EditSkillsRecyclerViewAdapter.java | 68 + .../ui/editmonster/EditSpeedFragment.java | 90 ++ .../ui/editmonster/EditStringFragment.java | 102 ++ .../ui/editmonster/EditStringViewModel.java | 32 + .../ui/editmonster/EditStringsFragment.java | 129 ++ .../EditStringsRecyclerViewAdapter.java | 64 + .../ui/editmonster/EditTraitFragment.java | 110 ++ .../ui/editmonster/EditTraitViewModel.java | 63 + .../ui/editmonster/EditTraitsFragment.java | 131 ++ .../EditTraitsRecyclerViewAdapter.java | 71 ++ .../ui/library/LibraryFragment.java | 121 ++ .../library/LibraryRecyclerViewAdapter.java | 115 ++ .../ui/monster/MonsterDetailFragment.java | 256 ++++ .../ui/monster/MonsterDetailViewModel.java | 214 ++++ .../ui/monster/MonsterImportFragment.java | 278 +++++ .../ui/monster/MonsterImportViewModel.java | 217 ++++ .../ui/search/SearchFragment.java | 53 + .../SearchResultsRecyclerViewAdapter.java | 90 ++ .../ui/shared/ChangeTrackedViewModel.java | 25 + .../monstercards/ui/shared/MCFragment.java | 35 + .../ui/shared/SwipeToDeleteCallback.java | 98 ++ .../utils/ChangeTrackedLiveData.java | 65 + .../majinnaibu/monstercards/utils/Logger.java | 139 +++ .../utils/TextChangedListener.java | 75 ++ app/src/main/res/color/radio_button_text.xml | 5 + .../drawable-v24/ic_launcher_foreground.xml | 30 + app/src/main/res/drawable/ic_add_24.xml | 10 + .../main/res/drawable/ic_chevron_right_24.xml | 10 + .../drawable/ic_collections_black_24dp.xml | 13 + .../res/drawable/ic_dashboard_black_24dp.xml | 9 + .../main/res/drawable/ic_delete_white_36.xml | 5 + app/src/main/res/drawable/ic_edit_24.xml | 10 + .../res/drawable/ic_launcher_background.xml | 170 +++ .../main/res/drawable/ic_library_add_24.xml | 5 + .../res/drawable/ic_library_black_24dp.xml | 10 + .../res/drawable/ic_search_black_24dp.xml | 10 + .../main/res/drawable/ic_section_divider.xml | 9 + .../res/drawable/radio_button_checked.xml | 6 + .../res/drawable/radio_button_selector.xml | 5 + .../res/drawable/radio_button_unchecked.xml | 6 + .../res/drawable/rectangle_background.xml | 7 + app/src/main/res/layout/activity_main.xml | 35 + .../main/res/layout/card_ability_score.xml | 59 + app/src/main/res/layout/card_action.xml | 40 + app/src/main/res/layout/card_armor_class.xml | 35 + .../main/res/layout/card_challenge_rating.xml | 35 + app/src/main/res/layout/card_hit_points.xml | 35 + app/src/main/res/layout/card_initiative.xml | 35 + app/src/main/res/layout/card_monster.xml | 174 +++ .../main/res/layout/card_monster_short.xml | 136 ++ .../layout/component_ability_score_picker.xml | 20 + .../res/layout/component_advantage_picker.xml | 77 ++ .../layout/component_advantage_picker2.xml | 78 ++ .../layout/component_proficiency_picker.xml | 77 ++ app/src/main/res/layout/component_stepper.xml | 77 ++ .../main/res/layout/dropdown_list_item.xml | 9 + .../main/res/layout/fragment_collections.xml | 22 + .../main/res/layout/fragment_dashboard.xml | 23 + .../layout/fragment_dashboard_list_item.xml | 42 + .../layout/fragment_edit_ability_scores.xml | 74 ++ .../main/res/layout/fragment_edit_armor.xml | 62 + .../res/layout/fragment_edit_basic_info.xml | 124 ++ .../layout/fragment_edit_challenge_rating.xml | 48 + .../res/layout/fragment_edit_language.xml | 36 + .../layout/fragment_edit_languages_list.xml | 33 + .../fragment_edit_languages_list_header.xml | 36 + .../fragment_edit_languages_list_item.xml | 13 + .../main/res/layout/fragment_edit_monster.xml | 189 +++ .../layout/fragment_edit_saving_throws.xml | 171 +++ .../main/res/layout/fragment_edit_skill.xml | 46 + .../res/layout/fragment_edit_skills_list.xml | 33 + .../layout/fragment_edit_skills_list_item.xml | 13 + .../main/res/layout/fragment_edit_speed.xml | 95 ++ .../main/res/layout/fragment_edit_string.xml | 22 + .../res/layout/fragment_edit_strings_list.xml | 33 + .../fragment_edit_strings_list_item.xml | 13 + .../main/res/layout/fragment_edit_trait.xml | 43 + .../res/layout/fragment_edit_traits_list.xml | 33 + .../layout/fragment_edit_traits_list_item.xml | 13 + app/src/main/res/layout/fragment_library.xml | 33 + app/src/main/res/layout/fragment_monster.xml | 664 ++++++++++ app/src/main/res/layout/fragment_search.xml | 40 + .../main/res/layout/monster_list_content.xml | 14 + app/src/main/res/layout/tile_monster.xml | 101 ++ .../main/res/layout/tile_monster_short.xml | 90 ++ app/src/main/res/menu/bottom_nav_menu.xml | 24 + app/src/main/res/menu/edit_skills_menu.xml | 10 + app/src/main/res/menu/import_monster.xml | 9 + app/src/main/res/menu/monster_detail_menu.xml | 10 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../main/res/navigation/mobile_navigation.xml | 244 ++++ app/src/main/res/values/attrs.xml | 14 + app/src/main/res/values/colors.xml | 26 + app/src/main/res/values/dimens.xml | 22 + app/src/main/res/values/strings.xml | 140 +++ app/src/main/res/values/styles.xml | 10 + .../init/AppCenterInitializer.java | 8 + .../monstercards/init/FlipperInitializer.java | 17 + .../monstercards/ExampleUnitTest.java | 17 + build.gradle | 29 + gradle.properties | 19 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++ gradlew.bat | 84 ++ settings.gradle | 2 + 192 files changed, 15048 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.name create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/saveactions_settings.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/schemas/com.majinnaibu.monstercards.AppDatabase/1.json create mode 100644 app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json create mode 100644 app/schemas/com.majinnaibu.monstercards.AppDatabase/3.json create mode 100644 app/schemas/com.majinnaibu.monstercards.AppDatabase/schema-notes.md create mode 100644 app/src/androidTest/java/com/majinnaibu/monstercards/ExampleInstrumentedTest.java create mode 100644 app/src/debug/java/com/majinnaibu/monstercards/init/AppCenterInitializer.java create mode 100644 app/src/debug/java/com/majinnaibu/monstercards/init/FlipperInitializer.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/MainActivity.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/DevContent.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/converters/ArmorTypeConverter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/converters/ChallengeRatingConverter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/converters/ListOfTraitsConverter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfLanguageConverter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfSkillConverter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfStringConverter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/converters/UUIDConverter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/enums/AdvantageType.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/enums/ArmorType.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/enums/ChallengeRating.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/enums/ProficiencyType.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/enums/StringType.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/data/enums/TraitType.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/helpers/ArrayHelper.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/helpers/StringHelper.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/models/Language.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/models/Monster.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/models/Skill.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/models/Trait.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/components/AbilityScorePicker.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/components/AdvantagePicker.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/components/ProficiencyPicker.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/components/Stepper.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditAbilityScoresFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditArmorFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditBasicInfoFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditChallengeRatingFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSavingThrowsFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillsFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillsRecyclerViewAdapter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSpeedFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsRecyclerViewAdapter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitsFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitsRecyclerViewAdapter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryRecyclerViewAdapter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchResultsRecyclerViewAdapter.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/shared/ChangeTrackedViewModel.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/ui/shared/SwipeToDeleteCallback.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/utils/ChangeTrackedLiveData.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/utils/Logger.java create mode 100644 app/src/main/java/com/majinnaibu/monstercards/utils/TextChangedListener.java create mode 100644 app/src/main/res/color/radio_button_text.xml create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_add_24.xml create mode 100644 app/src/main/res/drawable/ic_chevron_right_24.xml create mode 100644 app/src/main/res/drawable/ic_collections_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_dashboard_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_delete_white_36.xml create mode 100644 app/src/main/res/drawable/ic_edit_24.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_library_add_24.xml create mode 100644 app/src/main/res/drawable/ic_library_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_search_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_section_divider.xml create mode 100644 app/src/main/res/drawable/radio_button_checked.xml create mode 100644 app/src/main/res/drawable/radio_button_selector.xml create mode 100644 app/src/main/res/drawable/radio_button_unchecked.xml create mode 100644 app/src/main/res/drawable/rectangle_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/card_ability_score.xml create mode 100644 app/src/main/res/layout/card_action.xml create mode 100644 app/src/main/res/layout/card_armor_class.xml create mode 100644 app/src/main/res/layout/card_challenge_rating.xml create mode 100644 app/src/main/res/layout/card_hit_points.xml create mode 100644 app/src/main/res/layout/card_initiative.xml create mode 100644 app/src/main/res/layout/card_monster.xml create mode 100644 app/src/main/res/layout/card_monster_short.xml create mode 100644 app/src/main/res/layout/component_ability_score_picker.xml create mode 100644 app/src/main/res/layout/component_advantage_picker.xml create mode 100644 app/src/main/res/layout/component_advantage_picker2.xml create mode 100644 app/src/main/res/layout/component_proficiency_picker.xml create mode 100644 app/src/main/res/layout/component_stepper.xml create mode 100644 app/src/main/res/layout/dropdown_list_item.xml create mode 100644 app/src/main/res/layout/fragment_collections.xml create mode 100644 app/src/main/res/layout/fragment_dashboard.xml create mode 100644 app/src/main/res/layout/fragment_dashboard_list_item.xml create mode 100644 app/src/main/res/layout/fragment_edit_ability_scores.xml create mode 100644 app/src/main/res/layout/fragment_edit_armor.xml create mode 100644 app/src/main/res/layout/fragment_edit_basic_info.xml create mode 100644 app/src/main/res/layout/fragment_edit_challenge_rating.xml create mode 100644 app/src/main/res/layout/fragment_edit_language.xml create mode 100644 app/src/main/res/layout/fragment_edit_languages_list.xml create mode 100644 app/src/main/res/layout/fragment_edit_languages_list_header.xml create mode 100644 app/src/main/res/layout/fragment_edit_languages_list_item.xml create mode 100644 app/src/main/res/layout/fragment_edit_monster.xml create mode 100644 app/src/main/res/layout/fragment_edit_saving_throws.xml create mode 100644 app/src/main/res/layout/fragment_edit_skill.xml create mode 100644 app/src/main/res/layout/fragment_edit_skills_list.xml create mode 100644 app/src/main/res/layout/fragment_edit_skills_list_item.xml create mode 100644 app/src/main/res/layout/fragment_edit_speed.xml create mode 100644 app/src/main/res/layout/fragment_edit_string.xml create mode 100644 app/src/main/res/layout/fragment_edit_strings_list.xml create mode 100644 app/src/main/res/layout/fragment_edit_strings_list_item.xml create mode 100644 app/src/main/res/layout/fragment_edit_trait.xml create mode 100644 app/src/main/res/layout/fragment_edit_traits_list.xml create mode 100644 app/src/main/res/layout/fragment_edit_traits_list_item.xml create mode 100644 app/src/main/res/layout/fragment_library.xml create mode 100644 app/src/main/res/layout/fragment_monster.xml create mode 100644 app/src/main/res/layout/fragment_search.xml create mode 100644 app/src/main/res/layout/monster_list_content.xml create mode 100644 app/src/main/res/layout/tile_monster.xml create mode 100644 app/src/main/res/layout/tile_monster_short.xml create mode 100644 app/src/main/res/menu/bottom_nav_menu.xml create mode 100644 app/src/main/res/menu/edit_skills_menu.xml create mode 100644 app/src/main/res/menu/import_monster.xml create mode 100644 app/src/main/res/menu/monster_detail_menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/navigation/mobile_navigation.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/release/java/com/majinnaibu/monstercards/init/AppCenterInitializer.java create mode 100644 app/src/release/java/com/majinnaibu/monstercards/init/FlipperInitializer.java create mode 100644 app/src/test/java/com/majinnaibu/monstercards/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a840822 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea/dictionaries +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +/app/debug +/app/release diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..c136dab --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +MonsterCards \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..23a89bb --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..5d018a6 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..e34606c --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6c2e59d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..e497da9 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/saveactions_settings.xml b/.idea/saveactions_settings.xml new file mode 100644 index 0000000..033b46c --- /dev/null +++ b/.idea/saveactions_settings.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..430f62e --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +[![Build status](https://build.appcenter.ms/v0.1/apps/44e4ee45-fe39-4d2d-950f-943e9948ca35/branches/master/badge)](https://appcenter.ms) + +# MonsterCards for Android + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..1d8cad7 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,109 @@ +plugins { + id 'com.android.application' + id 'androidx.navigation.safeargs' +} + +Properties properties = new Properties() +def propertiesFile = project.rootProject.file('local.properties') +if (propertiesFile.exists()) { + properties.load(propertiesFile.newDataInputStream()) +} +def appCenterLocalSecret = properties.getProperty('appCenter.localSecret') +def appCenterEnvSecret = System.getenv('APPCENTER_SECRET') +def appCenterSecret = appCenterLocalSecret != null ? appCenterLocalSecret : appCenterEnvSecret != null ? appCenterEnvSecret : "" +def appCenterSdkVersion = '3.3.0' +def nav_version = '2.3.5' +def room_version = '2.3.0' +def rxjava_version = '3.0.0' +def flipper_version = '0.87.0' +def soloader_version = '0.10.1' +def gson_version = '2.8.6' + +android { + compileSdkVersion 30 + buildToolsVersion '30.0.3' + + defaultConfig { + applicationId "com.majinnaibu.monstercards" + minSdkVersion 22 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\"" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + } + + buildTypes { + release { + // Enables code shrinking, obfuscation, and optimization for only + // your project's release build type. + minifyEnabled true + + // Enables resource shrinking, which is performed by the + // Android Gradle plugin. + shrinkResources true + + // Includes the default ProGuard rules files that are packaged with + // the Android Gradle plugin. To learn more, go to the section about + // R8 configuration files. + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + viewBinding true + } +} + +dependencies { + // Included libs + implementation fileTree(dir: "libs", include: ["*.jar"]) + + // Google + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.0' + implementation "androidx.navigation:navigation-fragment:$nav_version" + implementation "androidx.navigation:navigation-ui:$nav_version" + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' + + // Testing + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + // Room DB + implementation "io.reactivex.rxjava3:rxjava:$rxjava_version" + implementation "io.reactivex.rxjava3:rxandroid:$rxjava_version" + implementation "androidx.room:room-runtime:$room_version" + annotationProcessor "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-rxjava3:$room_version" + //testImplementation "androidx.room:room-testing:$room_version" + + // AppCenter + debugImplementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}" + debugImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}" + + // Flipper + debugImplementation "com.facebook.flipper:flipper:$flipper_version" + debugImplementation "com.facebook.soloader:soloader:$soloader_version" + releaseImplementation "com.facebook.flipper:flipper-noop:$flipper_version" + + // Other 3rd Party + implementation 'com.atlassian.commonmark:commonmark:0.15.2' + implementation "com.google.code.gson:gson:$gson_version" +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..08e4fd6 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,32 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keep enum com.majinnaibu.monstercards.data.enums.AbilityScore +-keep enum com.majinnaibu.monstercards.data.enums.ProficiencyType +-keep enum com.majinnaibu.monstercards.data.enums.AdvantageType +-keep enum com.majinnaibu.monstercards.data.enums.TraitType +-keep enum com.majinnaibu.monstercards.data.enums.StringType +-keepclassmembers,allowoptimization enum * { + ; + public static **[] values(); + public static ** valueOf(java.lang.String); +} diff --git a/app/schemas/com.majinnaibu.monstercards.AppDatabase/1.json b/app/schemas/com.majinnaibu.monstercards.AppDatabase/1.json new file mode 100644 index 0000000..83ab2bc --- /dev/null +++ b/app/schemas/com.majinnaibu.monstercards.AppDatabase/1.json @@ -0,0 +1,440 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "db1293d2f490940b55ca1f4f56b21b1a", + "entities": [ + { + "tableName": "Monster", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `blindsight_range` INTEGER NOT NULL DEFAULT 0, `is_blind_beyond_blindsight_range` INTEGER NOT NULL DEFAULT false, `darkvision_range` INTEGER NOT NULL DEFAULT 0, `tremorsense_range` INTEGER NOT NULL DEFAULT 0, `truesight_range` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `skills` TEXT, `damage_immunities` TEXT, `damage_resistances` TEXT, `damage_vulnerabilities` TEXT, `condition_immunities` TEXT, `languages` TEXT, `abilities` TEXT, `actions` TEXT, `reactions` TEXT, `lair_actions` TEXT, `legendary_actions` TEXT, `regional_actions` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "strengthScore", + "columnName": "strength_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "strengthSavingThrowAdvantage", + "columnName": "strength_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "strengthSavingThrowProficiency", + "columnName": "strength_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "dexterityScore", + "columnName": "dexterity_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "dexteritySavingThrowAdvantage", + "columnName": "dexterity_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "dexteritySavingThrowProficiency", + "columnName": "dexterity_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionScore", + "columnName": "constitution_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "constitutionSavingThrowAdvantage", + "columnName": "constitution_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionSavingThrowProficiency", + "columnName": "constitution_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceScore", + "columnName": "intelligence_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "intelligenceSavingThrowAdvantage", + "columnName": "intelligence_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceSavingThrowProficiency", + "columnName": "intelligence_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomScore", + "columnName": "wisdom_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "wisdomSavingThrowAdvantage", + "columnName": "wisdom_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomSavingThrowProficiency", + "columnName": "wisdom_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaScore", + "columnName": "charisma_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "charismaSavingThrowAdvantage", + "columnName": "charisma_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaSavingThrowProficiency", + "columnName": "charisma_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "armorType", + "columnName": "armor_type", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "shieldBonus", + "columnName": "shield_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "naturalArmorBonus", + "columnName": "natural_armor_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "otherArmorDescription", + "columnName": "other_armor_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "hitDice", + "columnName": "hit_dice", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "hasCustomHP", + "columnName": "has_custom_hit_points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customHPDescription", + "columnName": "custom_hit_points_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "walkSpeed", + "columnName": "walk_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "burrowSpeed", + "columnName": "burrow_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "climbSpeed", + "columnName": "climb_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "flySpeed", + "columnName": "fly_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "canHover", + "columnName": "can_hover", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "swimSpeed", + "columnName": "swim_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasCustomSpeed", + "columnName": "has_custom_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "customSpeedDescription", + "columnName": "custom_speed_description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "challengeRating", + "columnName": "challenge_rating", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'1'" + }, + { + "fieldPath": "customChallengeRatingDescription", + "columnName": "custom_challenge_rating_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "customProficiencyBonus", + "columnName": "custom_proficiency_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blindsightRange", + "columnName": "blindsight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isBlindBeyondBlindsightRange", + "columnName": "is_blind_beyond_blindsight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "darkvisionRange", + "columnName": "darkvision_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "tremorsenseRange", + "columnName": "tremorsense_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "truesightRange", + "columnName": "truesight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "telepathyRange", + "columnName": "telepathy_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "understandsButDescription", + "columnName": "understands_but_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "skills", + "columnName": "skills", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageImmunities", + "columnName": "damage_immunities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageResistances", + "columnName": "damage_resistances", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageVulnerabilities", + "columnName": "damage_vulnerabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conditionImmunities", + "columnName": "condition_immunities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "languages", + "columnName": "languages", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "abilities", + "columnName": "abilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lairActions", + "columnName": "lair_actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "legendaryActions", + "columnName": "legendary_actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "regionalActions", + "columnName": "regional_actions", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'db1293d2f490940b55ca1f4f56b21b1a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json b/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json new file mode 100644 index 0000000..fada53b --- /dev/null +++ b/app/schemas/com.majinnaibu.monstercards.AppDatabase/2.json @@ -0,0 +1,499 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "6f1e7a2b2ab96fc4be4da1657a7a0138", + "entities": [ + { + "tableName": "monsters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `blindsight_range` INTEGER NOT NULL DEFAULT 0, `is_blind_beyond_blindsight_range` INTEGER NOT NULL DEFAULT false, `darkvision_range` INTEGER NOT NULL DEFAULT 0, `tremorsense_range` INTEGER NOT NULL DEFAULT 0, `truesight_range` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `skills` TEXT, `damage_immunities` TEXT, `damage_resistances` TEXT, `damage_vulnerabilities` TEXT, `condition_immunities` TEXT, `languages` TEXT, `abilities` TEXT, `actions` TEXT, `reactions` TEXT, `lair_actions` TEXT, `legendary_actions` TEXT, `regional_actions` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "strengthScore", + "columnName": "strength_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "strengthSavingThrowAdvantage", + "columnName": "strength_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "strengthSavingThrowProficiency", + "columnName": "strength_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "dexterityScore", + "columnName": "dexterity_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "dexteritySavingThrowAdvantage", + "columnName": "dexterity_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "dexteritySavingThrowProficiency", + "columnName": "dexterity_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionScore", + "columnName": "constitution_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "constitutionSavingThrowAdvantage", + "columnName": "constitution_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionSavingThrowProficiency", + "columnName": "constitution_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceScore", + "columnName": "intelligence_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "intelligenceSavingThrowAdvantage", + "columnName": "intelligence_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceSavingThrowProficiency", + "columnName": "intelligence_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomScore", + "columnName": "wisdom_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "wisdomSavingThrowAdvantage", + "columnName": "wisdom_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomSavingThrowProficiency", + "columnName": "wisdom_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaScore", + "columnName": "charisma_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "charismaSavingThrowAdvantage", + "columnName": "charisma_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaSavingThrowProficiency", + "columnName": "charisma_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "armorType", + "columnName": "armor_type", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "shieldBonus", + "columnName": "shield_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "naturalArmorBonus", + "columnName": "natural_armor_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "otherArmorDescription", + "columnName": "other_armor_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "hitDice", + "columnName": "hit_dice", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "hasCustomHP", + "columnName": "has_custom_hit_points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customHPDescription", + "columnName": "custom_hit_points_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "walkSpeed", + "columnName": "walk_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "burrowSpeed", + "columnName": "burrow_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "climbSpeed", + "columnName": "climb_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "flySpeed", + "columnName": "fly_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "canHover", + "columnName": "can_hover", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "swimSpeed", + "columnName": "swim_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasCustomSpeed", + "columnName": "has_custom_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "customSpeedDescription", + "columnName": "custom_speed_description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "challengeRating", + "columnName": "challenge_rating", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'1'" + }, + { + "fieldPath": "customChallengeRatingDescription", + "columnName": "custom_challenge_rating_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "customProficiencyBonus", + "columnName": "custom_proficiency_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blindsightRange", + "columnName": "blindsight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isBlindBeyondBlindsightRange", + "columnName": "is_blind_beyond_blindsight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "darkvisionRange", + "columnName": "darkvision_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "tremorsenseRange", + "columnName": "tremorsense_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "truesightRange", + "columnName": "truesight_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "telepathyRange", + "columnName": "telepathy_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "understandsButDescription", + "columnName": "understands_but_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "skills", + "columnName": "skills", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageImmunities", + "columnName": "damage_immunities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageResistances", + "columnName": "damage_resistances", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "damageVulnerabilities", + "columnName": "damage_vulnerabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conditionImmunities", + "columnName": "condition_immunities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "languages", + "columnName": "languages", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "abilities", + "columnName": "abilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lairActions", + "columnName": "lair_actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "legendaryActions", + "columnName": "legendary_actions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "regionalActions", + "columnName": "regional_actions", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "monsters", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END" + ], + "tableName": "monsters_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6f1e7a2b2ab96fc4be4da1657a7a0138')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.majinnaibu.monstercards.AppDatabase/3.json b/app/schemas/com.majinnaibu.monstercards.AppDatabase/3.json new file mode 100644 index 0000000..72ecdb6 --- /dev/null +++ b/app/schemas/com.majinnaibu.monstercards.AppDatabase/3.json @@ -0,0 +1,483 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "7c3c3ed79c7002102e7af7cfd21c23e0", + "entities": [ + { + "tableName": "monsters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "strengthScore", + "columnName": "strength_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "strengthSavingThrowAdvantage", + "columnName": "strength_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "strengthSavingThrowProficiency", + "columnName": "strength_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "dexterityScore", + "columnName": "dexterity_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "dexteritySavingThrowAdvantage", + "columnName": "dexterity_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "dexteritySavingThrowProficiency", + "columnName": "dexterity_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionScore", + "columnName": "constitution_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "constitutionSavingThrowAdvantage", + "columnName": "constitution_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionSavingThrowProficiency", + "columnName": "constitution_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceScore", + "columnName": "intelligence_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "intelligenceSavingThrowAdvantage", + "columnName": "intelligence_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceSavingThrowProficiency", + "columnName": "intelligence_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomScore", + "columnName": "wisdom_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "wisdomSavingThrowAdvantage", + "columnName": "wisdom_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomSavingThrowProficiency", + "columnName": "wisdom_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaScore", + "columnName": "charisma_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "charismaSavingThrowAdvantage", + "columnName": "charisma_saving_throw_advantage", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaSavingThrowProficiency", + "columnName": "charisma_saving_throw_proficiency", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "armorType", + "columnName": "armor_type", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'none'" + }, + { + "fieldPath": "shieldBonus", + "columnName": "shield_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "naturalArmorBonus", + "columnName": "natural_armor_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "otherArmorDescription", + "columnName": "other_armor_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "hitDice", + "columnName": "hit_dice", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "hasCustomHP", + "columnName": "has_custom_hit_points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customHPDescription", + "columnName": "custom_hit_points_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "walkSpeed", + "columnName": "walk_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "burrowSpeed", + "columnName": "burrow_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "climbSpeed", + "columnName": "climb_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "flySpeed", + "columnName": "fly_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "canHover", + "columnName": "can_hover", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "swimSpeed", + "columnName": "swim_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasCustomSpeed", + "columnName": "has_custom_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "customSpeedDescription", + "columnName": "custom_speed_description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "challengeRating", + "columnName": "challenge_rating", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'1'" + }, + { + "fieldPath": "customChallengeRatingDescription", + "columnName": "custom_challenge_rating_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "customProficiencyBonus", + "columnName": "custom_proficiency_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "telepathyRange", + "columnName": "telepathy_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "understandsButDescription", + "columnName": "understands_but_description", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "senses", + "columnName": "senses", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "skills", + "columnName": "skills", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "damageImmunities", + "columnName": "damage_immunities", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "damageResistances", + "columnName": "damage_resistances", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "damageVulnerabilities", + "columnName": "damage_vulnerabilities", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "conditionImmunities", + "columnName": "condition_immunities", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "languages", + "columnName": "languages", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "abilities", + "columnName": "abilities", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "lairActions", + "columnName": "lair_actions", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "legendaryActions", + "columnName": "legendary_actions", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + }, + { + "fieldPath": "regionalActions", + "columnName": "regional_actions", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'[]'" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "monsters", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END" + ], + "tableName": "monsters_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7c3c3ed79c7002102e7af7cfd21c23e0')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.majinnaibu.monstercards.AppDatabase/schema-notes.md b/app/schemas/com.majinnaibu.monstercards.AppDatabase/schema-notes.md new file mode 100644 index 0000000..24e4113 --- /dev/null +++ b/app/schemas/com.majinnaibu.monstercards.AppDatabase/schema-notes.md @@ -0,0 +1,73 @@ +## Monster +id: UUID as TEXT // doesn't exist in the iOS model +abilities: Set converted to JSON as TEXT +actions: Set converted to JSON as TEXT +alignment: String as TEXT +armor_type: Enum as TEXT +blindsight_range: int as INTEGER +burrow_speed: int as INTEGER +can_hover: boolean as INTEGER +challenge_rating: Enum as TEXT +charisma_saving_throw_advantage +charisma_saving_throw_proficiency +charisma_score: int as INTEGER +climb_speed: int as INTEGER +condition_immunities: Set converted to JSON as TEXT +constitution_saving_throw_advantage +constitution_saving_throw_proficiency +constitution_score: int as INTEGER +//other_armor_description: String as TEXT +custom_challenge_rating_description: String as TEXT +custom_hit_points_description: String +custom_proficiency_bonus: int as INTEGER +custom_speed_description: String as TEXT +damage_immunities: Set converted to JSON as TEXT +damage_resistances: Set converted to JSON as TEXT +damage_vulnerabilities: Set converted to JSON as TEXT +darkvision_range: int as INTEGER +dexterity_saving_throw_advantage +dexterity_saving_throw_proficiency +dexterity_score: int as INTEGER +fly_speed: int as INTEGER +has_custom_hit_points: boolean as INTEGER +has_custom_speed: boolean as INTEGER +// has_shield +hit_dice: int as INTEGER +intelligence_saving_throw_advantage +intelligence_saving_throw_proficiency +intelligence_score: int as INTEGER +is_blind_beyond_blindsight_range: boolean as INTEGER +lair_actions +languages: Set converted to JSON as TEXT +legendary_actions +name: String as TEXT +natural_armor_bonus: int as INTEGER +other_armor_description: String as TEXT +reactions +regional_actions +// senses +shield_bonus: int as INTEGER +size: String as TEXT +strength_saving_throw_advantage +strength_saving_throw_proficiency +strength_score: int as INTEGER +tag: String as TEXT // subtype || tag +swim_speed: int as INTEGER +telepathy_range: int as INTEGER +tremorsense_range: int as INTEGER +truesight_range: int as INTEGER +type: String as TEXT +understands_but_description: String as TEXT +walk_speed: int as INTEGER +wisdom_saving_throw_advantage +wisdom_saving_throw_proficiency +wisdom_score: int as INTEGER + +// tracked as relationship (don't do this) +skills: Set converted to JSON as TEXT + +## Skill +// ability_score_name String defaults to "strength" +// advantage String defaults to "none" +// name String defaults to "" +// proficiency String defaults to "none" diff --git a/app/src/androidTest/java/com/majinnaibu/monstercards/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/majinnaibu/monstercards/ExampleInstrumentedTest.java new file mode 100644 index 0000000..c07fd1a --- /dev/null +++ b/app/src/androidTest/java/com/majinnaibu/monstercards/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.majinnaibu.monstercards; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.majinnaibu.monstercards", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/debug/java/com/majinnaibu/monstercards/init/AppCenterInitializer.java b/app/src/debug/java/com/majinnaibu/monstercards/init/AppCenterInitializer.java new file mode 100644 index 0000000..ef577f2 --- /dev/null +++ b/app/src/debug/java/com/majinnaibu/monstercards/init/AppCenterInitializer.java @@ -0,0 +1,22 @@ +package com.majinnaibu.monstercards.init; + +import android.app.Application; + +import com.majinnaibu.monstercards.BuildConfig; +import com.microsoft.appcenter.AppCenter; +import com.microsoft.appcenter.analytics.Analytics; +import com.microsoft.appcenter.crashes.Crashes; + +public class AppCenterInitializer { + + public static void init(Application app) { + if (BuildConfig.APPCENTER_SECRET != null && !"".equals(BuildConfig.APPCENTER_SECRET)) { + AppCenter.start( + app, + BuildConfig.APPCENTER_SECRET, + Analytics.class, + Crashes.class + ); + } + } +} \ No newline at end of file diff --git a/app/src/debug/java/com/majinnaibu/monstercards/init/FlipperInitializer.java b/app/src/debug/java/com/majinnaibu/monstercards/init/FlipperInitializer.java new file mode 100644 index 0000000..dd04f40 --- /dev/null +++ b/app/src/debug/java/com/majinnaibu/monstercards/init/FlipperInitializer.java @@ -0,0 +1,42 @@ +package com.majinnaibu.monstercards.init; + + +import android.content.Context; +import android.os.Bundle; + +import androidx.navigation.NavController; +import androidx.navigation.NavDestination; + +import com.facebook.flipper.android.AndroidFlipperClient; +import com.facebook.flipper.android.utils.FlipperUtils; +import com.facebook.flipper.core.FlipperClient; +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; +import com.facebook.flipper.plugins.inspector.DescriptorMapping; +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; +import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin; +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; +import com.facebook.soloader.SoLoader; +import com.google.gson.Gson; +import com.majinnaibu.monstercards.BuildConfig; + +public class FlipperInitializer { + + public static void init(Context ctx) { + SoLoader.init(ctx, false); + + if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(ctx)) { + final FlipperClient client = AndroidFlipperClient.getInstance(ctx); + client.addPlugin(new InspectorFlipperPlugin(ctx, DescriptorMapping.withDefaults())); + client.addPlugin(new DatabasesFlipperPlugin(ctx)); + client.addPlugin(new SharedPreferencesFlipperPlugin(ctx)); + client.addPlugin(NavigationFlipperPlugin.getInstance()); + client.start(); + } + } + + public static void sendNavigationEvent(NavController controller, NavDestination destination, Bundle arguments) { + Gson gson = new Gson(); + String json = gson.toJson(arguments != null ? arguments : new Bundle()); + NavigationFlipperPlugin.getInstance().sendNavigationEvent(String.format("%s:%s", destination.getLabel(), json), null, null); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..eaa2137 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java b/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java new file mode 100644 index 0000000..7b27aa6 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java @@ -0,0 +1,30 @@ +package com.majinnaibu.monstercards; + +import androidx.room.Database; +import androidx.room.RoomDatabase; +import androidx.room.TypeConverters; + +import com.majinnaibu.monstercards.data.MonsterDAO; +import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter; +import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter; +import com.majinnaibu.monstercards.data.converters.ListOfTraitsConverter; +import com.majinnaibu.monstercards.data.converters.SetOfLanguageConverter; +import com.majinnaibu.monstercards.data.converters.SetOfSkillConverter; +import com.majinnaibu.monstercards.data.converters.SetOfStringConverter; +import com.majinnaibu.monstercards.data.converters.UUIDConverter; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.models.MonsterFTS; + +@Database(entities = {Monster.class, MonsterFTS.class}, version = 3) +@TypeConverters({ + ArmorTypeConverter.class, + ChallengeRatingConverter.class, + ListOfTraitsConverter.class, + SetOfLanguageConverter.class, + SetOfSkillConverter.class, + SetOfStringConverter.class, + UUIDConverter.class, +}) +public abstract class AppDatabase extends RoomDatabase { + public abstract MonsterDAO monsterDAO(); +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java b/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java new file mode 100644 index 0000000..2df5ace --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java @@ -0,0 +1,118 @@ +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.annotation.Nullable; +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) { + getOnBackPressedDispatcher().onBackPressed(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @SuppressWarnings("ConstantConditions") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + AppCenterInitializer.init(getApplication()); + setContentView(R.layout.activity_main); + BottomNavigationView navView = findViewById(R.id.nav_view); + // Passing each menu ID as a set of Ids because each + // menu should be considered as top level destinations. + AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder( + R.id.navigation_search, + R.id.navigation_dashboard, + R.id.navigation_collections, + R.id.navigation_library) + .build(); + NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment); + NavController navController = navHostFragment.getNavController(); + navController.addOnDestinationChangedListener(FlipperInitializer::sendNavigationEvent); + NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); + NavigationUI.setupWithNavController(navView, navController); + onNewIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + String json = readMonsterJSONFromIntent(intent); + if (!StringHelper.isNullOrEmpty(json)) { + NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment)); + NavController navController = navHostFragment.getNavController(); + NavDirections action = MobileNavigationDirections.actionGlobalMonsterImportFragment(json); + navController.navigate(action); + } + } + + @Nullable + private String readMonsterJSONFromIntent(@NonNull 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", action, type)); + } + if (uri == null) { + return null; + } + json = readContentsOfUri(uri); + if (StringHelper.isNullOrEmpty(json)) { + return null; + } + return json; + } + + @Nullable + 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/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java b/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java new file mode 100644 index 0000000..575720e --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java @@ -0,0 +1,82 @@ +package com.majinnaibu.monstercards; + +import android.app.Application; +import android.content.res.Configuration; + +import androidx.annotation.NonNull; +import androidx.room.Room; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.init.FlipperInitializer; + +public class MonsterCardsApplication extends Application { + + private static final Migration MIGRATION_1_2 = new Migration(1, 2) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // rename table monster to monsters + database.execSQL("ALTER TABLE monster RENAME TO monsters"); + // create the fts view + database.execSQL("CREATE VIRTUAL TABLE IF NOT EXISTS `monsters_fts` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)"); + // build the initial full text search index + database.execSQL("INSERT INTO monsters_fts(monsters_fts) VALUES('rebuild')"); + + } + }; + private static final Migration MIGRATION_2_3 = new Migration(2, 3) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // Add the senses column + database.execSQL("ALTER TABLE monsters ADD COLUMN 'senses' TEXT DEFAULT '[]'"); + database.execSQL("CREATE TABLE new_monsters (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', PRIMARY KEY(`id`))"); + database.execSQL("INSERT INTO new_monsters(id, name, size, type, subtype, alignment, strength_score, strength_saving_throw_advantage, strength_saving_throw_proficiency, dexterity_score, dexterity_saving_throw_advantage, dexterity_saving_throw_proficiency, constitution_score, constitution_saving_throw_advantage, constitution_saving_throw_proficiency, intelligence_score, intelligence_saving_throw_advantage, intelligence_saving_throw_proficiency, wisdom_score, wisdom_saving_throw_advantage, wisdom_saving_throw_proficiency, charisma_score, charisma_saving_throw_advantage, charisma_saving_throw_proficiency, armor_type, shield_bonus, natural_armor_bonus, other_armor_description, hit_dice, has_custom_hit_points, custom_hit_points_description, walk_speed, burrow_speed, climb_speed, fly_speed, can_hover, swim_speed, has_custom_speed, custom_speed_description, challenge_rating, custom_challenge_rating_description, custom_proficiency_bonus, telepathy_range, understands_but_description, senses, skills, damage_immunities, damage_resistances, damage_vulnerabilities, condition_immunities, languages, abilities, actions, reactions, lair_actions, legendary_actions, regional_actions) SELECT id, name, size, type, subtype, alignment, strength_score, strength_saving_throw_advantage, strength_saving_throw_proficiency, dexterity_score, dexterity_saving_throw_advantage, dexterity_saving_throw_proficiency, constitution_score, constitution_saving_throw_advantage, constitution_saving_throw_proficiency, intelligence_score, intelligence_saving_throw_advantage, intelligence_saving_throw_proficiency, wisdom_score, wisdom_saving_throw_advantage, wisdom_saving_throw_proficiency, charisma_score, charisma_saving_throw_advantage, charisma_saving_throw_proficiency, armor_type, shield_bonus, natural_armor_bonus, other_armor_description, hit_dice, has_custom_hit_points, custom_hit_points_description, walk_speed, burrow_speed, climb_speed, fly_speed, can_hover, swim_speed, has_custom_speed, custom_speed_description, challenge_rating, custom_challenge_rating_description, custom_proficiency_bonus, telepathy_range, understands_but_description, senses, skills, damage_immunities, damage_resistances, damage_vulnerabilities, condition_immunities, languages, abilities, actions, reactions, lair_actions, legendary_actions, regional_actions FROM monsters"); + database.execSQL("DROP TABLE monsters"); + database.execSQL("ALTER TABLE new_monsters RENAME TO monsters"); + } + }; + private MonsterRepository m_monsterLibraryRepository; + + + public MonsterCardsApplication() { + } + + public MonsterRepository getMonsterRepository() { + return m_monsterLibraryRepository; + } + + // Called when the application is starting, before any other application objects have been created. + // Overriding this method is totally optional! + @Override + public void onCreate() { + super.onCreate(); + // Required initialization logic here! + + FlipperInitializer.init(this); + + // .fallbackToDestructiveMigration() + AppDatabase m_db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "monsters") + .addMigrations(MIGRATION_1_2) + .addMigrations(MIGRATION_2_3) + .fallbackToDestructiveMigrationOnDowngrade() +// .fallbackToDestructiveMigration() + .build(); + m_monsterLibraryRepository = new MonsterRepository(m_db); + } + + // Called by the system when the device configuration changes while your component is running. + // Overriding this method is totally optional! + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + // This is called when the overall system is running low on memory, + // and would like actively running processes to tighten their belts. + // Overriding this method is totally optional! + @Override + public void onLowMemory() { + super.onLowMemory(); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/DevContent.java b/app/src/main/java/com/majinnaibu/monstercards/data/DevContent.java new file mode 100644 index 0000000..0145afe --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/DevContent.java @@ -0,0 +1,113 @@ +package com.majinnaibu.monstercards.data; + +import androidx.annotation.NonNull; + +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.data.enums.AdvantageType; +import com.majinnaibu.monstercards.data.enums.ArmorType; +import com.majinnaibu.monstercards.data.enums.ChallengeRating; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.models.Skill; +import com.majinnaibu.monstercards.models.Trait; + +@SuppressWarnings("unused") +public final class DevContent { + @NonNull + public static Monster createSampleMonster() { + Monster monster = new Monster(); + // Name + monster.name = "Pixie"; + // Meta + monster.size = "tiny"; + monster.type = "fey"; + monster.subtype = ""; + monster.alignment = "neutral good"; + monster.armorType = ArmorType.NONE; + // Armor & Armor Class + monster.shieldBonus = 0; + monster.naturalArmorBonus = 7; + monster.otherArmorDescription = "14"; + // Hit Points + monster.hitDice = 1; + monster.hasCustomHP = false; + monster.customHPDescription = "11 (2d8 + 2)"; + monster.walkSpeed = 10; + monster.burrowSpeed = 0; + monster.climbSpeed = 0; + monster.flySpeed = 30; + monster.canHover = false; + monster.swimSpeed = 0; + monster.hasCustomSpeed = false; + monster.customSpeedDescription = "30 ft., swim 30 ft."; + // Ability Scores + monster.strengthScore = Integer.parseInt("2"); + monster.dexterityScore = Integer.parseInt("20"); + monster.constitutionScore = Integer.parseInt("8"); + monster.intelligenceScore = Integer.parseInt("10"); + monster.wisdomScore = Integer.parseInt("14"); + monster.charismaScore = Integer.parseInt("15"); +// monster.strengthScore = 10; +// monster.dexterityScore = 10; +// monster.constitutionScore = 10; +// monster.intelligenceScore = 10; +// monster.wisdomScore = 10; +// monster.charismaScore = 10; + + // Saving Throws + monster.strengthSavingThrowAdvantage = AdvantageType.NONE; + monster.strengthSavingThrowProficiency = ProficiencyType.NONE; + monster.dexteritySavingThrowAdvantage = AdvantageType.ADVANTAGE; + monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT; + monster.constitutionSavingThrowAdvantage = AdvantageType.DISADVANTAGE; + monster.constitutionSavingThrowProficiency = ProficiencyType.EXPERTISE; + monster.intelligenceSavingThrowAdvantage = AdvantageType.NONE; + monster.intelligenceSavingThrowProficiency = ProficiencyType.EXPERTISE; + monster.wisdomSavingThrowAdvantage = AdvantageType.ADVANTAGE; + monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT; + monster.charismaSavingThrowAdvantage = AdvantageType.DISADVANTAGE; + monster.charismaSavingThrowProficiency = ProficiencyType.NONE; + //Skills + monster.skills.add(new Skill("perception", AbilityScore.WISDOM)); + monster.skills.add(new Skill("stealth", AbilityScore.DEXTERITY)); + // Damage Types + monster.damageImmunities.add("force"); + monster.damageImmunities.add("lightning"); + monster.damageImmunities.add("poison"); + monster.damageResistances.add("cold"); + monster.damageResistances.add("fire"); + monster.damageResistances.add("piercing"); + monster.damageVulnerabilities.add("acid"); + monster.damageVulnerabilities.add("bludgeoning"); + monster.damageVulnerabilities.add("necrotic"); + // Condition Immunities + monster.conditionImmunities.add("blinded"); + // Senses + monster.senses.add("blindsight 10 ft. (blind beyond this range)"); + monster.senses.add("darkvision 20 ft."); + monster.senses.add("tremorsense 30 ft."); + monster.senses.add("truesight 40 ft."); + monster.telepathyRange = 20; + monster.understandsButDescription = "doesn't care"; + // Languages + monster.languages.add(new Language("English", true)); + monster.languages.add(new Language("Steve", false)); + monster.languages.add(new Language("Spanish", true)); + monster.languages.add(new Language("French", true)); + monster.languages.add(new Language("Mermataur", false)); + monster.languages.add(new Language("Goldfish", false)); + // Challenge Rating + monster.challengeRating = ChallengeRating.CUSTOM; + monster.customChallengeRatingDescription = "Infinite (0XP)"; + monster.customProficiencyBonus = 4; + // Abilities + monster.abilities.add(new Trait("Spellcasting", "The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC [WIS SAVE], [WIS ATK] to hit with spell attacks). The acolyte has following cleric spells prepared:\n\n\n> Cantrips (at will): _light, sacred flame, thaumaturgy_\n> 1st level (3 slots): _bless, cure wounds, sanctuary_")); + monster.abilities.add(new Trait("Amphibious", "The dragon can breathe air and water.")); + monster.abilities.add(new Trait("Legendary Resistance (3/Day)", "If the dragon fails a saving throw, it can choose to succeed instead.")); + // Actions + monster.actions.add(new Trait("Club", "_Melee Weapon Attack:_ [STR ATK] to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage.")); + + return monster; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java b/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java new file mode 100644 index 0000000..9671790 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/MonsterDAO.java @@ -0,0 +1,39 @@ +package com.majinnaibu.monstercards.data; + + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.majinnaibu.monstercards.models.Monster; + +import java.util.List; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; + +@Dao +public interface MonsterDAO { + @Query("SELECT * FROM monsters") + Flowable> getAll(); + + @Query("SELECT * FROM monsters WHERE id IN (:monsterIds)") + Flowable> loadAllByIds(String[] monsterIds); + + @Query("SELECT * FROM monsters WHERE name LIKE :name LIMIT 1") + Flowable findByName(String name); + + @Query("SELECT monsters.* FROM monsters JOIN monsters_fts ON monsters.oid = monsters_fts.docid WHERE monsters_fts MATCH :searchText") + Flowable> search(String searchText); + + @Insert + Completable insertAll(Monster... monsters); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + Completable save(Monster... monsters); + + @Delete + Completable delete(Monster monster); +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java b/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java new file mode 100644 index 0000000..842fb2d --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java @@ -0,0 +1,112 @@ +package com.majinnaibu.monstercards.data; + +import androidx.annotation.NonNull; + +import com.majinnaibu.monstercards.AppDatabase; +import com.majinnaibu.monstercards.helpers.StringHelper; +import com.majinnaibu.monstercards.models.Monster; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@SuppressWarnings("ResultOfMethodCallIgnored") +public class MonsterRepository { + + private final AppDatabase m_db; + + public MonsterRepository(@NonNull AppDatabase db) { + m_db = db; + } + + public Flowable> getMonsters() { + return m_db.monsterDAO() + .getAll() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public Flowable> searchMonsters(String searchText) { + return m_db.monsterDAO() + .getAll() + .map(monsters -> { + ArrayList filteredMonsters = new ArrayList<>(); + for (Monster monster : monsters) { + if (Helpers.monsterMatchesSearch(monster, searchText)) { + filteredMonsters.add(monster); + } + } + return (List) filteredMonsters; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public Flowable getMonster(@NonNull UUID monsterId) { + return m_db.monsterDAO() + .loadAllByIds(new String[]{monsterId.toString()}) + .map( + monsters -> { + if (monsters.size() > 0) { + return monsters.get(0); + } else { + return null; + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public Completable addMonster(Monster monster) { + Completable result = m_db.monsterDAO().insertAll(monster); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + + public Completable deleteMonster(Monster monster) { + Completable result = m_db.monsterDAO().delete(monster); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + + public Completable saveMonster(Monster monster) { + Completable result = m_db.monsterDAO().save(monster); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + + private static class Helpers { + static boolean monsterMatchesSearch(Monster monster, String searchText) { + if (StringHelper.isNullOrEmpty(searchText)) { + return true; + } + + if (StringHelper.containsCaseInsensitive(monster.name, searchText)) { + return true; + } + + if (StringHelper.containsCaseInsensitive(monster.size, searchText)) { + return true; + } + + if (StringHelper.containsCaseInsensitive(monster.type, searchText)) { + return true; + } + + if (StringHelper.containsCaseInsensitive(monster.subtype, searchText)) { + return true; + } + + if (StringHelper.containsCaseInsensitive(monster.alignment, searchText)) { + return true; + } + + return false; + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/converters/ArmorTypeConverter.java b/app/src/main/java/com/majinnaibu/monstercards/data/converters/ArmorTypeConverter.java new file mode 100644 index 0000000..d6d1d59 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/converters/ArmorTypeConverter.java @@ -0,0 +1,19 @@ +package com.majinnaibu.monstercards.data.converters; + +import androidx.annotation.NonNull; +import androidx.room.TypeConverter; + +import com.majinnaibu.monstercards.data.enums.ArmorType; + +public class ArmorTypeConverter { + + @TypeConverter + public static String fromArmorType(@NonNull ArmorType armorType) { + return armorType.stringValue; + } + + @TypeConverter + public static ArmorType armorTypeFromStringValue(String stringValue) { + return ArmorType.valueOfString(stringValue); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/converters/ChallengeRatingConverter.java b/app/src/main/java/com/majinnaibu/monstercards/data/converters/ChallengeRatingConverter.java new file mode 100644 index 0000000..3d9712c --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/converters/ChallengeRatingConverter.java @@ -0,0 +1,19 @@ +package com.majinnaibu.monstercards.data.converters; + +import androidx.annotation.NonNull; +import androidx.room.TypeConverter; + +import com.majinnaibu.monstercards.data.enums.ChallengeRating; + +public class ChallengeRatingConverter { + + @TypeConverter + public static String fromChallengeRating(@NonNull ChallengeRating challengeRating) { + return challengeRating.stringValue; + } + + @TypeConverter + public static ChallengeRating challengeRatingFromStringValue(String stringValue) { + return ChallengeRating.valueOfString(stringValue); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/converters/ListOfTraitsConverter.java b/app/src/main/java/com/majinnaibu/monstercards/data/converters/ListOfTraitsConverter.java new file mode 100644 index 0000000..7a0e2ef --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/converters/ListOfTraitsConverter.java @@ -0,0 +1,27 @@ +package com.majinnaibu.monstercards.data.converters; + +import androidx.room.TypeConverter; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.majinnaibu.monstercards.models.Trait; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class ListOfTraitsConverter { + @TypeConverter + public static String fromListOfTraits(List traits) { + Gson gson = new Gson(); + return gson.toJson(traits); + } + + @TypeConverter + public static List listOfTraitsFromString(String string) { + Gson gson = new Gson(); + Type setType = new TypeToken>() { + }.getType(); + return gson.fromJson(string, setType); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfLanguageConverter.java b/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfLanguageConverter.java new file mode 100644 index 0000000..b076a4d --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfLanguageConverter.java @@ -0,0 +1,28 @@ +package com.majinnaibu.monstercards.data.converters; + +import androidx.room.TypeConverter; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.majinnaibu.monstercards.models.Language; + +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +public class SetOfLanguageConverter { + + @TypeConverter + public static String fromSetOfLanguage(Set languages) { + Gson gson = new Gson(); + return gson.toJson(languages); + } + + @TypeConverter + public static Set setOfLanguageFromString(String string) { + Gson gson = new Gson(); + Type setType = new TypeToken>() { + }.getType(); + return gson.fromJson(string, setType); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfSkillConverter.java b/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfSkillConverter.java new file mode 100644 index 0000000..6f2ee90 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfSkillConverter.java @@ -0,0 +1,28 @@ +package com.majinnaibu.monstercards.data.converters; + +import androidx.room.TypeConverter; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.majinnaibu.monstercards.models.Skill; + +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +public class SetOfSkillConverter { + + @TypeConverter + public static String fromSetOfSkill(Set skills) { + Gson gson = new Gson(); + return gson.toJson(skills); + } + + @TypeConverter + public static Set setOfSkillFromString(String string) { + Gson gson = new Gson(); + Type setType = new TypeToken>() { + }.getType(); + return gson.fromJson(string, setType); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfStringConverter.java b/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfStringConverter.java new file mode 100644 index 0000000..c09dcaa --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/converters/SetOfStringConverter.java @@ -0,0 +1,27 @@ +package com.majinnaibu.monstercards.data.converters; + +import androidx.room.TypeConverter; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +public class SetOfStringConverter { + + @TypeConverter + public static String fromSetOfString(Set strings) { + Gson gson = new Gson(); + return gson.toJson(strings); + } + + @TypeConverter + public static Set setOfStringFromString(String string) { + Gson gson = new Gson(); + Type setType = new TypeToken>() { + }.getType(); + return gson.fromJson(string, setType); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/converters/UUIDConverter.java b/app/src/main/java/com/majinnaibu/monstercards/data/converters/UUIDConverter.java new file mode 100644 index 0000000..d775179 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/converters/UUIDConverter.java @@ -0,0 +1,20 @@ +package com.majinnaibu.monstercards.data.converters; + +import androidx.annotation.NonNull; +import androidx.room.TypeConverter; + +import java.util.UUID; + +public class UUIDConverter { + + @NonNull + @TypeConverter + public static String fromUUID(@NonNull UUID uuid) { + return uuid.toString(); + } + + @TypeConverter + public static UUID uuidFromString(String string) { + return UUID.fromString(string); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java b/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java new file mode 100644 index 0000000..5a0b24e --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/enums/AbilityScore.java @@ -0,0 +1,31 @@ +package com.majinnaibu.monstercards.data.enums; + +@SuppressWarnings("unused") +public enum AbilityScore { + STRENGTH("strength", "Strength", "STR"), + DEXTERITY("dexterity", "Dexterity", "DEX"), + CONSTITUTION("constitution", "Constitution", "CON"), + INTELLIGENCE("intelligence", "Intelligence", "INT"), + WISDOM("wisdom", "Wisdom", "WIS"), + CHARISMA("charisma", "Charisma", "CHA"), + ; + + public final String displayName; + public final String shortDisplayName; + public final String stringValue; + + AbilityScore(String stringValue, String displayName, String shortDisplayName) { + this.displayName = displayName; + this.stringValue = stringValue; + this.shortDisplayName = shortDisplayName; + } + + public static AbilityScore valueOfString(String string) { + for (AbilityScore abilityScore : values()) { + if (abilityScore.stringValue.equals(string)) { + return abilityScore; + } + } + return AbilityScore.STRENGTH; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/enums/AdvantageType.java b/app/src/main/java/com/majinnaibu/monstercards/data/enums/AdvantageType.java new file mode 100644 index 0000000..1405696 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/enums/AdvantageType.java @@ -0,0 +1,27 @@ +package com.majinnaibu.monstercards.data.enums; + +public enum AdvantageType { + NONE("none", "None", ""), + ADVANTAGE("advantage", "Advantage", "A"), + DISADVANTAGE("disadvantage", "Disadvantage", "D"), + ; + + public final String displayName; + public final String stringValue; + public final String label; + + AdvantageType(String stringValue, String displayName, String label) { + this.displayName = displayName; + this.stringValue = stringValue; + this.label = label; + } + + public static AdvantageType valueOfString(String string) { + for (AdvantageType advantageType : values()) { + if (advantageType.stringValue.equals(string)) { + return advantageType; + } + } + return AdvantageType.NONE; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/enums/ArmorType.java b/app/src/main/java/com/majinnaibu/monstercards/data/enums/ArmorType.java new file mode 100644 index 0000000..67cf8ae --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/enums/ArmorType.java @@ -0,0 +1,41 @@ +package com.majinnaibu.monstercards.data.enums; + +@SuppressWarnings("unused") +public enum ArmorType { + NONE("none", "None", 10), + NATURAL_ARMOR("natural armor", "Natural Armor", 10), + MAGE_ARMOR("mage armor", "Mage Armor", 10), + PADDED("padded", "Padded", 11), + LEATHER("leather", "Leather", 11), + STUDDED_LEATHER("studded", "Studded Leather", 12), + HIDE("hide", "Hide", 12), + CHAIN_SHIRT("chain shirt", "Chain Shirt", 13), + SCALE_MAIL("scale mail", "Scale Mail", 14), + BREASTPLATE("breastplate", "Breastplate", 14), + HALF_PLATE("half plate", "Half Plate", 15), + RING_MAIL("ring mail", "Ring Mail", 14), + CHAIN_MAIL("chain mail", "Chain Mail", 16), + SPLINT_MAIL("splint", "Splint Mail", 17), + PLATE_MAIL("plate", "Plate Mail", 18), + OTHER("other", "Other", 10), + ; + + public final String displayName; + public final String stringValue; + public final int baseArmorClass; + + ArmorType(String stringValue, String displayName, int baseArmorClass) { + this.displayName = displayName; + this.stringValue = stringValue; + this.baseArmorClass = baseArmorClass; + } + + public static ArmorType valueOfString(String string) { + for (ArmorType armorType : values()) { + if (armorType.stringValue.equals(string)) { + return armorType; + } + } + return ArmorType.NONE; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/enums/ChallengeRating.java b/app/src/main/java/com/majinnaibu/monstercards/data/enums/ChallengeRating.java new file mode 100644 index 0000000..be8d87c --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/enums/ChallengeRating.java @@ -0,0 +1,60 @@ +package com.majinnaibu.monstercards.data.enums; + +@SuppressWarnings("unused") +public enum ChallengeRating { + CUSTOM("custom", "Custom", 0), + ZERO("zero", "0 (10 XP)", 2), + ONE_EIGHTH("1/8", "1/8 (25 XP)", 2), + ONE_QUARTER("1/4", "1/4 (50 XP)", 2), + ONE_HALF("1/2", "1/2 (100 XP)", 2), + ONE("1", "1 (200 XP)", 2), + TWO("2", "2 (450 XP)", 2), + THREE("3", "3 (700 XP)", 2), + FOUR("4", "4 (1,100 XP)", 2), + FIVE("5", "5 (1,800 XP)", 3), + SIX("6", "6 (2,300 XP)", 3), + SEVEN("7", "7 (2,900 XP)", 3), + EIGHT("8", "8 (3,900 XP)", 3), + NINE("9", "9 (5,000 XP)", 4), + TEN("10", "10 (5,900 XP)", 4), + ELEVEN("11", "11 (7,200 XP)", 4), + TWELVE("12", "12 (8,400 XP)", 4), + THIRTEEN("13", "13 (10,000 XP)", 5), + FOURTEEN("14", "14 (11,500 XP)", 5), + FIFTEEN("15", "15 (13,000 XP)", 5), + SIXTEEN("16", "16 (15,000 XP)", 5), + SEVENTEEN("17", "17 (18,000 XP)", 6), + EIGHTEEN("18", "18 (20,000 XP)", 6), + NINETEEN("19", "19 (22,000 XP)", 6), + TWENTY("20", "20 (25,000 XP)", 6), + TWENTY_ONE("21", "21 (33,000 XP)", 7), + TWENTY_TWO("22", "22 (41,000 XP)", 7), + TWENTY_THREE("23", "23 (50,000 XP)", 7), + TWENTY_FOUR("24", "24 (62,000 XP)", 7), + TWENTY_FIVE("25", "25 (75,000 XP)", 8), + TWENTY_SIX("26", "26 (90,000 XP)", 8), + TWENTY_SEVEN("27", "27 (105,000 XP)", 8), + TWENTY_EIGHT("28", "28 (120,000 XP)", 8), + TWENTY_NINE("29", "29 (135,000 XP)", 9), + THIRTY("30", "30 (155,000 XP)", 9), + ; + + public final String displayName; + public final String stringValue; + public final int proficiencyBonus; + + ChallengeRating(String stringValue, String displayName, int proficiencyBonus) { + this.displayName = displayName; + this.stringValue = stringValue; + this.proficiencyBonus = proficiencyBonus; + } + + public static ChallengeRating valueOfString(String string) { + for (ChallengeRating challengeRating : values()) { + if (challengeRating.stringValue.equals(string)) { + return challengeRating; + } + } + return ChallengeRating.ONE; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/enums/ProficiencyType.java b/app/src/main/java/com/majinnaibu/monstercards/data/enums/ProficiencyType.java new file mode 100644 index 0000000..9236ad0 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/enums/ProficiencyType.java @@ -0,0 +1,27 @@ +package com.majinnaibu.monstercards.data.enums; + +public enum ProficiencyType { + NONE("none", "None", ""), + PROFICIENT("proficient", "Proficient", "P"), + EXPERTISE("expertise", "Expertise", "Ex"), + ; + + public final String displayName; + public final String stringValue; + public final String label; + + ProficiencyType(String stringValue, String displayName, String label) { + this.displayName = displayName; + this.stringValue = stringValue; + this.label = label; + } + + public static ProficiencyType valueOfString(String string) { + for (ProficiencyType proficiencyType : values()) { + if (proficiencyType.stringValue.equals(string)) { + return proficiencyType; + } + } + return ProficiencyType.NONE; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/enums/StringType.java b/app/src/main/java/com/majinnaibu/monstercards/data/enums/StringType.java new file mode 100644 index 0000000..67b7497 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/enums/StringType.java @@ -0,0 +1,9 @@ +package com.majinnaibu.monstercards.data.enums; + +public enum StringType { + CONDITION_IMMUNITY, + DAMAGE_IMMUNITY, + DAMAGE_RESISTANCE, + DAMAGE_VULNERABILITY, + SENSE +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/data/enums/TraitType.java b/app/src/main/java/com/majinnaibu/monstercards/data/enums/TraitType.java new file mode 100644 index 0000000..ab7e858 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/data/enums/TraitType.java @@ -0,0 +1,10 @@ +package com.majinnaibu.monstercards.data.enums; + +public enum TraitType { + ABILITY, + ACTION, + LAIR_ACTION, + LEGENDARY_ACTION, + REGIONAL_ACTION, + REACTIONS +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/helpers/ArrayHelper.java b/app/src/main/java/com/majinnaibu/monstercards/helpers/ArrayHelper.java new file mode 100644 index 0000000..2a5d044 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/helpers/ArrayHelper.java @@ -0,0 +1,17 @@ +package com.majinnaibu.monstercards.helpers; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +public final class ArrayHelper { + public static int indexOf(@NonNull Object[] array, Object target) { + for (int index = 0; index < array.length; index++) { + if (Objects.equals(array[index], target)) { + return index; + } + } + + return -1; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java b/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java new file mode 100644 index 0000000..bf1e931 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/helpers/CommonMarkHelper.java @@ -0,0 +1,27 @@ +package com.majinnaibu.monstercards.helpers; + +import org.commonmark.node.Document; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; + +public final class CommonMarkHelper { + public static String toHtml(String rawCommonMark) { + Parser parser = Parser.builder().build(); + Node document = parser.parse(rawCommonMark); + Node parent1 = document.getFirstChild(); + Node parent2 = document.getLastChild(); + if (parent1 == parent2 && parent1 instanceof Paragraph) { + document = new Document(); + Node child = parent1.getFirstChild(); + while (child != null) { + Node nextChild = child.getNext(); + document.appendChild(child); + child = nextChild;//child.getNext(); + } + } + HtmlRenderer renderer = HtmlRenderer.builder().build(); + return renderer.render(document); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java b/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java new file mode 100644 index 0000000..de7e0e8 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/helpers/MonsterImportHelper.java @@ -0,0 +1,311 @@ +package com.majinnaibu.monstercards.helpers; + +import androidx.annotation.NonNull; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter; +import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter; +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.data.enums.AdvantageType; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.models.Skill; +import com.majinnaibu.monstercards.models.Trait; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class MonsterImportHelper { + @NonNull + public static Monster fromJSON(String json) { + JsonObject rootDict = JsonParser.parseString(json).getAsJsonObject(); + + Monster monster = new Monster(); + monster.name = Helpers.getString(rootDict, "name"); + monster.size = Helpers.getString(rootDict, "size"); + monster.type = Helpers.getString(rootDict, "type"); + monster.subtype = Helpers.getString(rootDict, "tag"); + monster.alignment = Helpers.getString(rootDict, "alignment"); + monster.hitDice = Helpers.getInt(rootDict, "hitDice"); + monster.armorType = ArmorTypeConverter.armorTypeFromStringValue(Helpers.getString(rootDict, "armorName")); + monster.shieldBonus = Helpers.getInt(rootDict, "shieldBonus"); + monster.naturalArmorBonus = Helpers.getInt(rootDict, "natArmorBonus"); + monster.otherArmorDescription = Helpers.getString(rootDict, "otherArmorDesc"); + monster.walkSpeed = Helpers.getInt(rootDict, "speed"); + monster.burrowSpeed = Helpers.getInt(rootDict, "burrowSpeed"); + monster.climbSpeed = Helpers.getInt(rootDict, "climbSpeed"); + monster.flySpeed = Helpers.getInt(rootDict, "flySpeed"); + monster.canHover = Helpers.getBool(rootDict, "hover"); + monster.swimSpeed = Helpers.getInt(rootDict, "swimSpeed"); + monster.hasCustomHP = Helpers.getBool(rootDict, "customHP"); + monster.hasCustomSpeed = Helpers.getBool(rootDict, "customSpeed"); + monster.customHPDescription = Helpers.getString(rootDict, "hpText"); + monster.customSpeedDescription = Helpers.getString(rootDict, "speedDesc"); + monster.strengthScore = Helpers.getInt(rootDict, "strPoints"); + monster.dexterityScore = Helpers.getInt(rootDict, "dexPoints"); + monster.constitutionScore = Helpers.getInt(rootDict, "conPoints"); + monster.intelligenceScore = Helpers.getInt(rootDict, "intPoints"); + monster.wisdomScore = Helpers.getInt(rootDict, "wisPoints"); + monster.charismaScore = Helpers.getInt(rootDict, "chaPoints"); + Helpers.addSense(monster, rootDict, "blindsight"); + // Helpers.getBool(rootDict, "blind"); + Helpers.addSense(monster, rootDict, "darkvision"); + Helpers.addSense(monster, rootDict, "tremorsense"); + Helpers.addSense(monster, rootDict, "truesight"); + monster.telepathyRange = Helpers.getInt(rootDict, "telepathy"); + monster.challengeRating = ChallengeRatingConverter.challengeRatingFromStringValue(Helpers.getString(rootDict, "cr")); + monster.customChallengeRatingDescription = Helpers.getString(rootDict, "customCr"); + monster.customProficiencyBonus = Helpers.getInt(rootDict, "customProf"); + // Helpers.getBool(rootDict, "isLegendary"); + // Helpers.getString(rootDict, "legendariesDescription"); + // Helpers.getBool(rootDict, "isLair"); + // Helpers.getString(rootDict, "lairDescription"); + // Helpers.getString(rootDict, "lairDescriptionEnd"); + // Helpers.getBool(rootDict, "isRegional"); + // Helpers.getString(rootDict, "regionalDescription"); + // Helpers.getString(rootDict, "regionalDescriptionEnd"); + // properties: [] + monster.abilities = Helpers.getListOfTraits(rootDict, "abilities"); + monster.actions = Helpers.getListOfTraits(rootDict, "actions"); + monster.reactions = Helpers.getListOfTraits(rootDict, "reactions"); + monster.legendaryActions = Helpers.getListOfTraits(rootDict, "legendaries"); + monster.lairActions = Helpers.getListOfTraits(rootDict, "lairs"); + monster.regionalActions = Helpers.getListOfTraits(rootDict, "regionals"); + Helpers.addSavingThrows(monster, rootDict); + // skills: [] + monster.skills = Helpers.getSetOfSkills(rootDict); + // damagetypes: [] + // specialdamage: [] + monster.damageImmunities = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "i"); + monster.damageImmunities.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "i")); + monster.damageResistances = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "r"); + monster.damageResistances.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "r")); + monster.damageVulnerabilities = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "v"); + monster.damageVulnerabilities.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "v")); + // conditions: [] + monster.conditionImmunities = Helpers.getSetOfDamageTypes(rootDict, "conditions"); + // languages: [] + monster.languages = Helpers.getSetOfLanguages(rootDict, "languages"); + // understandsBut: "" + monster.understandsButDescription = Helpers.getString(rootDict, "understandsBut"); + // shortName: "" + // doubleColumns: true + // separationPoint: -1 + // damage: [] + // pluralName: "" + + return monster; + } + + + public static class Helpers { + public static String getString(JsonObject dict, String name) { + return getString(dict, name, ""); + } + + public static String getString(@NonNull JsonObject dict, String name, String defaultValue) { + if (dict.has(name)) { + return dict.get(name).getAsString(); + } + + return defaultValue; + } + + public static int getInt(JsonObject dict, String name) { + return getInt(dict, name, 0); + } + + public static int getInt(@NonNull JsonObject dict, String name, int defaultValue) { + if (dict.has(name)) { + JsonElement element = dict.get(name); + if (element.isJsonPrimitive()) { + JsonPrimitive rawValue = element.getAsJsonPrimitive(); + if (rawValue.isNumber()) { + return rawValue.getAsInt(); + } else { + try { + return rawValue.getAsInt(); + } catch (Exception ex) { + return defaultValue; + } + } + } + } + return defaultValue; + } + + public static boolean getBool(JsonObject dict, String name) { + return getBool(dict, name, false); + } + + public static boolean getBool(@NonNull JsonObject dict, String name, boolean defaultValue) { + if (dict.has(name)) { + JsonElement element = dict.get(name); + if (element.isJsonPrimitive()) { + JsonPrimitive rawValue = element.getAsJsonPrimitive(); + if (rawValue.isBoolean()) { + return rawValue.getAsBoolean(); + } else { + try { + return rawValue.getAsBoolean(); + } catch (Exception ex) { + return defaultValue; + } + } + } + } + return defaultValue; + } + + @NonNull + public static String formatDistance(String name, int distance) { + // TODO: consider moving this to a string resource so it can be localized + return String.format(Locale.getDefault(), "%s %d ft.", name, distance); + } + + public static void addSense(Monster monster, JsonObject root, String name) { + int distance = Helpers.getInt(root, name); + if (distance > 0) { + monster.senses.add(Helpers.formatDistance(name, distance)); + } + } + + @NonNull + public static List getListOfTraits(@NonNull JsonObject dict, String name) { + ArrayList traits = new ArrayList<>(); + if (dict.has(name)) { + JsonElement arrayElement = dict.get(name); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String traitName = Helpers.getString(jsonObject, "name"); + String description = Helpers.getString(jsonObject, "desc"); + Trait trait = new Trait(traitName, description); + traits.add(trait); + } + } + } + } + return traits; + } + + public static void addSavingThrows(Monster monster, @NonNull JsonObject root) { + if (root.has("sthrows")) { + JsonElement arrayElement = root.get("sthrows"); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String name = Helpers.getString(jsonObject, "name"); + if ("str".equals(name)) { + monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("dex".equals(name)) { + monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("con".equals(name)) { + monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("int".equals(name)) { + monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("wis".equals(name)) { + monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT; + } else if ("cha".equals(name)) { + monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT; + } + } + } + } + } + } + + @NonNull + public static Set getSetOfSkills(@NonNull JsonObject root) { + HashSet skills = new HashSet<>(); + if (root.has("skills")) { + JsonElement arrayElement = root.get("skills"); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String name = Helpers.getString(jsonObject, "name"); + String stat = Helpers.getString(jsonObject, "stat"); + String note = Helpers.getString(jsonObject, "note"); + + Skill skill = new Skill(name, AbilityScore.valueOfString(stat), AdvantageType.NONE, " (ex)".equals(note) ? ProficiencyType.EXPERTISE : ProficiencyType.PROFICIENT); + skills.add(skill); + } + } + } + } + return skills; + } + + @NonNull + public static Set getSetOfDamageTypes(JsonObject rootDict, String name) { + return getSetOfDamageTypes(rootDict, name, null); + } + + @NonNull + public static Set getSetOfDamageTypes(@NonNull JsonObject root, String name, String type) { + HashSet damageTypes = new HashSet<>(); + if (root.has(name)) { + JsonElement arrayElement = root.get(name); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String dtName = Helpers.getString(jsonObject, "name"); + String dtType = Helpers.getString(jsonObject, "type"); + if (type == null || type.equals(dtType)) { + damageTypes.add(dtName); + } + } + } + } + } + return damageTypes; + } + + @NonNull + public static Set getSetOfLanguages(@NonNull JsonObject root, String name) { + HashSet languages = new HashSet<>(); + if (root.has(name)) { + JsonElement arrayElement = root.get(name); + if (arrayElement.isJsonArray()) { + JsonArray array = arrayElement.getAsJsonArray(); + int size = array.size(); + for (int index = 0; index < size; index++) { + JsonElement jsonElement = array.get(index); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String languageName = Helpers.getString(jsonObject, "name"); + boolean canSpeak = Helpers.getBool(jsonObject, "speaks"); + Language language = new Language(languageName, canSpeak); + languages.add(language); + } + } + } + } + return languages; + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/helpers/StringHelper.java b/app/src/main/java/com/majinnaibu/monstercards/helpers/StringHelper.java new file mode 100644 index 0000000..bdad3b2 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/helpers/StringHelper.java @@ -0,0 +1,79 @@ +package com.majinnaibu.monstercards.helpers; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collection; + +@SuppressWarnings({"RedundantIfStatement"}) +public final class StringHelper { + public static boolean isNullOrEmpty(CharSequence value) { + if (value == null) { + return true; + } + + if ("".contentEquals(value)) { + return true; + } + + return false; + } + + @NonNull + public static String join(String delimiter, @NonNull Collection strings) { + int length = strings.size(); + if (length < 1) { + return ""; + } else { + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (String element : strings) { + if (!isFirst) { + sb.append(delimiter); + } + + sb.append(element); + + isFirst = false; + } + return sb.toString(); + } + } + + public static String oxfordJoin(String delimiter, String lastDelimiter, String onlyDelimiter, @NonNull Collection strings) { + int length = strings.size(); + if (length < 1) { + return ""; + } else if (length == 2) { + return join(onlyDelimiter, strings); + } else { + StringBuilder sb = new StringBuilder(); + int index = 0; + int lastIndex = length - 1; + for (String element : strings) { + if (index > 0 && index < lastIndex) { + sb.append(delimiter); + } else if (index > 0 && index >= lastIndex) { + sb.append(lastDelimiter); + } + sb.append(element); + index++; + } + return sb.toString(); + } + } + + @Nullable + public static Integer parseInt(String s) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException _ex) { + return null; + } + } + + public static boolean containsCaseInsensitive(@NonNull String text, @NonNull String search) { + // TODO: find a locale independent way to do this + return text.toLowerCase().contains(search.toLowerCase()); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/Language.java b/app/src/main/java/com/majinnaibu/monstercards/models/Language.java new file mode 100644 index 0000000..a670740 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/models/Language.java @@ -0,0 +1,74 @@ +package com.majinnaibu.monstercards.models; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Comparator; +import java.util.Objects; + +public class Language implements Comparator, Comparable { + + private String mName; + private boolean mSpeaks; + + public Language(String name, boolean speaks) { + mName = name; + mSpeaks = speaks; + } + + public String getName() { + return mName; + } + + public void setName(String value) { + mName = value; + } + + public boolean getSpeaks() { + return mSpeaks; + } + + public void setSpeaks(boolean value) { + mSpeaks = value; + } + + @Override + public int compareTo(Language o) { + if (this.mSpeaks && !o.mSpeaks) { + return -1; + } + if (!this.mSpeaks && o.mSpeaks) { + return 1; + } + return this.mName.compareToIgnoreCase(o.mName); + } + + @Override + public int compare(@NonNull Language o1, Language o2) { + if (o1.mSpeaks && !o2.mSpeaks) { + return -1; + } + if (!o1.mSpeaks && o2.mSpeaks) { + return 1; + } + return o1.mName.compareToIgnoreCase(o2.mName); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof Language)) { + return false; + } + Language otherLanguage = (Language) obj; + if (!Objects.equals(this.mName, otherLanguage.mName)) { + return false; + } + if (this.mSpeaks != otherLanguage.mSpeaks) { + return false; + } + return true; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java b/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java new file mode 100644 index 0000000..aa04826 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/models/Monster.java @@ -0,0 +1,1070 @@ +package com.majinnaibu.monstercards.models; + +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.data.enums.AdvantageType; +import com.majinnaibu.monstercards.data.enums.ArmorType; +import com.majinnaibu.monstercards.data.enums.ChallengeRating; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; +import com.majinnaibu.monstercards.helpers.StringHelper; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Entity(tableName = "monsters") +@SuppressLint("DefaultLocale") +@SuppressWarnings("unused") +public class Monster { + + @PrimaryKey + @NonNull + public UUID id; + + @NonNull + @ColumnInfo(defaultValue = "") + public String name; + + @NonNull() + @ColumnInfo(defaultValue = "") + public String size; + + @NonNull() + @ColumnInfo(defaultValue = "") + public String type; + + @NonNull() + @ColumnInfo(defaultValue = "") + public String subtype; + + @NonNull() + @ColumnInfo(defaultValue = "") + public String alignment; + + @ColumnInfo(name = "strength_score", defaultValue = "10") + public int strengthScore; + + @ColumnInfo(name = "strength_saving_throw_advantage", defaultValue = "none") + public AdvantageType strengthSavingThrowAdvantage; + + @ColumnInfo(name = "strength_saving_throw_proficiency", defaultValue = "none") + public ProficiencyType strengthSavingThrowProficiency; + + @ColumnInfo(name = "dexterity_score", defaultValue = "10") + public int dexterityScore; + + @ColumnInfo(name = "dexterity_saving_throw_advantage", defaultValue = "none") + public AdvantageType dexteritySavingThrowAdvantage; + + @ColumnInfo(name = "dexterity_saving_throw_proficiency", defaultValue = "none") + public ProficiencyType dexteritySavingThrowProficiency; + + @ColumnInfo(name = "constitution_score", defaultValue = "10") + public int constitutionScore; + + @ColumnInfo(name = "constitution_saving_throw_advantage", defaultValue = "none") + public AdvantageType constitutionSavingThrowAdvantage; + + @ColumnInfo(name = "constitution_saving_throw_proficiency", defaultValue = "none") + public ProficiencyType constitutionSavingThrowProficiency; + + @ColumnInfo(name = "intelligence_score", defaultValue = "10") + public int intelligenceScore; + + @ColumnInfo(name = "intelligence_saving_throw_advantage", defaultValue = "none") + public AdvantageType intelligenceSavingThrowAdvantage; + + @ColumnInfo(name = "intelligence_saving_throw_proficiency", defaultValue = "none") + public ProficiencyType intelligenceSavingThrowProficiency; + + @ColumnInfo(name = "wisdom_score", defaultValue = "10") + public int wisdomScore; + + @ColumnInfo(name = "wisdom_saving_throw_advantage", defaultValue = "none") + public AdvantageType wisdomSavingThrowAdvantage; + + @ColumnInfo(name = "wisdom_saving_throw_proficiency", defaultValue = "none") + public ProficiencyType wisdomSavingThrowProficiency; + + @ColumnInfo(name = "charisma_score", defaultValue = "10") + public int charismaScore; + + @ColumnInfo(name = "charisma_saving_throw_advantage", defaultValue = "none") + public AdvantageType charismaSavingThrowAdvantage; + + @ColumnInfo(name = "charisma_saving_throw_proficiency", defaultValue = "none") + public ProficiencyType charismaSavingThrowProficiency; + + @ColumnInfo(name = "armor_type", defaultValue = "none"/* ArmorType.NONE.stringValue */) + public ArmorType armorType; + + @ColumnInfo(name = "shield_bonus", defaultValue = "0") + public int shieldBonus; + + @ColumnInfo(name = "natural_armor_bonus", defaultValue = "0") + public int naturalArmorBonus; + + @ColumnInfo(name = "other_armor_description", defaultValue = "") + public String otherArmorDescription; + + @ColumnInfo(name = "hit_dice", defaultValue = "1") + public int hitDice; + + @ColumnInfo(name = "has_custom_hit_points", defaultValue = "") + public boolean hasCustomHP; + + @ColumnInfo(name = "custom_hit_points_description", defaultValue = "") + public String customHPDescription; + + @ColumnInfo(name = "walk_speed", defaultValue = "0") + public int walkSpeed; + + @ColumnInfo(name = "burrow_speed", defaultValue = "0") + public int burrowSpeed; + + @ColumnInfo(name = "climb_speed", defaultValue = "0") + public int climbSpeed; + + @ColumnInfo(name = "fly_speed", defaultValue = "0") + public int flySpeed; + + @ColumnInfo(name = "can_hover", defaultValue = "false") + public boolean canHover; + + @ColumnInfo(name = "swim_speed", defaultValue = "0") + public int swimSpeed; + + @ColumnInfo(name = "has_custom_speed", defaultValue = "false") + public boolean hasCustomSpeed; + + @ColumnInfo(name = "custom_speed_description") + public String customSpeedDescription; + + @ColumnInfo(name = "challenge_rating", defaultValue = "1") + public ChallengeRating challengeRating; + + @ColumnInfo(name = "custom_challenge_rating_description", defaultValue = "") + public String customChallengeRatingDescription; + + @ColumnInfo(name = "custom_proficiency_bonus", defaultValue = "0") + public int customProficiencyBonus; + + @ColumnInfo(name = "telepathy_range", defaultValue = "0") + public int telepathyRange; + + @ColumnInfo(name = "understands_but_description", defaultValue = "") + public String understandsButDescription; + + @ColumnInfo(name = "senses", defaultValue = "[]") + public Set senses; + + @ColumnInfo(name = "skills", defaultValue = "[]") + public Set skills; + + @ColumnInfo(name = "damage_immunities", defaultValue = "[]") + public Set damageImmunities; + + @ColumnInfo(name = "damage_resistances", defaultValue = "[]") + public Set damageResistances; + + @ColumnInfo(name = "damage_vulnerabilities", defaultValue = "[]") + public Set damageVulnerabilities; + + @ColumnInfo(name = "condition_immunities", defaultValue = "[]") + public Set conditionImmunities; + + @ColumnInfo(name = "languages", defaultValue = "[]") + public Set languages; + + @ColumnInfo(name = "abilities", defaultValue = "[]") + public List abilities; + + @ColumnInfo(name = "actions", defaultValue = "[]") + public List actions; + + @ColumnInfo(name = "reactions", defaultValue = "[]") + public List reactions; + + @ColumnInfo(name = "lair_actions", defaultValue = "[]") + public List lairActions; + + @ColumnInfo(name = "legendary_actions", defaultValue = "[]") + public List legendaryActions; + + @ColumnInfo(name = "regional_actions", defaultValue = "[]") + public List regionalActions; + + public Monster() { + id = UUID.randomUUID(); + name = ""; + size = ""; + type = ""; + subtype = ""; + alignment = ""; + strengthScore = 10; + dexterityScore = 10; + constitutionScore = 10; + intelligenceScore = 10; + wisdomScore = 10; + charismaScore = 10; + armorType = ArmorType.NONE; + shieldBonus = 0; + naturalArmorBonus = 0; + otherArmorDescription = ""; + hitDice = 1; + hasCustomHP = false; + customHPDescription = ""; + walkSpeed = 0; + burrowSpeed = 0; + climbSpeed = 0; + flySpeed = 0; + canHover = false; + swimSpeed = 0; + hasCustomSpeed = false; + customSpeedDescription = ""; + challengeRating = ChallengeRating.ONE; + customChallengeRatingDescription = ""; + customProficiencyBonus = 0; + telepathyRange = 0; + understandsButDescription = ""; + strengthSavingThrowAdvantage = AdvantageType.NONE; + strengthSavingThrowProficiency = ProficiencyType.NONE; + dexteritySavingThrowAdvantage = AdvantageType.NONE; + dexteritySavingThrowProficiency = ProficiencyType.NONE; + constitutionSavingThrowAdvantage = AdvantageType.NONE; + constitutionSavingThrowProficiency = ProficiencyType.NONE; + intelligenceSavingThrowAdvantage = AdvantageType.NONE; + intelligenceSavingThrowProficiency = ProficiencyType.NONE; + wisdomSavingThrowAdvantage = AdvantageType.NONE; + wisdomSavingThrowProficiency = ProficiencyType.NONE; + charismaSavingThrowAdvantage = AdvantageType.NONE; + charismaSavingThrowProficiency = ProficiencyType.NONE; + + + skills = new HashSet<>(); + senses = new HashSet<>(); + damageImmunities = new HashSet<>(); + damageResistances = new HashSet<>(); + damageVulnerabilities = new HashSet<>(); + conditionImmunities = new HashSet<>(); + languages = new HashSet<>(); + abilities = new ArrayList<>(); + actions = new ArrayList<>(); + reactions = new ArrayList<>(); + lairActions = new ArrayList<>(); + legendaryActions = new ArrayList<>(); + regionalActions = new ArrayList<>(); + } + + public String getMeta() { + StringBuilder sb = new StringBuilder(); + boolean isFirstOutput = true; + if (!StringHelper.isNullOrEmpty(size)) { + sb.append(size); + isFirstOutput = false; + } + + if (!StringHelper.isNullOrEmpty(type)) { + if (!isFirstOutput) { + sb.append(" "); + } + sb.append(type); + isFirstOutput = false; + } + + if (!StringHelper.isNullOrEmpty(subtype)) { + if (!isFirstOutput) { + sb.append(" "); + } + sb.append("("); + sb.append(subtype); + sb.append(")"); + isFirstOutput = false; + } + + if (!StringHelper.isNullOrEmpty(alignment)) { + if (!isFirstOutput) { + sb.append(", "); + } + sb.append(alignment); + } + + return sb.toString(); + } + + public int getAbilityScore(@NonNull AbilityScore abilityScore) { + switch (abilityScore) { + case STRENGTH: + return strengthScore; + case DEXTERITY: + return dexterityScore; + case CONSTITUTION: + return constitutionScore; + case INTELLIGENCE: + return intelligenceScore; + case WISDOM: + return wisdomScore; + case CHARISMA: + return charismaScore; + default: + return 10; + } + } + + public int getAbilityModifier(@NonNull AbilityScore abilityScore) { + switch (abilityScore) { + case STRENGTH: + return getStrengthModifier(); + case DEXTERITY: + return getDexterityModifier(); + case CONSTITUTION: + return getConstitutionModifier(); + case INTELLIGENCE: + return getIntelligenceModifier(); + case WISDOM: + return getWisdomModifier(); + case CHARISMA: + return getCharismaModifier(); + default: + return 0; + } + } + + public AdvantageType getSavingThrowAdvantageType(@NonNull AbilityScore abilityScore) { + switch (abilityScore) { + case STRENGTH: + return strengthSavingThrowAdvantage; + case DEXTERITY: + return dexteritySavingThrowAdvantage; + case CONSTITUTION: + return constitutionSavingThrowAdvantage; + case INTELLIGENCE: + return intelligenceSavingThrowAdvantage; + case WISDOM: + return wisdomSavingThrowAdvantage; + case CHARISMA: + return charismaSavingThrowAdvantage; + default: + return AdvantageType.NONE; + } + } + + public ProficiencyType getSavingThrowProficiencyType(@NonNull AbilityScore abilityScore) { + switch (abilityScore) { + case STRENGTH: + return strengthSavingThrowProficiency; + case DEXTERITY: + return dexteritySavingThrowProficiency; + case CONSTITUTION: + return constitutionSavingThrowProficiency; + case INTELLIGENCE: + return intelligenceSavingThrowProficiency; + case WISDOM: + return wisdomSavingThrowProficiency; + case CHARISMA: + return charismaSavingThrowProficiency; + default: + return ProficiencyType.NONE; + } + } + + public int getStrengthModifier() { + return Helpers.getAbilityModifierForScore(strengthScore); + } + + public int getDexterityModifier() { + return Helpers.getAbilityModifierForScore(dexterityScore); + } + + public int getConstitutionModifier() { + return Helpers.getAbilityModifierForScore(constitutionScore); + } + + public int getIntelligenceModifier() { + return Helpers.getAbilityModifierForScore(intelligenceScore); + } + + public int getWisdomModifier() { + return Helpers.getAbilityModifierForScore(wisdomScore); + } + + public int getCharismaModifier() { + return Helpers.getAbilityModifierForScore(charismaScore); + } + + public String getArmorClass() { + boolean hasShield = shieldBonus != 0; + ArmorType armorType = this.armorType != null ? this.armorType : ArmorType.NONE; + switch (armorType) { + case NONE: + // 10 + dexMod + 2 for shieldBonus "15" or "17 (shield)" + return String.format("%d%s", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? " (shield)" : ""); + case NATURAL_ARMOR: + // 10 + dexMod + naturalArmorBonus + 2 for shieldBonus "16 (natural armor)" or "18 (natural armor, shield)" + return String.format("%d (natural armor%s)", armorType.baseArmorClass + getDexterityModifier() + naturalArmorBonus + shieldBonus, hasShield ? ", shield" : ""); + case MAGE_ARMOR: + // 10 + dexMod + 2 for shield + 3 for mage armor "15 (18 with mage armor)" or 17 (shield, 20 with mage armor) + return String.format("%d (%s%d with mage armor)", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? "shield, " : "", armorType.baseArmorClass + 3 + getDexterityModifier() + shieldBonus); + case PADDED: + // 11 + dexMod + 2 for shield "18 (padded armor, shield)" + return String.format("%d (padded%s)", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? ", shield" : ""); + case LEATHER: + // 11 + dexMod + 2 for shield "18 (leather, shield)" + return String.format("%d (leather%s)", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? ", shield" : ""); + case STUDDED_LEATHER: + // 12 + dexMod +2 for shield "17 (studded leather)" + return String.format("%d (studded leather%s)", armorType.baseArmorClass + getDexterityModifier() + shieldBonus, hasShield ? ", shield" : ""); + case HIDE: + // 12 + Min(2, dexMod) + 2 for shield "12 (hide armor)" + return String.format("%d (hide%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : ""); + case CHAIN_SHIRT: + // 13 + Min(2, dexMod) + 2 for shield "12 (chain shirt)" + return String.format("%d (chain shirt%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : ""); + case SCALE_MAIL: + // 14 + Min(2, dexMod) + 2 for shield "14 (scale mail)" + return String.format("%d (scale mail%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : ""); + case BREASTPLATE: + // 14 + Min(2, dexMod) + 2 for shield "16 (breastplate)" + return String.format("%d (breastplate%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : ""); + case HALF_PLATE: + // 15 + Min(2, dexMod) + 2 for shield "17 (half plate)" + return String.format("%d (half plate%s)", armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus, hasShield ? ", shield" : ""); + case RING_MAIL: + // 14 + 2 for shield "14 (ring mail) + return String.format("%d (ring mail%s)", armorType.baseArmorClass + shieldBonus, hasShield ? ", shield" : ""); + case CHAIN_MAIL: + // 16 + 2 for shield "16 (chain mail)" + return String.format("%d (chain mail%s)", armorType.baseArmorClass + shieldBonus, hasShield ? ", shield" : ""); + case SPLINT_MAIL: + // 17 + 2 for shield "17 (splint)" + return String.format("%d (splint%s)", armorType.baseArmorClass + shieldBonus, hasShield ? ", shield" : ""); + case PLATE_MAIL: + // 18 + 2 for shield "18 (plate)" + return String.format("%d (plate%s)", armorType.baseArmorClass + shieldBonus, hasShield ? ", shield" : ""); + case OTHER: + // pure string value shield check does nothing just copies the string from otherArmorDesc + return otherArmorDescription; + default: + return ""; + } + } + + public int getArmorClassValue() { + boolean hasShield = shieldBonus != 0; + ArmorType armorType = this.armorType != null ? this.armorType : ArmorType.NONE; + switch (armorType) { + case NATURAL_ARMOR: + // 10 + dexMod + naturalArmorBonus + 2 for shieldBonus "16 (natural armor)" or "18 (natural armor, shield)" + return armorType.baseArmorClass + getDexterityModifier() + naturalArmorBonus + shieldBonus; + case MAGE_ARMOR: + // 10 + dexMod + 2 for shield + 3 for mage armor "15 (18 with mage armor)" or 17 (shield, 20 with mage armor) + return armorType.baseArmorClass + 3 + getDexterityModifier() + shieldBonus; + case NONE: + // 10 + dexMod + 2 for shieldBonus "15" or "17 (shield)" + case PADDED: + // 11 + dexMod + 2 for shield "18 (padded armor, shield)" + case LEATHER: + // 11 + dexMod + 2 for shield "18 (leather, shield)" + case STUDDED_LEATHER: + // 12 + dexMod +2 for shield "17 (studded leather)" + return armorType.baseArmorClass + getDexterityModifier() + shieldBonus; + case HIDE: + // 12 + Min(2, dexMod) + 2 for shield "12 (hide armor)" + case CHAIN_SHIRT: + // 13 + Min(2, dexMod) + 2 for shield "12 (chain shirt)" + case SCALE_MAIL: + // 14 + Min(2, dexMod) + 2 for shield "14 (scale mail)" + case BREASTPLATE: + // 14 + Min(2, dexMod) + 2 for shield "16 (breastplate)" + case HALF_PLATE: + // 15 + Min(2, dexMod) + 2 for shield "17 (half plate)" + return armorType.baseArmorClass + Math.min(2, getDexterityModifier()) + shieldBonus; + case RING_MAIL: + // 14 + 2 for shield "14 (ring mail) + case CHAIN_MAIL: + // 16 + 2 for shield "16 (chain mail)" + case SPLINT_MAIL: + // 17 + 2 for shield "17 (splint)" + case PLATE_MAIL: + // 18 + 2 for shield "18 (plate)" + return armorType.baseArmorClass + shieldBonus; + case OTHER: + // pure string value shield check does nothing just copies the string from otherArmorDesc + return 0; + default: + Logger.logUnimplementedFeature(String.format("Getting the armor class value with an unknown armor type %s", armorType)); + return -1; + } + } + + public String getHitPoints() { + if (hasCustomHP) { + return customHPDescription; + } else { + int dieSize = Helpers.getHitDieForSize(size); + int conMod = getConstitutionModifier(); + // For PC style calculations use this + //int hpTotal = (int) Math.max(1, Math.ceil(dieSize + conMod + (hitDice - 1) * ((dieSize + 1) / 2.0 + conMod))); + // For monster style calculations use this + int hpTotal = (int) Math.max(1, Math.ceil(hitDice * ((dieSize + 1) / 2.0 + conMod))); + return String.format("%d (%dd%d %+d)", hpTotal, hitDice, dieSize, conMod * hitDice); + } + } + + public int getHitPointsValue() { + if (hasCustomHP) { + return 0; + } else { + int dieSize = Helpers.getHitDieForSize(size); + int conMod = getConstitutionModifier(); + // For PC style calculations use this + //return (int) Math.max(1, Math.ceil(dieSize + conMod + (hitDice - 1) * ((dieSize + 1) / 2.0 + conMod))); + // For monster style calculations use this + return (int) Math.max(1, Math.ceil(hitDice * ((dieSize + 1) / 2.0 + conMod))); + } + } + + public String getSpeedText() { + if (hasCustomSpeed) { + return customSpeedDescription; + } else { + ArrayList speedParts = new ArrayList<>(); + if (walkSpeed > 0) { + speedParts.add(String.format("%d ft.", walkSpeed)); + } + if (burrowSpeed > 0) { + speedParts.add(String.format("burrow %d ft.", burrowSpeed)); + } + if (climbSpeed > 0) { + speedParts.add(String.format("climb %d ft.", climbSpeed)); + } + if (flySpeed > 0) { + speedParts.add(String.format("fly %d ft.%s", flySpeed, canHover ? " (hover)" : "")); + } + if (swimSpeed > 0) { + speedParts.add(String.format("swim %d ft.", swimSpeed)); + } + return StringHelper.join(", ", speedParts); + } + } + + public String getStrengthDescription() { + return String.format("%d (%+d)", strengthScore, getStrengthModifier()); + } + + public String getDexterityDescription() { + return String.format("%d (%+d)", dexterityScore, getDexterityModifier()); + } + + public String getConstitutionDescription() { + return String.format("%d (%+d)", constitutionScore, getConstitutionModifier()); + } + + public String getIntelligenceDescription() { + return String.format("%d (%+d)", intelligenceScore, getIntelligenceModifier()); + } + + public String getWisdomDescription() { + return String.format("%d (%+d)", wisdomScore, getWisdomModifier()); + } + + public String getCharismaDescription() { + return String.format("%d (%+d)", charismaScore, getCharismaModifier()); + } + + public String getSavingThrowsDescription() { + List parts = new ArrayList<>(); + for (AbilityScore abilityScore : AbilityScore.values()) { + AdvantageType advantage = getSavingThrowAdvantageType(abilityScore); + ProficiencyType proficiency = getSavingThrowProficiencyType(abilityScore); + if (advantage != AdvantageType.NONE || proficiency != ProficiencyType.NONE) { + int bonus = getAbilityModifier(abilityScore) + getProficiencyBonus(proficiency); + String part = String.format("%s %+d%s", abilityScore.displayName, bonus, advantage != AdvantageType.NONE ? " " + advantage.label : ""); + parts.add(part); + } + } + return StringHelper.join(", ", parts); + } + + public int getProficiencyBonus() { + ChallengeRating challengeRating = this.challengeRating != null ? this.challengeRating : ChallengeRating.ONE; + if (challengeRating == ChallengeRating.CUSTOM) { + return customProficiencyBonus; + } else { + return challengeRating.proficiencyBonus; + } + } + + public int getProficiencyBonus(@NonNull ProficiencyType proficiencyType) { + switch (proficiencyType) { + case PROFICIENT: + return getProficiencyBonus(); + case EXPERTISE: + return getProficiencyBonus() * 2; + case NONE: + default: + return 0; + } + + } + + public String getSkillsDescription() { + Skill[] elements = new Skill[skills.size()]; + elements = skills.toArray(elements); + Arrays.sort(elements); + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (Skill skill : elements) { + if (!isFirst) { + sb.append(", "); + } + sb.append(skill.getText(this)); + isFirst = false; + } + return sb.toString(); + } + + public String getDamageVulnerabilitiesDescription() { + ArrayList vulnerabilities = new ArrayList<>(); + for (String damageType : damageVulnerabilities) { + if (!StringHelper.isNullOrEmpty(damageType)) { + vulnerabilities.add(damageType); + } + } + Collections.sort(vulnerabilities); + return StringHelper.oxfordJoin(", ", ", and ", " and ", vulnerabilities); + } + + public String getDamageResistancesDescription() { + ArrayList resistances = new ArrayList<>(); + for (String damageType : damageResistances) { + if (!StringHelper.isNullOrEmpty(damageType)) { + resistances.add(damageType); + } + } + Collections.sort(resistances); + return StringHelper.oxfordJoin(", ", ", and ", " and ", resistances); + } + + public String getDamageImmunitiesDescription() { + ArrayList immunities = new ArrayList<>(); + for (String damageType : damageImmunities) { + if (!StringHelper.isNullOrEmpty(damageType)) { + immunities.add(damageType); + } + } + Collections.sort(immunities); + return StringHelper.oxfordJoin(", ", ", and ", " and ", immunities); + } + + public String getConditionImmunitiesDescription() { + ArrayList immunities = new ArrayList<>(conditionImmunities); + Collections.sort(immunities); + return StringHelper.oxfordJoin(", ", ", and ", " and ", immunities); + } + + public String getSensesDescription() { + ArrayList parts = new ArrayList<>(senses); + parts.add(String.format("passive Perception %d", 10 + getWisdomModifier())); + return StringHelper.join(", ", parts); + } + + public String getLanguagesDescription() { + ArrayList spokenLanguages = new ArrayList<>(); + ArrayList understoodLanguages = new ArrayList<>(); + for (Language language : languages) { + if (language != null) { + if (language.getSpeaks()) { + spokenLanguages.add(language.getName()); + } else { + understoodLanguages.add(language.getName()); + } + } + } + Collections.sort(spokenLanguages); + Collections.sort(understoodLanguages); + + String spokenLanguagesString = StringHelper.oxfordJoin(", ", ", and ", " and ", spokenLanguages); + String understoodLanguagesString = StringHelper.oxfordJoin(", ", ", and ", " and ", understoodLanguages); + + boolean hasUnderstandsBut = understandsButDescription.length() > 0; + boolean hasTelepathy = telepathyRange > 0; + String telepathyString = String.format(", telepathy %d ft.", telepathyRange); + + if (spokenLanguages.size() > 0) { + if (understoodLanguages.size() > 0) { + return String.format( + "%s, understands %s%s%s", + spokenLanguagesString, + understoodLanguagesString, + hasUnderstandsBut ? " but " + understandsButDescription : "", + hasTelepathy ? telepathyString : ""); + } else { + return String.format( + "%s%s%s", + spokenLanguagesString, + hasUnderstandsBut ? " but " + understandsButDescription : "", + hasTelepathy ? telepathyString : ""); + } + } else { + if (understoodLanguages.size() > 0) { + return String.format( + "understands %s%s%s", + understoodLanguagesString, + hasUnderstandsBut ? " but " + understandsButDescription : "", + hasTelepathy ? telepathyString : ""); + } else { + return String.format( + "%S%s", + hasUnderstandsBut ? "none but " + understandsButDescription : "", + hasTelepathy ? telepathyString : ""); + } + } + } + + public String getChallengeRatingDescription() { + ChallengeRating challengeRating = this.challengeRating != null ? this.challengeRating : ChallengeRating.ONE; + if (challengeRating == ChallengeRating.CUSTOM) { + return customChallengeRatingDescription; + } else { + return challengeRating.displayName; + } + } + + public List getAbilityDescriptions() { + ArrayList abilityDescriptions = new ArrayList<>(); + for (Trait ability : abilities) { + abilityDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", ability.name, ability.description))); + } + return abilityDescriptions; + } + + public String getPlaceholderReplacedText(@NonNull String rawText) { + return rawText + .replaceAll("\\[STR SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.STRENGTH))) + .replaceAll("\\[STR ATK]", String.format("%+d", getAttackBonus(AbilityScore.STRENGTH))) + .replaceAll("\\[DEX SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.DEXTERITY))) + .replaceAll("\\[DEX ATK]", String.format("%+d", getAttackBonus(AbilityScore.DEXTERITY))) + .replaceAll("\\[CON SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.CONSTITUTION))) + .replaceAll("\\[CON ATK]", String.format("%+d", getAttackBonus(AbilityScore.CONSTITUTION))) + .replaceAll("\\[INT SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.INTELLIGENCE))) + .replaceAll("\\[INT ATK]", String.format("%+d", getAttackBonus(AbilityScore.INTELLIGENCE))) + .replaceAll("\\[WIS SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.WISDOM))) + .replaceAll("\\[WIS ATK]", String.format("%+d", getAttackBonus(AbilityScore.WISDOM))) + .replaceAll("\\[CHA SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.CHARISMA))) + .replaceAll("\\[CHA ATK]", String.format("%+d", getAttackBonus(AbilityScore.CHARISMA))); + } + + public int getSpellSaveDC(AbilityScore abilityScore) { + return 8 + getProficiencyBonus() + getAbilityModifier(abilityScore); + } + + public int getAttackBonus(AbilityScore abilityScore) { + return getProficiencyBonus() + getAbilityModifier(abilityScore); + } + + public List getActionDescriptions() { + ArrayList actionDescriptions = new ArrayList<>(); + for (Trait action : actions) { + actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description))); + } + return actionDescriptions; + } + + public List getReactionDescriptions() { + ArrayList actionDescriptions = new ArrayList<>(); + for (Trait action : reactions) { + actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description))); + } + return actionDescriptions; + } + + public List getLegendaryActionDescriptions() { + ArrayList actionDescriptions = new ArrayList<>(); + for (Trait action : legendaryActions) { + actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description))); + } + return actionDescriptions; + } + + public List getLairActionDescriptions() { + ArrayList actionDescriptions = new ArrayList<>(); + for (Trait action : lairActions) { + actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description))); + } + return actionDescriptions; + } + + public List getRegionalActionDescriptions() { + ArrayList actionDescriptions = new ArrayList<>(); + for (Trait action : regionalActions) { + actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description))); + } + return actionDescriptions; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof Monster)) { + return false; + } + Monster other = (Monster) obj; + if (!challengeRating.equals(other.challengeRating)) { + return false; + } + if (!understandsButDescription.equals(other.understandsButDescription)) { + return false; + } + if (!languages.equals(other.languages)) { + return false; + } + if (!damageVulnerabilities.equals(other.damageVulnerabilities)) { + return false; + } + if (!damageResistances.equals(other.damageResistances)) { + return false; + } + if (!damageImmunities.equals(other.damageImmunities)) { + return false; + } + if (!conditionImmunities.equals(other.conditionImmunities)) { + return false; + } + if (!charismaSavingThrowProficiency.equals(other.charismaSavingThrowProficiency)) { + return false; + } + if (!wisdomSavingThrowProficiency.equals(other.wisdomSavingThrowProficiency)) { + return false; + } + if (!intelligenceSavingThrowProficiency.equals(other.intelligenceSavingThrowProficiency)) { + return false; + } + if (constitutionSavingThrowProficiency.equals(other.constitutionSavingThrowProficiency)) { + return false; + } + + if (dexteritySavingThrowProficiency.equals(other.dexteritySavingThrowProficiency)) { + return false; + } + + if (strengthSavingThrowProficiency.equals(other.strengthSavingThrowProficiency)) { + return false; + } + + if (legendaryActions.equals(other.legendaryActions)) { + return false; + } + + if (customChallengeRatingDescription.equals(other.customChallengeRatingDescription)) { + return false; + } + + if (customSpeedDescription.equals(other.customSpeedDescription)) { + return false; + } + + if (customHPDescription.equals(other.customHPDescription)) { + return false; + } + + if (otherArmorDescription.equals(other.otherArmorDescription)) { + return false; + } + + if (alignment.equals(other.alignment)) { + return false; + } + + if (subtype.equals(other.subtype)) { + return false; + } + + if (abilities.equals(other.abilities)) { + return false; + } + + if (actions.equals(other.actions)) { + return false; + } + + if (armorType.equals(other.armorType)) { + return false; + } + + if (charismaSavingThrowAdvantage.equals(other.charismaSavingThrowAdvantage)) { + return false; + } + + if (constitutionSavingThrowAdvantage.equals(other.constitutionSavingThrowAdvantage)) { + return false; + } + + if (dexteritySavingThrowAdvantage.equals(other.dexteritySavingThrowAdvantage)) { + return false; + } + + if (hitDice == other.hitDice) { + return false; + } + + if (id.equals(other.id)) { + return false; + } + + if (intelligenceSavingThrowAdvantage.equals(other.intelligenceSavingThrowAdvantage)) { + return false; + } + + if (lairActions.equals(other.lairActions)) { + return false; + } + + if (name.equals(other.name)) { + return false; + } + + if (reactions.equals(other.reactions)) { + return false; + } + + if (regionalActions.equals(other.regionalActions)) { + return false; + } + + if (senses.equals(other.senses)) { + return false; + } + + if (shieldBonus == other.shieldBonus) { + return false; + } + + if (size.equals(other.size)) { + return false; + } + + if (skills.equals(other.skills)) { + return false; + } + + if (strengthSavingThrowAdvantage.equals(other.strengthSavingThrowAdvantage)) { + return false; + } + + if (strengthScore == other.strengthScore) { + return false; + } + + if (type.equals(other.type)) { + return false; + } + + if (wisdomSavingThrowAdvantage.equals(other.wisdomSavingThrowAdvantage)) { + return false; + } + + if (wisdomScore == other.wisdomScore) { + return false; + } + if (customProficiencyBonus == other.customProficiencyBonus) { + return false; + } + + if (telepathyRange == other.telepathyRange) { + return false; + } + + if (intelligenceScore == other.intelligenceScore) { + return false; + } + + if (constitutionScore == other.constitutionScore) { + return false; + } + + if (dexterityScore == other.dexterityScore) { + return false; + } + + if (hasCustomSpeed == other.hasCustomSpeed) { + return false; + } + + if (hasCustomHP == other.hasCustomHP) { + return false; + } + + if (swimSpeed == other.swimSpeed) { + return false; + } + + if (flySpeed == other.flySpeed) { + return false; + } + + if (climbSpeed == other.climbSpeed) { + return false; + } + + if (burrowSpeed == other.burrowSpeed) { + return false; + } + + if (walkSpeed == other.walkSpeed) { + return false; + } + + if (naturalArmorBonus == other.naturalArmorBonus) { + return false; + } + + if (canHover == other.canHover) { + return false; + } + + if (charismaScore == other.charismaScore) { + return false; + } + + return true; + } + + public static class Helpers { + public static int getAbilityModifierForScore(int score) { + return (int) Math.floor((score - 10) / 2.0); + } + + public static int getHitDieForSize(String size) { + if ("tiny".equals(size)) { + return 4; + } else if ("small".equals(size)) { + return 6; + } else if ("medium".equals(size)) { + return 8; + } else if ("large".equals(size)) { + return 10; + } else if ("huge".equals(size)) { + return 12; + } else if ("gargantuan".equals(size)) { + return 20; + } else { + return 8; + } + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java b/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java new file mode 100644 index 0000000..7d6c110 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/models/MonsterFTS.java @@ -0,0 +1,14 @@ +package com.majinnaibu.monstercards.models; + +import androidx.room.Entity; +import androidx.room.Fts4; + +@Entity(tableName = "monsters_fts") +@Fts4(contentEntity = Monster.class) +public class MonsterFTS { + public String name; + public String size; + public String type; + public String subtype; + public String alignment; +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java b/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java new file mode 100644 index 0000000..9730cc5 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/models/Skill.java @@ -0,0 +1,95 @@ +package com.majinnaibu.monstercards.models; + +import android.annotation.SuppressLint; + +import androidx.annotation.Nullable; + +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.data.enums.AdvantageType; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; + +import java.util.Comparator; +import java.util.Objects; + +@SuppressLint("DefaultLocale") +public class Skill implements Comparator, Comparable { + + public String name; + public AbilityScore abilityScore; + public AdvantageType advantageType; + public ProficiencyType proficiencyType; + + public Skill(String name, AbilityScore abilityScore) { + this(name, abilityScore, AdvantageType.NONE, ProficiencyType.PROFICIENT); + } + + public Skill(String name, AbilityScore abilityScore, AdvantageType advantageType) { + this(name, abilityScore, advantageType, ProficiencyType.PROFICIENT); + } + + public Skill(String name, AbilityScore abilityScore, AdvantageType advantageType, ProficiencyType proficiencyType) { + this.name = name; + this.abilityScore = abilityScore; + this.advantageType = advantageType; + this.proficiencyType = proficiencyType; + } + + public int getSkillBonus(Monster monster) { + int modifier = monster.getAbilityModifier(abilityScore); + switch (proficiencyType) { + case PROFICIENT: + return modifier + monster.getProficiencyBonus(); + case EXPERTISE: + return modifier + monster.getProficiencyBonus() * 2; + case NONE: + default: + return modifier; + } + } + + public String getText(Monster monster) { + int bonus = getSkillBonus(monster); + + return String.format( + "%s%s %+d%s", + name.charAt(0), + name.substring(1), + bonus, + advantageType == AdvantageType.ADVANTAGE ? " A" : advantageType == AdvantageType.DISADVANTAGE ? " D" : "" + ); + } + + @Override + public int compareTo(Skill o) { + return this.name.compareToIgnoreCase(o.name); + } + + @Override + public int compare(Skill o1, Skill o2) { + return o1.name.compareToIgnoreCase(o2.name); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof Skill)) { + return false; + } + Skill otherSkill = (Skill) obj; + if (!Objects.equals(this.name, otherSkill.name)) { + return false; + } + if (this.abilityScore != otherSkill.abilityScore) { + return false; + } + if (this.advantageType != otherSkill.advantageType) { + return false; + } + if (this.proficiencyType != otherSkill.proficiencyType) { + return false; + } + return true; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/models/Trait.java b/app/src/main/java/com/majinnaibu/monstercards/models/Trait.java new file mode 100644 index 0000000..f6a5703 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/models/Trait.java @@ -0,0 +1,49 @@ +package com.majinnaibu.monstercards.models; + +import androidx.annotation.Nullable; + +import java.util.Comparator; +import java.util.Objects; + +public class Trait implements Comparator, Comparable { + + public String name; + public String description; + + public Trait(String name, String description) { + this.name = name; + this.description = description; + } + + @Override + public int compareTo(Trait o) { + return compare(this, o); + } + + @Override + public int compare(Trait o1, Trait o2) { + int result = o1.name.compareToIgnoreCase(o2.name); + if (result != 0) { + return result; + } + return o1.description.compareToIgnoreCase(o2.description); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof Trait)) { + return false; + } + Trait otherTrait = (Trait) obj; + if (!Objects.equals(this.name, otherTrait.name)) { + return false; + } + if (!Objects.equals(this.description, otherTrait.description)) { + return false; + } + return true; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java new file mode 100644 index 0000000..a0fd733 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java @@ -0,0 +1,27 @@ +package com.majinnaibu.monstercards.ui.collections; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.ui.shared.MCFragment; + +public class CollectionsFragment extends MCFragment { + + private CollectionsViewModel collectionsViewModel; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + collectionsViewModel = new ViewModelProvider(this).get(CollectionsViewModel.class); + View root = inflater.inflate(R.layout.fragment_collections, container, false); + final TextView textView = root.findViewById(R.id.text_collections); + collectionsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText); + return root; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsViewModel.java new file mode 100644 index 0000000..1ea5b1a --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsViewModel.java @@ -0,0 +1,19 @@ +package com.majinnaibu.monstercards.ui.collections; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +public class CollectionsViewModel extends ViewModel { + + private final MutableLiveData mText; + + public CollectionsViewModel() { + mText = new MutableLiveData<>(); + mText.setValue("This is collections fragment"); + } + + public LiveData getText() { + return mText; + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/components/AbilityScorePicker.java b/app/src/main/java/com/majinnaibu/monstercards/ui/components/AbilityScorePicker.java new file mode 100644 index 0000000..6d15709 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/components/AbilityScorePicker.java @@ -0,0 +1,133 @@ +package com.majinnaibu.monstercards.ui.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.helpers.ArrayHelper; + +import java.util.Objects; + +public class AbilityScorePicker extends LinearLayout { + private final ViewHolder mHolder; + private OnValueChangedListener mOnValueChangedListener; + private AbilityScore mSelectedValue; + private String mLabel; + + public AbilityScorePicker(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + mSelectedValue = AbilityScore.STRENGTH; + mOnValueChangedListener = null; + // TODO: use this as default but allow setting via attribute + mLabel = "Ability Score"; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AbilityScorePicker, 0, 0); + String label = a.getString(R.styleable.AbilityScorePicker_label); + if (label != null) { + mLabel = label; + } + a.recycle(); + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View root = inflater.inflate(R.layout.component_ability_score_picker, this, true); + + mHolder = new ViewHolder(root); + + mHolder.label.setText(mLabel); + + mHolder.spinner.setAdapter(new ArrayAdapter(getContext(), R.layout.dropdown_list_item, AbilityScore.values()) { + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + AbilityScore item = getItem(position); + TextView view = (TextView) super.getView(position, convertView, parent); + view.setText(item.displayName); + return view; + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + AbilityScore item = getItem(position); + TextView view = (TextView) super.getDropDownView(position, convertView, parent); + view.setText(item.displayName); + return view; + } + }); + mHolder.spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + setValue((AbilityScore) parent.getItemAtPosition(position)); + } + + @Override + public void onNothingSelected(AdapterView parent) { + setValue(mSelectedValue = AbilityScore.STRENGTH); + } + }); + mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), mSelectedValue)); + + setValue(AbilityScore.STRENGTH); + } + + public AbilityScorePicker(@NonNull Context context) { + this(context, null); + } + + public AbilityScore getValue() { + return mSelectedValue; + } + + public void setValue(AbilityScore value) { + if (value != mSelectedValue) { + mSelectedValue = value; + mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), value)); + if (mOnValueChangedListener != null) { + mOnValueChangedListener.onValueChanged(value); + } + } + } + + public void setOnValueChangedListener(OnValueChangedListener listener) { + mOnValueChangedListener = listener; + } + + public String getLabel() { + return mLabel; + } + + public void setLabel(String label) { + if (!Objects.equals(mLabel, label)) { + mLabel = label; + mHolder.label.setText(label); + } + } + + public interface OnValueChangedListener { + void onValueChanged(AbilityScore value); + } + + private static class ViewHolder { + + private final Spinner spinner; + private final TextView label; + + ViewHolder(@NonNull View root) { + spinner = root.findViewById(R.id.spinner); + label = root.findViewById(R.id.label); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/components/AdvantagePicker.java b/app/src/main/java/com/majinnaibu/monstercards/ui/components/AdvantagePicker.java new file mode 100644 index 0000000..7ac572f --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/components/AdvantagePicker.java @@ -0,0 +1,98 @@ +package com.majinnaibu.monstercards.ui.components; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RadioGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.google.android.material.radiobutton.MaterialRadioButton; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.AdvantageType; + +@SuppressWarnings("unused") +public class AdvantagePicker extends ConstraintLayout { + private final ViewHolder mHolder; + private OnValueChangedListener mOnValueChangedListener; + private AdvantageType mSelectedValue; + + public AdvantagePicker(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + mSelectedValue = AdvantageType.NONE; + mOnValueChangedListener = null; + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View root = inflater.inflate(R.layout.component_advantage_picker, this, true); + + mHolder = new ViewHolder(root); + + setValue(AdvantageType.NONE); + mHolder.group.setOnCheckedChangeListener((group, checkedId) -> { + if (R.id.hasAdvantage == checkedId) { + setValue(AdvantageType.ADVANTAGE); + } else if (R.id.hasDisadvantage == checkedId) { + setValue(AdvantageType.DISADVANTAGE); + } else { + setValue(AdvantageType.NONE); + } + }); + } + + public AdvantagePicker(@NonNull Context context) { + this(context, null); + } + + public AdvantageType getValue() { + return mSelectedValue; + } + + public void setValue(AdvantageType value) { + if (mSelectedValue != value) { + mSelectedValue = value; + if (mOnValueChangedListener != null) { + mOnValueChangedListener.onValueChanged(mSelectedValue); + } + } + final int checkedId = mHolder.group.getCheckedRadioButtonId(); + if (mSelectedValue == AdvantageType.ADVANTAGE) { + if (checkedId != R.id.hasAdvantage) { + mHolder.advantage.setChecked(true); + } + } else if (mSelectedValue == AdvantageType.DISADVANTAGE) { + if (checkedId != R.id.hasDisadvantage) { + mHolder.disadvantage.setChecked(true); + } + } else { + if (checkedId != R.id.none) { + mHolder.none.setChecked(true); + } + } + } + + public void setOnValueChangedListener(OnValueChangedListener listener) { + mOnValueChangedListener = listener; + } + + public interface OnValueChangedListener { + void onValueChanged(AdvantageType value); + } + + private static class ViewHolder { + final RadioGroup group; + final MaterialRadioButton none; + final MaterialRadioButton advantage; + final MaterialRadioButton disadvantage; + + ViewHolder(@NonNull View root) { + group = root.findViewById(R.id.group); + none = root.findViewById(R.id.hasNoAdvantage); + advantage = root.findViewById(R.id.hasAdvantage); + disadvantage = root.findViewById(R.id.hasDisadvantage); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/components/ProficiencyPicker.java b/app/src/main/java/com/majinnaibu/monstercards/ui/components/ProficiencyPicker.java new file mode 100644 index 0000000..8b33849 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/components/ProficiencyPicker.java @@ -0,0 +1,98 @@ +package com.majinnaibu.monstercards.ui.components; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RadioGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.google.android.material.radiobutton.MaterialRadioButton; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; + +@SuppressWarnings("unused") +public class ProficiencyPicker extends ConstraintLayout { + private final ViewHolder mHolder; + private OnValueChangedListener mOnValueChangedListener; + private ProficiencyType mSelectedValue; + + public ProficiencyPicker(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + mSelectedValue = ProficiencyType.NONE; + mOnValueChangedListener = null; + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View root = inflater.inflate(R.layout.component_proficiency_picker, this, true); + + mHolder = new ViewHolder(root); + + setValue(ProficiencyType.NONE); + mHolder.group.setOnCheckedChangeListener((group, checkedId) -> { + if (R.id.proficient == checkedId) { + setValue(ProficiencyType.PROFICIENT); + } else if (R.id.expertise == checkedId) { + setValue(ProficiencyType.EXPERTISE); + } else { + setValue(ProficiencyType.NONE); + } + }); + } + + public ProficiencyPicker(@NonNull Context context) { + this(context, null); + } + + public ProficiencyType getValue() { + return mSelectedValue; + } + + public void setValue(ProficiencyType value) { + if (mSelectedValue != value) { + mSelectedValue = value; + if (mOnValueChangedListener != null) { + mOnValueChangedListener.onValueChanged(mSelectedValue); + } + } + final int checkedId = mHolder.group.getCheckedRadioButtonId(); + if (mSelectedValue == ProficiencyType.PROFICIENT) { + if (checkedId != R.id.proficient) { + mHolder.proficient.setChecked(true); + } + } else if (mSelectedValue == ProficiencyType.EXPERTISE) { + if (checkedId != R.id.expertise) { + mHolder.expertise.setChecked(true); + } + } else { + if (checkedId != R.id.none) { + mHolder.none.setChecked(true); + } + } + } + + public void setOnValueChangedListener(OnValueChangedListener listener) { + mOnValueChangedListener = listener; + } + + public interface OnValueChangedListener { + void onValueChanged(ProficiencyType value); + } + + private static class ViewHolder { + final RadioGroup group; + final MaterialRadioButton none; + final MaterialRadioButton proficient; + final MaterialRadioButton expertise; + + ViewHolder(@NonNull View root) { + group = root.findViewById(R.id.group); + none = root.findViewById(R.id.none); + proficient = root.findViewById(R.id.proficient); + expertise = root.findViewById(R.id.expertise); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/components/Stepper.java b/app/src/main/java/com/majinnaibu/monstercards/ui/components/Stepper.java new file mode 100644 index 0000000..911a5b9 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/components/Stepper.java @@ -0,0 +1,149 @@ +package com.majinnaibu.monstercards.ui.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.majinnaibu.monstercards.R; + +import java.util.Objects; + + +@SuppressWarnings("unused") +public class Stepper extends ConstraintLayout { + private final ViewHolder mHolder; + private int mCurrentValue; + private int mStep; + private int mMinValue; + private int mMaxValue; + private String mLabel; + private OnValueChangeListener mOnValueChangeListener; + private OnFormatValueCallback mOnFormatValueCallback; + + public Stepper(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + mCurrentValue = 0; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Stepper, 0, 0); + mStep = a.getInt(R.styleable.Stepper_stepAmount, 1); + mMinValue = a.getInt(R.styleable.Stepper_minValue, Integer.MIN_VALUE); + mMaxValue = a.getInt(R.styleable.Stepper_maxValue, Integer.MAX_VALUE); + mLabel = a.getString(R.styleable.Stepper_label); + a.recycle(); + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View root = inflater.inflate(R.layout.component_stepper, this, true); + + mHolder = new ViewHolder(root); + + setValue(mCurrentValue); + updateDisplayedValue(); + mHolder.increment.setOnClickListener(v -> setValue(mCurrentValue + mStep)); + mHolder.decrement.setOnClickListener(v -> setValue(mCurrentValue - mStep)); + + mHolder.label.setText(mLabel); + } + + public Stepper(Context context) { + this(context, null); + } + + public String getLabel() { + return mLabel; + } + + public void setLabel(String newLabel) { + if (!Objects.equals(mLabel, newLabel)) { + mLabel = newLabel; + mHolder.label.setText(mLabel); + } + } + + public int getValue() { + return mCurrentValue; + } + + public void setValue(int value) { + int oldValue = this.mCurrentValue; + int newValue = Math.min(mMaxValue, Math.max(mMinValue, value)); + if (newValue != oldValue) { + this.mCurrentValue = newValue; + if (mOnValueChangeListener != null) { + mOnValueChangeListener.onChange(newValue, oldValue); + } + updateDisplayedValue(); + } + } + + private void updateDisplayedValue() { + if (mOnFormatValueCallback != null) { + mHolder.text.setText(mOnFormatValueCallback.onFormatValue(this.mCurrentValue)); + } else { + mHolder.text.setText(String.valueOf(this.mCurrentValue)); + } + } + + public void setOnValueChangeListener(OnValueChangeListener listener) { + mOnValueChangeListener = listener; + } + + public void setOnFormatValueCallback(OnFormatValueCallback callback) { + mOnFormatValueCallback = callback; + updateDisplayedValue(); + } + + public int getStep() { + return mStep; + } + + public void setStep(int step) { + this.mStep = step; + } + + public int getMinValue() { + return mMinValue; + } + + public void setMinValue(int minValue) { + this.mMinValue = minValue; + } + + public int getMaxValue() { + return mMaxValue; + } + + public void setMaxValue(int maxValue) { + this.mMaxValue = maxValue; + } + + public interface OnValueChangeListener { + void onChange(int value, int previousValue); + } + + public interface OnFormatValueCallback { + String onFormatValue(int value); + } + + private static class ViewHolder { + final TextView text; + final TextView label; + final Button increment; + final Button decrement; + + ViewHolder(@NonNull View root) { + text = root.findViewById(R.id.text); + label = root.findViewById(R.id.label); + increment = root.findViewById(R.id.increment); + decrement = root.findViewById(R.id.decrement); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java new file mode 100644 index 0000000..92e61ce --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java @@ -0,0 +1,84 @@ +package com.majinnaibu.monstercards.ui.dashboard; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.List; +import java.util.Locale; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class DashboardFragment extends MCFragment { + private DashboardViewModel mViewModel; + private ViewHolder mHolder; + private DashboardRecyclerViewAdapter mAdapter; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + mViewModel = new ViewModelProvider(this).get(DashboardViewModel.class); + View root = inflater.inflate(R.layout.fragment_dashboard, container, false); + mHolder = new ViewHolder(root); + + setupRecyclerView(mHolder.list); + + getMonsterRepository() + .getMonsters() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(monsters -> mViewModel.setMonsters(monsters)); + + return root; + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { + int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 396); + Logger.logWTF(String.format(Locale.US, "Setting column count to %d", columnCount)); + Context context = requireContext(); + GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount); + recyclerView.setLayoutManager(layoutManager); + + LiveData> monsterData = mViewModel.getMonsters(); + mAdapter = new DashboardRecyclerViewAdapter(monster -> { + if (monster != null) { + navigateToMonsterDetail(monster); + } else { + Logger.logError("Can't navigate to MonsterDetailFragment with a null monster"); + } + }); + if (monsterData != null) { + monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters)); + } + recyclerView.setAdapter(mAdapter); + } + + private void navigateToMonsterDetail(Monster monster) { + NavDirections action = DashboardFragmentDirections.actionNavigationDashboardToNavigationMonster(monster.id.toString()); + Navigation.findNavController(requireView()).navigate(action); + } + + private static class ViewHolder { + final RecyclerView list; + + ViewHolder(View root) { + list = root.findViewById(R.id.list); + } + } + +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java new file mode 100644 index 0000000..d6e6149 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardRecyclerViewAdapter.java @@ -0,0 +1,353 @@ +package com.majinnaibu.monstercards.ui.dashboard; + +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.data.enums.AdvantageType; +import com.majinnaibu.monstercards.data.enums.ChallengeRating; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; +import com.majinnaibu.monstercards.databinding.CardMonsterBinding; +import com.majinnaibu.monstercards.helpers.CommonMarkHelper; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.models.Trait; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.Locale; + +public class DashboardRecyclerViewAdapter extends ListAdapter { + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) { + return oldItem.id.equals(newItem.id); + } + + @Override + public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) { + return oldItem.equals(newItem); + } + }; + private final ItemCallback mOnClick; + + protected DashboardRecyclerViewAdapter(ItemCallback onClick) { + super(DIFF_CALLBACK); + mOnClick = onClick; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(CardMonsterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Logger.logUnimplementedMethod(); + Monster monster = getItem(position); + holder.monster = monster; + holder.name.setText(monster.name); + holder.meta.setText(monster.getMeta()); + holder.strengthAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.strengthSavingThrowAdvantage)); + holder.strengthModifier.setText(Helpers.getModifierString(monster.getStrengthModifier())); + holder.strengthName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.STRENGTH)); + holder.strengthProficiency.setText(Helpers.getProficiencyAbbreviation(monster.strengthSavingThrowProficiency)); + + holder.dexterityAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.dexteritySavingThrowAdvantage)); + holder.dexterityModifier.setText(Helpers.getModifierString(monster.getDexterityModifier())); + holder.dexterityName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.DEXTERITY)); + holder.dexterityProficiency.setText(Helpers.getProficiencyAbbreviation(monster.dexteritySavingThrowProficiency)); + + holder.constitutionAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.constitutionSavingThrowAdvantage)); + holder.constitutionModifier.setText(Helpers.getModifierString(monster.getConstitutionModifier())); + holder.constitutionName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.CONSTITUTION)); + holder.constitutionProficiency.setText(Helpers.getProficiencyAbbreviation(monster.constitutionSavingThrowProficiency)); + + holder.intelligenceAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.intelligenceSavingThrowAdvantage)); + holder.intelligenceModifier.setText(Helpers.getModifierString(monster.getIntelligenceModifier())); + holder.intelligenceName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.INTELLIGENCE)); + holder.intelligenceProficiency.setText(Helpers.getProficiencyAbbreviation(monster.intelligenceSavingThrowProficiency)); + + holder.wisdomAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.wisdomSavingThrowAdvantage)); + holder.wisdomModifier.setText(Helpers.getModifierString(monster.getWisdomModifier())); + holder.wisdomName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.WISDOM)); + holder.wisdomProficiency.setText(Helpers.getProficiencyAbbreviation(monster.wisdomSavingThrowProficiency)); + + holder.charismaAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.charismaSavingThrowAdvantage)); + holder.charismaModifier.setText(Helpers.getModifierString(monster.getCharismaModifier())); + holder.charismaName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.CHARISMA)); + holder.charismaProficiency.setText(Helpers.getProficiencyAbbreviation(monster.charismaSavingThrowProficiency)); + + holder.armorClass.setText(String.valueOf(monster.getArmorClassValue())); + holder.hitPoints.setText(String.valueOf(monster.getHitPointsValue())); + holder.challengeRating.setText(holder.challengeRating.getResources().getString(R.string.label_challenge_rating_with_value, Helpers.getChallengeRatingAbbreviation(monster.challengeRating))); + + int numActions = monster.actions.size(); + if (numActions > 0) { + holder.action1Group.setVisibility(View.VISIBLE); + Trait action = monster.actions.get(0); + holder.action1Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description))); + holder.action1Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name))); + } else { + holder.action1Group.setVisibility(View.GONE); + } + + if (numActions > 1) { + holder.action2Group.setVisibility(View.VISIBLE); + Trait action = monster.actions.get(1); + holder.action2Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description))); + holder.action2Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name))); + } else { + holder.action2Group.setVisibility(View.GONE); + } + + if (numActions > 2) { + holder.action3Group.setVisibility(View.VISIBLE); + Trait action = monster.actions.get(2); + holder.action3Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description))); + holder.action3Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name))); + } else { + holder.action3Group.setVisibility(View.GONE); + } + + holder.itemView.setOnClickListener(v -> { + if (mOnClick != null) { + mOnClick.onItemCallback(holder.monster); + } + }); + } + + public interface ItemCallback { + void onItemCallback(Monster monster); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public final TextView name; + public final TextView meta; + public final View action1Group; + public final TextView action1Name; + public final TextView action1Description; + public final View action2Group; + public final TextView action2Name; + public final TextView action2Description; + public final View action3Group; + public final TextView action3Name; + public final TextView action3Description; + public final TextView strengthName; + public final TextView strengthModifier; + public final TextView strengthProficiency; + public final TextView strengthAdvantage; + public final TextView dexterityName; + public final TextView dexterityModifier; + public final TextView dexterityProficiency; + public final TextView dexterityAdvantage; + public final TextView constitutionName; + public final TextView constitutionModifier; + public final TextView constitutionProficiency; + public final TextView constitutionAdvantage; + public final TextView intelligenceName; + public final TextView intelligenceModifier; + public final TextView intelligenceProficiency; + public final TextView intelligenceAdvantage; + public final TextView wisdomName; + public final TextView wisdomModifier; + public final TextView wisdomProficiency; + public final TextView wisdomAdvantage; + public final TextView charismaName; + public final TextView charismaModifier; + public final TextView charismaProficiency; + public final TextView charismaAdvantage; + public final TextView armorClass; + public final TextView hitPoints; + public final TextView challengeRating; + public Monster monster; + + public ViewHolder(@NonNull CardMonsterBinding binding) { + super(binding.getRoot()); + name = binding.name; + meta = binding.meta; + action1Group = binding.action1.getRoot(); + action1Name = binding.action1.name; + action1Description = binding.action1.description; + action2Group = binding.action2.getRoot(); + action2Name = binding.action2.name; + action2Description = binding.action2.description; + action3Group = binding.action3.getRoot(); + action3Name = binding.action3.name; + action3Description = binding.action3.description; + strengthName = binding.strength.name; + strengthModifier = binding.strength.modifier; + strengthProficiency = binding.strength.proficiency; + strengthAdvantage = binding.strength.advantage; + dexterityName = binding.dexterity.name; + dexterityModifier = binding.dexterity.modifier; + dexterityProficiency = binding.dexterity.proficiency; + dexterityAdvantage = binding.dexterity.advantage; + constitutionName = binding.constitution.name; + constitutionModifier = binding.constitution.modifier; + constitutionProficiency = binding.constitution.proficiency; + constitutionAdvantage = binding.constitution.advantage; + intelligenceName = binding.intelligence.name; + intelligenceModifier = binding.intelligence.modifier; + intelligenceProficiency = binding.intelligence.proficiency; + intelligenceAdvantage = binding.intelligence.advantage; + wisdomName = binding.wisdom.name; + wisdomModifier = binding.wisdom.modifier; + wisdomProficiency = binding.wisdom.proficiency; + wisdomAdvantage = binding.wisdom.advantage; + charismaName = binding.charisma.name; + charismaModifier = binding.charisma.modifier; + charismaProficiency = binding.charisma.proficiency; + charismaAdvantage = binding.charisma.advantage; + armorClass = binding.armorClass.value; + hitPoints = binding.hitPoints.value; + challengeRating = binding.challengeRating; + } + } + + public static class Helpers { + @NonNull + public static String getModifierString(int value) { + return String.format(Locale.getDefault(), "%+d", value); + } + + @NonNull + public static String getAbilityScoreAbbreviation(@NonNull AbilityScore abilityScore) { + switch (abilityScore) { + case STRENGTH: + return "S"; + case DEXTERITY: + return "D"; + case CONSTITUTION: + return "C"; + case INTELLIGENCE: + return "I"; + case WISDOM: + return "W"; + case CHARISMA: + return "Ch"; + default: + Logger.logUnimplementedFeature(String.format("Get an abbreviation for AbilityScore value %s", abilityScore)); + return ""; + } + } + + @NonNull + public static String getChallengeRatingAbbreviation(@NonNull ChallengeRating challengeRating) { + Logger.logUnimplementedMethod(); + switch (challengeRating) { + case CUSTOM: + return "*"; + case ZERO: + return "0"; + case ONE_EIGHTH: + return "1/8"; + case ONE_QUARTER: + return "1/4"; + case ONE_HALF: + return "1/2"; + case ONE: + return "1"; + case TWO: + return "2"; + case THREE: + return "3"; + case FOUR: + return "4"; + case FIVE: + return "5"; + case SIX: + return "6"; + case SEVEN: + return "7"; + case EIGHT: + return "8"; + case NINE: + return "9"; + case TEN: + return "10"; + case ELEVEN: + return "11"; + case TWELVE: + return "12"; + case THIRTEEN: + return "13"; + case FOURTEEN: + return "14"; + case FIFTEEN: + return "15"; + case SIXTEEN: + return "16"; + case SEVENTEEN: + return "17"; + case EIGHTEEN: + return "18"; + case NINETEEN: + return "19"; + case TWENTY: + return "20"; + case TWENTY_ONE: + return "21"; + case TWENTY_TWO: + return "22"; + case TWENTY_THREE: + return "23"; + case TWENTY_FOUR: + return "24"; + case TWENTY_FIVE: + return "25"; + case TWENTY_SIX: + return "26"; + case TWENTY_SEVEN: + return "27"; + case TWENTY_EIGHT: + return "28"; + case TWENTY_NINE: + return "29"; + case THIRTY: + return "30"; + default: + Logger.logUnimplementedFeature(String.format("Get an abbreviation for ChallengeRating value %s", challengeRating)); + return ""; + } + } + + @NonNull + public static String getProficiencyAbbreviation(@NonNull ProficiencyType proficiency) { + switch (proficiency) { + case NONE: + return ""; + case EXPERTISE: + return "E"; + case PROFICIENT: + return "P"; + default: + Logger.logUnimplementedFeature(String.format("Get an abbreviation for ProficiencyType value %s", proficiency)); + return ""; + } + } + + @NonNull + public static String getAdvantageAbbreviation(@NonNull AdvantageType advantage) { + switch (advantage) { + case NONE: + return ""; + case ADVANTAGE: + return "A"; + case DISADVANTAGE: + return "D"; + default: + Logger.logUnimplementedFeature(String.format("Get an abbreviation for AdvantageType value %s", advantage)); + return ""; + } + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java new file mode 100644 index 0000000..98b6711 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardViewModel.java @@ -0,0 +1,26 @@ +package com.majinnaibu.monstercards.ui.dashboard; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.majinnaibu.monstercards.models.Monster; + +import java.util.ArrayList; +import java.util.List; + +public class DashboardViewModel extends ViewModel { + private final MutableLiveData> mMonsters; + + public DashboardViewModel() { + mMonsters = new MutableLiveData<>(new ArrayList<>()); + } + + public LiveData> getMonsters() { + return mMonsters; + } + + public void setMonsters(List monsters) { + mMonsters.setValue(monsters); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditAbilityScoresFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditAbilityScoresFragment.java new file mode 100644 index 0000000..f22c94d --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditAbilityScoresFragment.java @@ -0,0 +1,82 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.ui.components.Stepper; +import com.majinnaibu.monstercards.ui.shared.MCFragment; + +import java.util.Locale; + +public class EditAbilityScoresFragment extends MCFragment { + private final String ABILITY_SCORE_FORMAT = "%d (%+d)"; + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + private int getModifier(int value) { + return value / 2 - 5; + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_ability_scores, container, false); + mHolder = new ViewHolder(root); + + mViewModel.getStrength().observe(getViewLifecycleOwner(), value -> mHolder.strength.setValue(value)); + mHolder.strength.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setStrength(newValue)); + mHolder.strength.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value))); + + mViewModel.getDexterity().observe(getViewLifecycleOwner(), value -> mHolder.dexterity.setValue(value)); + mHolder.dexterity.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setDexterity(newValue)); + mHolder.dexterity.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value))); + + mViewModel.getConstitution().observe(getViewLifecycleOwner(), value -> mHolder.constitution.setValue(value)); + mHolder.constitution.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setConstitution(newValue)); + mHolder.constitution.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value))); + + mViewModel.getIntelligence().observe(getViewLifecycleOwner(), value -> mHolder.intelligence.setValue(value)); + mHolder.intelligence.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setIntelligence(newValue)); + mHolder.intelligence.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value))); + + mViewModel.getWisdom().observe(getViewLifecycleOwner(), value -> mHolder.wisdom.setValue(value)); + mHolder.wisdom.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setWisdom(newValue)); + mHolder.wisdom.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value))); + + mViewModel.getCharisma().observe(getViewLifecycleOwner(), value -> mHolder.charisma.setValue(value)); + mHolder.charisma.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setCharisma(newValue)); + mHolder.charisma.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value))); + + return root; + } + + private static class ViewHolder { + final Stepper strength; + final Stepper dexterity; + final Stepper constitution; + final Stepper intelligence; + final Stepper wisdom; + final Stepper charisma; + + ViewHolder(@NonNull View root) { + 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); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditArmorFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditArmorFragment.java new file mode 100644 index 0000000..a50534e --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditArmorFragment.java @@ -0,0 +1,104 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SwitchCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.ArmorType; +import com.majinnaibu.monstercards.helpers.ArrayHelper; +import com.majinnaibu.monstercards.ui.components.Stepper; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditArmorFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_armor, container, false); + mHolder = new ViewHolder(root); + + mHolder.armorType.setAdapter(new ArrayAdapter(requireContext(), R.layout.dropdown_list_item, ArmorType.values()) { + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + ArmorType item = getItem(position); + TextView view = (TextView) super.getView(position, convertView, parent); + view.setText(item.displayName); + return view; + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + ArmorType item = getItem(position); + TextView view = (TextView) super.getDropDownView(position, convertView, parent); + view.setText(item.displayName); + return view; + } + }); + mHolder.armorType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + ArmorType selectedItem = (ArmorType) parent.getItemAtPosition(position); + mViewModel.setArmorType(selectedItem); + } + + @Override + public void onNothingSelected(AdapterView parent) { + mViewModel.setArmorType(ArmorType.NONE); + } + }); + mHolder.armorType.setSelection(ArrayHelper.indexOf(ArmorType.values(), mViewModel.getArmorType().getValue())); + + mHolder.naturalArmorBonus.setValue(mViewModel.getNaturalArmorBonusUnboxed()); + mHolder.naturalArmorBonus.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setNaturalArmorBonus(newValue)); + + mHolder.hasShield.setChecked(mViewModel.getHasShieldValueAsBoolean()); + mHolder.hasShield.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setHasShield(isChecked)); + + mHolder.shieldBonus.setValue(mViewModel.getShieldBonusUnboxed()); + mHolder.shieldBonus.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setShieldBonus(newValue)); + + mHolder.customArmor.setText(mViewModel.getCustomArmor().getValue()); + mHolder.customArmor.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomArmor(s.toString())))); + + return root; + } + + private static class ViewHolder { + private final Spinner armorType; + private final Stepper naturalArmorBonus; + private final SwitchCompat hasShield; + private final Stepper shieldBonus; + private final EditText customArmor; + + ViewHolder(@NonNull View root) { + armorType = root.findViewById(R.id.armorType); + naturalArmorBonus = root.findViewById(R.id.naturalArmorBonus); + hasShield = root.findViewById(R.id.hasShield); + shieldBonus = root.findViewById(R.id.shieldBonus); + customArmor = root.findViewById(R.id.customArmor); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditBasicInfoFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditBasicInfoFragment.java new file mode 100644 index 0000000..3fba542 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditBasicInfoFragment.java @@ -0,0 +1,88 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.google.android.material.switchmaterial.SwitchMaterial; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.ui.components.Stepper; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditBasicInfoFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + @Override + public void onStart() { + super.onStart(); + mHolder.name.requestFocus(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_basic_info, container, false); + mHolder = new ViewHolder(root); + + mHolder.name.setText(mViewModel.getName().getValue()); + mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString()))); + + mHolder.size.setText(mViewModel.getSize().getValue()); + mHolder.size.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setSize(s.toString()))); + + mHolder.type.setText(mViewModel.getType().getValue()); + mHolder.type.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setType(s.toString()))); + + mHolder.subtype.setText(mViewModel.getSubtype().getValue()); + mHolder.subtype.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setSubtype(s.toString()))); + + mHolder.alignment.setText(mViewModel.getAlignment().getValue()); + mHolder.alignment.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setAlignment(s.toString()))); + + mHolder.customHitPoints.setText(mViewModel.getCustomHitPoints().getValue()); + mHolder.customHitPoints.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomHitPoints(s.toString())))); + + mHolder.hitDice.setValue(mViewModel.getHitDiceUnboxed()); + mHolder.hitDice.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setHitDice(newValue)); + + mHolder.hasCustomHitPoints.setChecked(mViewModel.getHasCustomHitPointsValueAsBoolean()); + mHolder.hasCustomHitPoints.setOnCheckedChangeListener((button, isChecked) -> mViewModel.setHasCustomHitPoints(isChecked)); + + return root; + } + + private static class ViewHolder { + private final EditText name; + private final EditText size; + private final EditText type; + private final EditText subtype; + private final EditText alignment; + private final EditText customHitPoints; + private final Stepper hitDice; + private final SwitchMaterial hasCustomHitPoints; + + ViewHolder(@NonNull View root) { + name = root.findViewById(R.id.name); + size = root.findViewById(R.id.size); + type = root.findViewById(R.id.type); + subtype = root.findViewById(R.id.subtype); + alignment = root.findViewById(R.id.alignment); + customHitPoints = root.findViewById(R.id.customHitPoints); + hitDice = root.findViewById(R.id.hitDice); + hasCustomHitPoints = root.findViewById(R.id.hasCustomHitPoints); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditChallengeRatingFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditChallengeRatingFragment.java new file mode 100644 index 0000000..33196cd --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditChallengeRatingFragment.java @@ -0,0 +1,91 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.ChallengeRating; +import com.majinnaibu.monstercards.helpers.ArrayHelper; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditChallengeRatingFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_challenge_rating, container, false); + mHolder = new ViewHolder(root); + + mHolder.challengeRating.setAdapter(new ArrayAdapter(requireContext(), R.layout.dropdown_list_item, ChallengeRating.values()) { + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + ChallengeRating item = getItem(position); + TextView view = (TextView) super.getView(position, convertView, parent); + view.setText(item.displayName); + return view; + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + ChallengeRating item = getItem(position); + TextView view = (TextView) super.getDropDownView(position, convertView, parent); + view.setText(item.displayName); + return view; + } + }); + mHolder.challengeRating.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + ChallengeRating selectedItem = (ChallengeRating) parent.getItemAtPosition(position); + mViewModel.setChallengeRating(selectedItem); + } + + @Override + public void onNothingSelected(AdapterView parent) { + mViewModel.setChallengeRating(ChallengeRating.CUSTOM); + } + }); + mHolder.challengeRating.setSelection(ArrayHelper.indexOf(ChallengeRating.values(), mViewModel.getChallengeRating().getValue())); + + mHolder.customChallengeRatingDescription.setText(mViewModel.getCustomChallengeRatingDescription().getValue()); + mHolder.customChallengeRatingDescription.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomChallengeRatingDescription(s.toString())))); + + mHolder.customProficiencyBonus.setText(mViewModel.getCustomProficiencyBonusValueAsString()); + mHolder.customProficiencyBonus.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomProficiencyBonus(s.toString())))); + + return root; + } + + private static class ViewHolder { + final Spinner challengeRating; + final EditText customChallengeRatingDescription; + final EditText customProficiencyBonus; + + ViewHolder(@NonNull View root) { + challengeRating = root.findViewById(R.id.challengeRating); + customChallengeRatingDescription = root.findViewById(R.id.customChallengeRatingDescription); + customProficiencyBonus = root.findViewById(R.id.customProficiencyBonus); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java new file mode 100644 index 0000000..bc39d8d --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java @@ -0,0 +1,88 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SwitchCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditLanguageFragment extends MCFragment { + private EditMonsterViewModel mEditMonsterViewModel; + private EditLanguageViewModel mViewModel; + private ViewHolder mHolder; + private Language mOldLanguage; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + mViewModel = new ViewModelProvider(this).get(EditLanguageViewModel.class); + Bundle arguments = getArguments(); + if (arguments != null) { + EditLanguageFragmentArgs args = EditLanguageFragmentArgs.fromBundle(arguments); + mOldLanguage = new Language(args.getName(), args.getCanSpeak()); + mViewModel.copyFromLanguage(mOldLanguage); + } else { + Logger.logWTF("EditLanguageFragment needs arguments"); + mOldLanguage = null; + } + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_language, container, false); + mHolder = new ViewHolder(root); + + mHolder.name.setText(mViewModel.getName().getValue()); + mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString()))); + + mHolder.canSpeak.setChecked(mViewModel.getCanSpeakValue()); + mHolder.canSpeak.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setCanSpeak(isChecked)); + + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mViewModel.hasChanges()) { + mEditMonsterViewModel.replaceLanguage(mOldLanguage, mViewModel.getLanguage().getValue()); + } + Navigation.findNavController(requireView()).navigateUp(); + } + }); + + return root; + } + + @Override + public void onStart() { + super.onStart(); + mHolder.name.requestFocus(); + } + + private static class ViewHolder { + EditText name; + SwitchCompat canSpeak; + + ViewHolder(@NonNull View root) { + name = root.findViewById(R.id.name); + canSpeak = root.findViewById(R.id.canSpeak); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java new file mode 100644 index 0000000..1b38692 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java @@ -0,0 +1,68 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel; +import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData; + +public class EditLanguageViewModel extends ChangeTrackedViewModel { + private final ChangeTrackedLiveData mName; + private final ChangeTrackedLiveData mCanSpeak; + private final ChangeTrackedLiveData mLanguage; + + public EditLanguageViewModel() { + super(); + mName = new ChangeTrackedLiveData<>("New Language", this::makeDirty); + mCanSpeak = new ChangeTrackedLiveData<>(true, this::makeDirty); + mLanguage = new ChangeTrackedLiveData<>(makeLanguage(), this::makeDirty); + } + + public void copyFromLanguage(@NonNull Language language) { + mName.resetValue(language.getName()); + mCanSpeak.resetValue(language.getSpeaks()); + makeClean(); + } + + public LiveData getLanguage() { + return mLanguage; + } + + public LiveData getName() { + return mName; + } + + public void setName(String name) { + mName.setValue(name); + mLanguage.setValue(makeLanguage()); + } + + public LiveData getCanSpeak() { + return mCanSpeak; + } + + public void setCanSpeak(boolean canSpeak) { + mCanSpeak.setValue(canSpeak); + mLanguage.setValue(makeLanguage()); + } + + public boolean getCanSpeakValue(boolean defaultIfNull) { + Boolean boxedValue = mCanSpeak.getValue(); + if (boxedValue == null) { + return defaultIfNull; + } + return boxedValue; + } + + public boolean getCanSpeakValue() { + return getCanSpeakValue(false); + } + + @NonNull + private Language makeLanguage() { + Boolean boxedValue = mCanSpeak.getValue(); + boolean canSpeak = boxedValue != null && boxedValue; + return new Language(mName.getValue(), canSpeak); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java new file mode 100644 index 0000000..de9d70c --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java @@ -0,0 +1,101 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; +import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditLanguagesFragment extends MCFragment { + // TODO: Make the swipe to delete not happen for the header + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + private void navigateToEditLanguage(@NonNull Language language) { + NavDirections action = EditLanguagesFragmentDirections.actionEditLanguagesFragmentToEditLanguageFragment(language.getName(), language.getSpeaks()); + Navigation.findNavController(requireView()).navigate(action); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_languages_list, container, false); + mHolder = new ViewHolder(root); + setupRecyclerView(mHolder.list); + setupAddLanguageButton(mHolder.addLanguage); + + return root; + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { + Context context = requireContext(); + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(layoutManager); + + mViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> { + EditLanguagesRecyclerViewAdapter adapter = new EditLanguagesRecyclerViewAdapter( + mViewModel.getLanguagesArray(), + language -> { + if (language != null) { + navigateToEditLanguage(language); + } else { + Logger.logError("Can't navigate to EditSkill with a null skill"); + } + }, + mViewModel.getTelepathyRangeUnboxed(), + (value, previousValue) -> mViewModel.setTelepathyRange(value), + mViewModel.getUnderstandsButDescription().getValue(), + new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setUnderstandsButDescription(s.toString()))); + recyclerView.setAdapter(adapter); + }); + + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> { + if (position > 0) { + mViewModel.removeLanguage(position - 1); + } + }, null)); + itemTouchHelper.attachToRecyclerView(recyclerView); + } + + private void setupAddLanguageButton(@NonNull FloatingActionButton fab) { + fab.setOnClickListener(view -> { + Language newLanguage = mViewModel.addNewLanguage(); + navigateToEditLanguage(newLanguage); + }); + } + + private static class ViewHolder { + RecyclerView list; + FloatingActionButton addLanguage; + + ViewHolder(@NonNull View root) { + this.list = root.findViewById(R.id.list); + this.addLanguage = root.findViewById(R.id.add_language); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java new file mode 100644 index 0000000..e9163c6 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java @@ -0,0 +1,114 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListHeaderBinding; +import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListItemBinding; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.ui.components.Stepper; + +import java.util.List; +import java.util.Locale; + +public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter { + private final List mValues; + private final ItemCallback mOnClick; + private final int mTelepathyRange; + private final String mUnderstandsBut; + private final Stepper.OnValueChangeListener mOnTelepathyRangeChanged; + private final TextWatcher mOnUnderstandsButChanged; + + private final int HEADER_VIEW_TYPE = 1; + private final int ITEM_VIEW_TYPE = 2; + private final String DISTANCE_IN_FEET_FORMAT = "%d ft."; + + public EditLanguagesRecyclerViewAdapter(List items, ItemCallback onClick, int telepathyRange, Stepper.OnValueChangeListener telepathyRangeChangedListener, String understandsBut, TextWatcher understandsButChangedListener) { + mValues = items; + mOnClick = onClick; + mTelepathyRange = telepathyRange; + mOnTelepathyRangeChanged = telepathyRangeChangedListener; + mUnderstandsBut = understandsBut; + mOnUnderstandsButChanged = understandsButChangedListener; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == HEADER_VIEW_TYPE) { + return new HeaderViewHolder(FragmentEditLanguagesListHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + return new ItemViewHolder(FragmentEditLanguagesListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) { + if (holder instanceof HeaderViewHolder) { + HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder; + headerViewHolder.telepathy.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), DISTANCE_IN_FEET_FORMAT, value)); + headerViewHolder.telepathy.setValue(mTelepathyRange); + headerViewHolder.telepathy.setOnValueChangeListener(mOnTelepathyRangeChanged); + headerViewHolder.understandsBut.setText(mUnderstandsBut); + headerViewHolder.understandsBut.addTextChangedListener(mOnUnderstandsButChanged); + } else if (holder instanceof ItemViewHolder) { + ItemViewHolder itemViewHolder = (ItemViewHolder) holder; + itemViewHolder.mItem = mValues.get(position - 1); + itemViewHolder.mContentView.setText(itemViewHolder.mItem.getName()); + itemViewHolder.itemView.setOnClickListener(view -> { + if (mOnClick != null) { + mOnClick.onItemCallback(itemViewHolder.mItem); + } + }); + } + } + + @Override + public int getItemCount() { + return mValues.size() + 1; + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return HEADER_VIEW_TYPE; + } + return ITEM_VIEW_TYPE; + } + + public interface ItemCallback { + void onItemCallback(Language language); + } + + public static class HeaderViewHolder extends RecyclerView.ViewHolder { + public final Stepper telepathy; + public final EditText understandsBut; + + public HeaderViewHolder(@NonNull FragmentEditLanguagesListHeaderBinding binding) { + super(binding.getRoot()); + telepathy = binding.telepathy; + understandsBut = binding.understandsBut; + } + } + + public static class ItemViewHolder extends RecyclerView.ViewHolder { + public final TextView mContentView; + public Language mItem; + + public ItemViewHolder(@NonNull FragmentEditLanguagesListItemBinding binding) { + super(binding.getRoot()); + mContentView = binding.content; + } + + @NonNull + @Override + public String toString() { + return super.toString() + " '" + mContentView.getText() + "'"; + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java new file mode 100644 index 0000000..0e24067 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java @@ -0,0 +1,272 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; + +import com.google.android.material.snackbar.Snackbar; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.data.enums.StringType; +import com.majinnaibu.monstercards.data.enums.TraitType; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentArgs; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; + +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.observers.DisposableSingleObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class EditMonsterFragment extends MCFragment { + + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + + MonsterRepository repository = getMonsterRepository(); + Bundle arguments = getArguments(); + assert arguments != null; + UUID monsterId = UUID.fromString(MonsterDetailFragmentArgs.fromBundle(arguments).getMonsterId()); + + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + + View root = inflater.inflate(R.layout.fragment_edit_monster, container, false); + mHolder = new ViewHolder(root); + + setTitle(getString(R.string.title_editMonster_fmt, getString(R.string.default_monster_name))); + + // TODO: Show a loading spinner until we have the monster loaded. + if (mViewModel.hasError() || !mViewModel.hasLoaded() || !Objects.equals(mViewModel.getMonsterId().getValue(), monsterId)) { + repository.getMonster(monsterId).toObservable() + .firstOrError() + .subscribe(new DisposableSingleObserver() { + @Override + public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) { + mViewModel.setHasLoaded(true); + mViewModel.setHasError(false); + mViewModel.copyFromMonster(monster); + setTitle(getString(R.string.title_editMonster_fmt, monster.name)); + dispose(); + } + + @Override + public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { + // TODO: Show an error state. + Logger.logError(e); + mViewModel.setHasError(true); + mViewModel.setErrorMessage(e.toString()); + dispose(); + } + }); + } + + mHolder.basicInfoButton.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.armorButton.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditArmorFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.speedButton.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSpeedFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.abilityScoresButton.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditAbilityScoresFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.savingThrows.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSavingThrowsFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.challengeRating.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditChallengeRatingFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.skills.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSkillsFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.senses.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.SENSE); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.conditionImmunities.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.CONDITION_IMMUNITY); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.damageImmunities.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_IMMUNITY); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.damageResistances.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_RESISTANCE); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.damageVulnerabilities.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_VULNERABILITY); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.languages.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditLanguagesFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.abilities.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ABILITY); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.actions.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ACTION); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.lairActions.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LAIR_ACTION); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.legendaryActions.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LEGENDARY_ACTION); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.reactions.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REACTIONS); + Navigation.findNavController(requireView()).navigate(action); + }); + + mHolder.regionalActions.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REGIONAL_ACTION); + Navigation.findNavController(requireView()).navigate(action); + }); + + + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mViewModel.hasChanges()) { + View view = getView(); + AlertDialog alertDialog = new AlertDialog.Builder(requireContext()).create(); + alertDialog.setTitle("Unsaved Changes"); + alertDialog.setMessage("Do you want to save your changes?"); + alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Save", (dialog, id) -> { + // Save the monster. Navigate up if the save is successful. Show a SnackBar if there was an error. + getMonsterRepository().saveMonster(mViewModel.buildMonster()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + new DisposableCompletableObserver() { + @Override + public void onComplete() { + Navigation.findNavController(requireView()).navigateUp(); + } + + @Override + public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { + Logger.logError("Error creating monster", e); + assert view != null; + Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + } + }); + }); + alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Discard", (dialog, id) -> { + // Navigate up ignoring unsaved changes. + Navigation.findNavController(requireView()).navigateUp(); + }); + alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "Cancel", (dialog, id) -> { + // Do nothing. + }); + alertDialog.show(); + } else { + // No changes so we can safely leave. + Navigation.findNavController(requireView()).navigateUp(); + } + } + }); + + return root; + } + + private static class ViewHolder { + + TextView basicInfoButton; + TextView armorButton; + TextView speedButton; + TextView abilityScoresButton; + TextView savingThrows; + TextView skills; + TextView conditionImmunities; + TextView damageImmunities; + TextView damageResistances; + TextView damageVulnerabilities; + TextView senses; + TextView languages; + TextView challengeRating; + TextView abilities; + TextView actions; + TextView reactions; + TextView legendaryActions; + TextView lairActions; + TextView regionalActions; + + ViewHolder(@NonNull View root) { + basicInfoButton = root.findViewById(R.id.basicInfo); + armorButton = root.findViewById(R.id.armor); + speedButton = root.findViewById(R.id.speed); + abilityScoresButton = root.findViewById(R.id.abilityScores); + savingThrows = root.findViewById(R.id.savingThrows); + skills = root.findViewById(R.id.skills); + conditionImmunities = root.findViewById(R.id.conditionImmunities); + damageImmunities = root.findViewById(R.id.damageImmunities); + damageResistances = root.findViewById(R.id.damageResistances); + damageVulnerabilities = root.findViewById(R.id.damageVulnerabilities); + senses = root.findViewById(R.id.senses); + languages = root.findViewById(R.id.languages); + challengeRating = root.findViewById(R.id.challengeRating); + abilities = root.findViewById(R.id.abilities); + actions = root.findViewById(R.id.actions); + reactions = root.findViewById(R.id.reactions); + legendaryActions = root.findViewById(R.id.legendaryActions); + lairActions = root.findViewById(R.id.lairActions); + regionalActions = root.findViewById(R.id.regionalActions); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java new file mode 100644 index 0000000..00f76f8 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java @@ -0,0 +1,1092 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.data.enums.AdvantageType; +import com.majinnaibu.monstercards.data.enums.ArmorType; +import com.majinnaibu.monstercards.data.enums.ChallengeRating; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; +import com.majinnaibu.monstercards.data.enums.StringType; +import com.majinnaibu.monstercards.data.enums.TraitType; +import com.majinnaibu.monstercards.helpers.StringHelper; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.models.Skill; +import com.majinnaibu.monstercards.models.Trait; +import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel; +import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@SuppressWarnings({"ConstantConditions"}) +public class EditMonsterViewModel extends ChangeTrackedViewModel { + private final ChangeTrackedLiveData mMonsterId; + private final MutableLiveData mHasError; + private final MutableLiveData mHasLoaded; + private final ChangeTrackedLiveData mHasCustomHitPoints; + private final ChangeTrackedLiveData mHasShield; + private final ChangeTrackedLiveData mCanHover; + private final ChangeTrackedLiveData mHasCustomSpeed; + private final ChangeTrackedLiveData mArmorType; + private final ChangeTrackedLiveData mStrengthProficiency; + private final ChangeTrackedLiveData mStrengthAdvantage; + private final ChangeTrackedLiveData mDexterityProficiency; + private final ChangeTrackedLiveData mDexterityAdvantage; + private final ChangeTrackedLiveData mConstitutionProficiency; + private final ChangeTrackedLiveData mConstitutionAdvantage; + private final ChangeTrackedLiveData mIntelligenceProficiency; + private final ChangeTrackedLiveData mIntelligenceAdvantage; + private final ChangeTrackedLiveData mWisdomProficiency; + private final ChangeTrackedLiveData mWisdomAdvantage; + private final ChangeTrackedLiveData mCharismaProficiency; + private final ChangeTrackedLiveData mCharismaAdvantage; + private final ChangeTrackedLiveData mHitDice; + private final ChangeTrackedLiveData mNaturalArmorBonus; + private final ChangeTrackedLiveData mShieldBonus; + private final ChangeTrackedLiveData mWalkSpeed; + private final ChangeTrackedLiveData mBurrowSpeed; + private final ChangeTrackedLiveData mClimbSpeed; + private final ChangeTrackedLiveData mFlySpeed; + private final ChangeTrackedLiveData mSwimSpeed; + private final ChangeTrackedLiveData mStrength; + private final ChangeTrackedLiveData mDexterity; + private final ChangeTrackedLiveData mConstitution; + private final ChangeTrackedLiveData mIntelligence; + private final ChangeTrackedLiveData mWisdom; + private final ChangeTrackedLiveData mCharisma; + private final ChangeTrackedLiveData mName; + private final MutableLiveData mErrorMessage; + private final ChangeTrackedLiveData mSize; + private final ChangeTrackedLiveData mType; + private final ChangeTrackedLiveData mSubtype; + private final ChangeTrackedLiveData mAlignment; + private final ChangeTrackedLiveData mCustomHitPoints; + private final ChangeTrackedLiveData mCustomArmor; + private final ChangeTrackedLiveData mCustomSpeed; + private final ChangeTrackedLiveData mChallengeRating; + private final ChangeTrackedLiveData mCustomChallengeRatingDescription; + private final ChangeTrackedLiveData mCustomProficiencyBonus; + private final ChangeTrackedLiveData mTelepathyRange; + private final ChangeTrackedLiveData mUnderstandsButDescription; + private final ChangeTrackedLiveData> mSkills; + private final ChangeTrackedLiveData> mSenses; + private final ChangeTrackedLiveData> mDamageImmunities; + private final ChangeTrackedLiveData> mDamageResistances; + private final ChangeTrackedLiveData> mDamageVulnerabilities; + private final ChangeTrackedLiveData> mConditionImmunities; + private final ChangeTrackedLiveData> mLanguages; + private final ChangeTrackedLiveData> mAbilities; + private final ChangeTrackedLiveData> mActions; + private final ChangeTrackedLiveData> mReactions; + private final ChangeTrackedLiveData> mLairActions; + private final ChangeTrackedLiveData> mLegendaryActions; + private final ChangeTrackedLiveData> mRegionalActions; + + public EditMonsterViewModel() { + super(); + mErrorMessage = new MutableLiveData<>(""); + mHasError = new MutableLiveData<>(false); + mHasLoaded = new MutableLiveData<>(false); + + mName = new ChangeTrackedLiveData<>("", this::makeDirty); + mMonsterId = new ChangeTrackedLiveData<>(UUID.randomUUID(), this::makeDirty); + mSize = new ChangeTrackedLiveData<>("", this::makeDirty); + mType = new ChangeTrackedLiveData<>("", this::makeDirty); + mSubtype = new ChangeTrackedLiveData<>("", this::makeDirty); + mAlignment = new ChangeTrackedLiveData<>("", this::makeDirty); + mCustomHitPoints = new ChangeTrackedLiveData<>("", this::makeDirty); + mHitDice = new ChangeTrackedLiveData<>(0, this::makeDirty); + mNaturalArmorBonus = new ChangeTrackedLiveData<>(0, this::makeDirty); + mHasCustomHitPoints = new ChangeTrackedLiveData<>(false, this::makeDirty); + mArmorType = new ChangeTrackedLiveData<>(ArmorType.NONE, this::makeDirty); + mHasShield = new ChangeTrackedLiveData<>(false, this::makeDirty); + mShieldBonus = new ChangeTrackedLiveData<>(0, this::makeDirty); + mCustomArmor = new ChangeTrackedLiveData<>("", this::makeDirty); + mWalkSpeed = new ChangeTrackedLiveData<>(0, this::makeDirty); + mBurrowSpeed = new ChangeTrackedLiveData<>(0, this::makeDirty); + mClimbSpeed = new ChangeTrackedLiveData<>(0, this::makeDirty); + mFlySpeed = new ChangeTrackedLiveData<>(0, this::makeDirty); + mSwimSpeed = new ChangeTrackedLiveData<>(0, this::makeDirty); + mCanHover = new ChangeTrackedLiveData<>(false, this::makeDirty); + mHasCustomSpeed = new ChangeTrackedLiveData<>(false, this::makeDirty); + mCustomSpeed = new ChangeTrackedLiveData<>("", this::makeDirty); + mStrength = new ChangeTrackedLiveData<>(10, this::makeDirty); + mDexterity = new ChangeTrackedLiveData<>(10, this::makeDirty); + mConstitution = new ChangeTrackedLiveData<>(10, this::makeDirty); + mIntelligence = new ChangeTrackedLiveData<>(10, this::makeDirty); + mWisdom = new ChangeTrackedLiveData<>(10, this::makeDirty); + mCharisma = new ChangeTrackedLiveData<>(10, this::makeDirty); + mStrengthProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty); + mStrengthAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty); + mDexterityProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty); + mDexterityAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty); + mConstitutionProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty); + mConstitutionAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty); + mIntelligenceProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty); + mIntelligenceAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty); + mWisdomProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty); + mWisdomAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty); + mCharismaProficiency = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty); + mCharismaAdvantage = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty); + mChallengeRating = new ChangeTrackedLiveData<>(ChallengeRating.ONE_EIGHTH, this::makeDirty); + mCustomChallengeRatingDescription = new ChangeTrackedLiveData<>("", this::makeDirty); + mCustomProficiencyBonus = new ChangeTrackedLiveData<>(0, this::makeDirty); + mTelepathyRange = new ChangeTrackedLiveData<>(0, this::makeDirty); + mUnderstandsButDescription = new ChangeTrackedLiveData<>("", this::makeDirty); + mSkills = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mSenses = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mDamageImmunities = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mDamageResistances = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mDamageVulnerabilities = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mConditionImmunities = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mLanguages = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mAbilities = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mReactions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mLairActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mLegendaryActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + mRegionalActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty); + } + + public void copyFromMonster(@NonNull Monster monster) { + mMonsterId.resetValue(monster.id); + mName.resetValue(monster.name); + mSize.resetValue(monster.size); + mType.resetValue(monster.type); + mSubtype.resetValue(monster.subtype); + mAlignment.resetValue(monster.alignment); + mCustomHitPoints.resetValue(monster.customHPDescription); + mHitDice.resetValue(monster.hitDice); + mNaturalArmorBonus.resetValue(monster.naturalArmorBonus); + mHasCustomHitPoints.resetValue(monster.hasCustomHP); + mArmorType.resetValue(monster.armorType); + mHasShield.resetValue(monster.shieldBonus != 0); + mShieldBonus.resetValue(monster.shieldBonus); + mCustomArmor.resetValue(monster.otherArmorDescription); + mWalkSpeed.resetValue(monster.walkSpeed); + mBurrowSpeed.resetValue(monster.burrowSpeed); + mClimbSpeed.resetValue(monster.climbSpeed); + mFlySpeed.resetValue(monster.flySpeed); + mSwimSpeed.resetValue(monster.swimSpeed); + mCanHover.resetValue(monster.canHover); + mHasCustomSpeed.resetValue(monster.hasCustomSpeed); + mCustomSpeed.resetValue(monster.customSpeedDescription); + mStrength.resetValue(monster.strengthScore); + mDexterity.resetValue(monster.dexterityScore); + mConstitution.resetValue(monster.constitutionScore); + mIntelligence.resetValue(monster.intelligenceScore); + mWisdom.resetValue(monster.wisdomScore); + mCharisma.resetValue(monster.charismaScore); + mStrengthProficiency.resetValue(monster.strengthSavingThrowProficiency); + mStrengthAdvantage.resetValue(monster.strengthSavingThrowAdvantage); + mDexterityProficiency.resetValue(monster.dexteritySavingThrowProficiency); + mDexterityAdvantage.resetValue(monster.dexteritySavingThrowAdvantage); + mConstitutionProficiency.resetValue(monster.constitutionSavingThrowProficiency); + mConstitutionAdvantage.resetValue(monster.constitutionSavingThrowAdvantage); + mIntelligenceProficiency.resetValue(monster.intelligenceSavingThrowProficiency); + mIntelligenceAdvantage.resetValue(monster.intelligenceSavingThrowAdvantage); + mWisdomProficiency.resetValue(monster.wisdomSavingThrowProficiency); + mWisdomAdvantage.resetValue(monster.wisdomSavingThrowAdvantage); + mCharismaProficiency.resetValue(monster.charismaSavingThrowProficiency); + mCharismaAdvantage.resetValue(monster.charismaSavingThrowAdvantage); + mChallengeRating.resetValue(monster.challengeRating); + mCustomChallengeRatingDescription.resetValue(monster.customChallengeRatingDescription); + mCustomProficiencyBonus.resetValue(monster.customProficiencyBonus); + mTelepathyRange.resetValue(monster.telepathyRange); + mUnderstandsButDescription.resetValue(monster.understandsButDescription); + + ArrayList skills = new ArrayList<>(monster.skills); + Collections.sort(skills, Skill::compareTo); + mSkills.resetValue(skills); + ArrayList senses = new ArrayList<>(monster.senses); + Collections.sort(senses, String::compareToIgnoreCase); + mSenses.resetValue(senses); + ArrayList damageImmunities = new ArrayList<>(monster.damageImmunities); + Collections.sort(damageImmunities, String::compareToIgnoreCase); + mDamageImmunities.resetValue(damageImmunities); + ArrayList damageResistances = new ArrayList<>(monster.damageResistances); + Collections.sort(damageResistances, String::compareToIgnoreCase); + mDamageResistances.resetValue(damageResistances); + ArrayList damageVulnerabilities = new ArrayList<>(monster.damageVulnerabilities); + Collections.sort(damageVulnerabilities, String::compareToIgnoreCase); + mDamageVulnerabilities.resetValue(damageVulnerabilities); + ArrayList conditionImmunities = new ArrayList<>(monster.conditionImmunities); + Collections.sort(conditionImmunities, String::compareToIgnoreCase); + mConditionImmunities.resetValue(conditionImmunities); + ArrayList languages = new ArrayList<>(monster.languages); + Collections.sort(languages, Language::compareTo); + mLanguages.resetValue(languages); + mAbilities.resetValue(new ArrayList<>(monster.abilities)); + mActions.resetValue(new ArrayList<>(monster.actions)); + mReactions.resetValue(new ArrayList<>(monster.reactions)); + mLairActions.resetValue(new ArrayList<>(monster.lairActions)); + mLegendaryActions.resetValue(new ArrayList<>(monster.legendaryActions)); + mRegionalActions.resetValue(new ArrayList<>(monster.regionalActions)); + makeClean(); + } + + public LiveData getName() { + return mName; + } + + public void setName(@NonNull String name) { + mName.setValue(name); + } + + public LiveData getMonsterId() { + return mMonsterId; + } + + public void setErrorMessage(@NonNull String errorMessage) { + mErrorMessage.setValue(errorMessage); + } + + public LiveData getHasError() { + return mHasError; + } + + public void setHasError(@NonNull Boolean hasError) { + mHasError.setValue(hasError); + } + + public boolean hasError() { + return getHasError().getValue(); + } + + public LiveData getHasLoaded() { + return mHasLoaded; + } + + public void setHasLoaded(@NonNull Boolean hasLoaded) { + mHasLoaded.setValue(hasLoaded); + } + + public boolean hasLoaded() { + return getHasLoaded().getValue(); + } + + public LiveData getSize() { + return mSize; + } + + public void setSize(@NonNull String size) { + mSize.setValue(size); + } + + public LiveData getType() { + return mType; + } + + public void setType(@NonNull String type) { + mType.setValue(type); + } + + public LiveData getSubtype() { + return mSubtype; + } + + public void setSubtype(@NonNull String subtype) { + mSubtype.setValue(subtype); + } + + public LiveData getAlignment() { + return mAlignment; + } + + public void setAlignment(@NonNull String alignment) { + mAlignment.setValue(alignment); + } + + public LiveData getCustomHitPoints() { + return mCustomHitPoints; + } + + public void setCustomHitPoints(String customHitPoints) { + mCustomHitPoints.setValue(customHitPoints); + } + + public void setHitDice(int hitDice) { + mHitDice.setValue(hitDice); + } + + public int getHitDiceUnboxed() { + return Helpers.unboxInteger(mHitDice.getValue(), 1); + } + + public void setNaturalArmorBonus(int naturalArmorBonus) { + mNaturalArmorBonus.setValue(naturalArmorBonus); + } + + public int getNaturalArmorBonusUnboxed() { + return Helpers.unboxInteger(mNaturalArmorBonus.getValue(), 0); + } + + public LiveData getHasCustomHitPoints() { + return mHasCustomHitPoints; + } + + public void setHasCustomHitPoints(boolean hasCustomHitPoints) { + mHasCustomHitPoints.setValue(hasCustomHitPoints); + } + + public boolean getHasCustomHitPointsValueAsBoolean() { + return mHasCustomHitPoints.getValue(); + } + + public LiveData getArmorType() { + return mArmorType; + } + + public void setArmorType(ArmorType armorType) { + mArmorType.setValue(armorType); + } + + public void setHasShield(boolean hasShield) { + mHasShield.setValue(hasShield); + } + + public boolean getHasShieldValueAsBoolean() { + return mHasShield.getValue(); + } + + public void setShieldBonus(int shieldBonus) { + mShieldBonus.setValue(shieldBonus); + } + + public int getShieldBonusUnboxed() { + return Helpers.unboxInteger(mShieldBonus.getValue(), 0); + } + + public LiveData getCustomArmor() { + return mCustomArmor; + } + + public void setCustomArmor(String customArmor) { + mCustomArmor.setValue(customArmor); + } + + public String getShieldBonusValueAsString() { + return mShieldBonus.getValue().toString(); + } + + public LiveData getWalkSpeed() { + return mWalkSpeed; + } + + public void setWalkSpeed(int walkSpeed) { + mWalkSpeed.setValue(walkSpeed); + } + + public LiveData getBurrowSpeed() { + return mBurrowSpeed; + } + + public void setBurrowSpeed(int burrowSpeed) { + mBurrowSpeed.setValue(burrowSpeed); + } + + public LiveData getClimbSpeed() { + return mClimbSpeed; + } + + public void setClimbSpeed(int climbSpeed) { + mClimbSpeed.setValue(climbSpeed); + } + + public LiveData getFlySpeed() { + return mFlySpeed; + } + + public void setFlySpeed(int flySpeed) { + mFlySpeed.setValue(flySpeed); + } + + public LiveData getSwimSpeed() { + return mSwimSpeed; + } + + public void setSwimSpeed(int swimSpeed) { + mSwimSpeed.setValue(swimSpeed); + } + + public LiveData getCanHover() { + return mCanHover; + } + + public void setCanHover(boolean canHover) { + mCanHover.setValue(canHover); + } + + public LiveData getHasCustomSpeed() { + return mHasCustomSpeed; + } + + public void setHasCustomSpeed(boolean hasCustomSpeed) { + mHasCustomSpeed.setValue(hasCustomSpeed); + } + + public LiveData getCustomSpeed() { + return mCustomSpeed; + } + + public void setCustomSpeed(String customSpeed) { + mCustomSpeed.setValue(customSpeed); + } + + public LiveData getStrength() { + return mStrength; + } + + public void setStrength(int strength) { + mStrength.setValue(strength); + } + + public LiveData getDexterity() { + return mDexterity; + } + + public void setDexterity(int dexterity) { + mDexterity.setValue(dexterity); + } + + public LiveData getConstitution() { + return mConstitution; + } + + public void setConstitution(int constitution) { + mConstitution.setValue(constitution); + } + + public LiveData getIntelligence() { + return mIntelligence; + } + + public void setIntelligence(int intelligence) { + mIntelligence.setValue(intelligence); + } + + public LiveData getWisdom() { + return mWisdom; + } + + public void setWisdom(int wisdom) { + mWisdom.setValue(wisdom); + } + + public LiveData getCharisma() { + return mCharisma; + } + + public void setCharisma(int charisma) { + mCharisma.setValue(charisma); + } + + public LiveData getStrengthProficiency() { + return mStrengthProficiency; + } + + public void setStrengthProficiency(ProficiencyType proficiency) { + mStrengthProficiency.setValue(proficiency); + } + + public LiveData getStrengthAdvantage() { + return mStrengthAdvantage; + } + + public void setStrengthAdvantage(AdvantageType advantage) { + mStrengthAdvantage.setValue(advantage); + } + + public LiveData getDexterityProficiency() { + return mDexterityProficiency; + } + + public void setDexterityProficiency(ProficiencyType proficiency) { + mDexterityProficiency.setValue(proficiency); + } + + public LiveData getDexterityAdvantage() { + return mDexterityAdvantage; + } + + public void setDexterityAdvantage(AdvantageType advantage) { + mDexterityAdvantage.setValue(advantage); + } + + public LiveData getConstitutionProficiency() { + return mConstitutionProficiency; + } + + public void setConstitutionProficiency(ProficiencyType proficiency) { + mConstitutionProficiency.setValue(proficiency); + } + + public LiveData getConstitutionAdvantage() { + return mConstitutionAdvantage; + } + + public void setConstitutionAdvantage(AdvantageType advantage) { + mConstitutionAdvantage.setValue(advantage); + } + + public LiveData getIntelligenceProficiency() { + return mIntelligenceProficiency; + } + + public void setIntelligenceProficiency(ProficiencyType proficiency) { + mIntelligenceProficiency.setValue(proficiency); + } + + public LiveData getIntelligenceAdvantage() { + return mIntelligenceAdvantage; + } + + public void setIntelligenceAdvantage(AdvantageType advantage) { + mIntelligenceAdvantage.setValue(advantage); + } + + public LiveData getWisdomProficiency() { + return mWisdomProficiency; + } + + public void setWisdomProficiency(ProficiencyType proficiency) { + mWisdomProficiency.setValue(proficiency); + } + + public LiveData getWisdomAdvantage() { + return mWisdomAdvantage; + } + + public void setWisdomAdvantage(AdvantageType advantage) { + mWisdomAdvantage.setValue(advantage); + } + + public LiveData getCharismaProficiency() { + return mCharismaProficiency; + } + + public void setCharismaProficiency(ProficiencyType proficiency) { + mCharismaProficiency.setValue(proficiency); + } + + public LiveData getCharismaAdvantage() { + return mCharismaAdvantage; + } + + public void setCharismaAdvantage(AdvantageType advantage) { + mCharismaAdvantage.setValue(advantage); + } + + public LiveData getChallengeRating() { + return mChallengeRating; + } + + public void setChallengeRating(ChallengeRating challengeRating) { + mChallengeRating.setValue(challengeRating); + } + + public LiveData getCustomChallengeRatingDescription() { + return mCustomChallengeRatingDescription; + } + + public void setCustomChallengeRatingDescription(String customChallengeRatingDescription) { + mCustomChallengeRatingDescription.setValue(customChallengeRatingDescription); + } + + public LiveData getCustomProficiencyBonus() { + return mCustomProficiencyBonus; + } + + public void setCustomProficiencyBonus(int proficiencyBonus) { + mCustomProficiencyBonus.setValue(proficiencyBonus); + } + + public void setCustomProficiencyBonus(String proficiencyBonus) { + Integer parsedValue = StringHelper.parseInt(proficiencyBonus); + this.setCustomProficiencyBonus(parsedValue != null ? parsedValue : 0); + } + + public String getCustomProficiencyBonusValueAsString() { + return mCustomProficiencyBonus.getValue().toString(); + } + + public LiveData getTelepathyRange() { + return mTelepathyRange; + } + + public void setTelepathyRange(int telepathyRange) { + mTelepathyRange.setValue(telepathyRange); + } + + public int getTelepathyRangeUnboxed() { + return Helpers.unboxInteger(mTelepathyRange.getValue(), 0); + } + + public LiveData getUnderstandsButDescription() { + return mUnderstandsButDescription; + } + + public void setUnderstandsButDescription(String understandsButDescription) { + mUnderstandsButDescription.setValue(understandsButDescription); + } + + public LiveData> getSkills() { + return mSkills; + } + + public List getSkillsArray() { + return mSkills.getValue(); + } + + public Skill addNewSkill() { + Skill newSkill = new Skill("Unnamed Skill", AbilityScore.DEXTERITY); + ArrayList newSkills = new ArrayList<>(mSkills.getValue()); + newSkills.add(newSkill); + Collections.sort(newSkills, (skill1, skill2) -> skill1.name.compareToIgnoreCase(skill2.name)); + mSkills.setValue(newSkills); + return newSkill; + } + + public void removeSkill(int position) { + List skills = mSkills.getValue(); + ArrayList newSkills = new ArrayList<>(skills); + newSkills.remove(position); + mSkills.setValue(newSkills); + } + + public void replaceSkill(Skill newSkill, Skill oldSkill) { + List oldSkills = mSkills.getValue(); + if (oldSkills == null) { + oldSkills = new ArrayList<>(); + } + boolean hasReplaced = false; + ArrayList newSkills = new ArrayList<>(oldSkills.size()); + for (Skill skill : oldSkills) { + if (Objects.equals(skill, oldSkill)) { + newSkills.add(newSkill); + hasReplaced = true; + } else { + newSkills.add(skill); + } + } + if (!hasReplaced) { + newSkills.add(newSkill); + } + Collections.sort(newSkills, (skill1, skill2) -> skill1.name.compareToIgnoreCase(skill2.name)); + mSkills.setValue(newSkills); + } + + public LiveData> getSenses() { + return mSenses; + } + + public LiveData> getDamageImmunities() { + return mDamageImmunities; + } + + public List getDamageImmunitiesArray() { + return mDamageImmunities.getValue(); + } + + public LiveData> getDamageResistances() { + return mDamageResistances; + } + + public LiveData> getLanguages() { + return mLanguages; + } + + public List getLanguagesArray() { + return mLanguages.getValue(); + } + + public Language addNewLanguage() { + Language newLanguage = new Language("", true); + return Helpers.addItemToList(mLanguages, newLanguage, Language::compareTo); + } + + public void removeLanguage(int position) { + Helpers.removeFromList(mLanguages, position); + } + + public void replaceLanguage(Language oldLanguage, Language newLanguage) { + Helpers.replaceItemInList(mLanguages, oldLanguage, newLanguage, Language::compareTo); + } + + public Monster buildMonster() { + Monster monster = new Monster(); + + monster.id = mMonsterId.getValue(); + monster.name = mName.getValue(); + monster.size = mSize.getValue(); + monster.type = mType.getValue(); + monster.subtype = mSubtype.getValue(); + monster.alignment = mAlignment.getValue(); + monster.customHPDescription = mCustomHitPoints.getValue(); + monster.hitDice = mHitDice.getValue(); + monster.hasCustomHP = mHasCustomHitPoints.getValue(); + monster.armorType = mArmorType.getValue(); + monster.naturalArmorBonus = mNaturalArmorBonus.getValue(); + monster.shieldBonus = mShieldBonus.getValue(); + monster.otherArmorDescription = mCustomArmor.getValue(); + monster.walkSpeed = mWalkSpeed.getValue(); + monster.burrowSpeed = mBurrowSpeed.getValue(); + monster.climbSpeed = mClimbSpeed.getValue(); + monster.flySpeed = mFlySpeed.getValue(); + monster.swimSpeed = mSwimSpeed.getValue(); + monster.canHover = mCanHover.getValue(); + monster.hasCustomSpeed = mHasCustomSpeed.getValue(); + monster.customSpeedDescription = mCustomSpeed.getValue(); + monster.strengthScore = mStrength.getValue(); + monster.dexterityScore = mDexterity.getValue(); + monster.constitutionScore = mConstitution.getValue(); + monster.intelligenceScore = mIntelligence.getValue(); + monster.wisdomScore = mWisdom.getValue(); + monster.charismaScore = mCharisma.getValue(); + monster.strengthSavingThrowAdvantage = mStrengthAdvantage.getValue(); + monster.strengthSavingThrowProficiency = mStrengthProficiency.getValue(); + monster.dexteritySavingThrowAdvantage = mDexterityAdvantage.getValue(); + monster.dexteritySavingThrowProficiency = mDexterityProficiency.getValue(); + monster.constitutionSavingThrowAdvantage = mConstitutionAdvantage.getValue(); + monster.constitutionSavingThrowProficiency = mConstitutionProficiency.getValue(); + monster.intelligenceSavingThrowAdvantage = mIntelligenceAdvantage.getValue(); + monster.intelligenceSavingThrowProficiency = mIntelligenceProficiency.getValue(); + monster.wisdomSavingThrowAdvantage = mWisdomAdvantage.getValue(); + monster.wisdomSavingThrowProficiency = mWisdomProficiency.getValue(); + monster.charismaSavingThrowAdvantage = mCharismaAdvantage.getValue(); + monster.charismaSavingThrowProficiency = mCharismaProficiency.getValue(); + monster.challengeRating = mChallengeRating.getValue(); + monster.customChallengeRatingDescription = mCustomChallengeRatingDescription.getValue(); + monster.customProficiencyBonus = mCustomProficiencyBonus.getValue(); + monster.telepathyRange = mTelepathyRange.getValue(); + monster.understandsButDescription = mUnderstandsButDescription.getValue(); + monster.skills = new HashSet<>(mSkills.getValue()); + monster.senses = new HashSet<>(mSenses.getValue()); + monster.damageImmunities = new HashSet<>(mDamageImmunities.getValue()); + monster.damageResistances = new HashSet<>(mDamageResistances.getValue()); + monster.damageVulnerabilities = new HashSet<>(mDamageVulnerabilities.getValue()); + monster.conditionImmunities = new HashSet<>(mConditionImmunities.getValue()); + monster.languages = new HashSet<>(mLanguages.getValue()); + monster.abilities = new ArrayList<>(mAbilities.getValue()); + monster.actions = new ArrayList<>(mActions.getValue()); + monster.reactions = new ArrayList<>(mReactions.getValue()); + monster.lairActions = new ArrayList<>(mLairActions.getValue()); + monster.legendaryActions = new ArrayList<>(mLegendaryActions.getValue()); + monster.regionalActions = new ArrayList<>(mRegionalActions.getValue()); + + return monster; + } + + public LiveData> getTraits(@NonNull TraitType type) { + switch (type) { + case ABILITY: + return mAbilities; + case ACTION: + return mActions; + case LAIR_ACTION: + return mLairActions; + case LEGENDARY_ACTION: + return mLegendaryActions; + case REACTIONS: + return mReactions; + case REGIONAL_ACTION: + return mRegionalActions; + default: + Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type)); + return null; + } + } + + public void removeTrait(@NonNull TraitType type, int position) { + switch (type) { + case ABILITY: + Helpers.removeFromList(mAbilities, position); + break; + case ACTION: + Helpers.removeFromList(mActions, position); + break; + case LAIR_ACTION: + Helpers.removeFromList(mLairActions, position); + break; + case LEGENDARY_ACTION: + Helpers.removeFromList(mLegendaryActions, position); + break; + case REACTIONS: + Helpers.removeFromList(mReactions, position); + break; + case REGIONAL_ACTION: + Helpers.removeFromList(mRegionalActions, position); + break; + default: + Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type)); + break; + } + } + + public void replaceTrait(@NonNull TraitType type, Trait oldTrait, Trait newTrait) { + switch (type) { + case ABILITY: + Helpers.replaceItemInList(mAbilities, oldTrait, newTrait); + break; + case ACTION: + Helpers.replaceItemInList(mActions, oldTrait, newTrait); + break; + case LAIR_ACTION: + Helpers.replaceItemInList(mLairActions, oldTrait, newTrait); + break; + case LEGENDARY_ACTION: + Helpers.replaceItemInList(mLegendaryActions, oldTrait, newTrait); + break; + case REACTIONS: + Helpers.replaceItemInList(mReactions, oldTrait, newTrait); + break; + case REGIONAL_ACTION: + Helpers.replaceItemInList(mRegionalActions, oldTrait, newTrait); + break; + default: + Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type)); + } + } + + public Trait addNewTrait(@NonNull TraitType type) { + Trait newAction = new Trait("", ""); + switch (type) { + case ABILITY: + return Helpers.addItemToList(mAbilities, newAction); + case ACTION: + return Helpers.addItemToList(mActions, newAction); + case LAIR_ACTION: + return Helpers.addItemToList(mLairActions, newAction); + case LEGENDARY_ACTION: + return Helpers.addItemToList(mLegendaryActions, newAction); + case REACTIONS: + return Helpers.addItemToList(mReactions, newAction); + case REGIONAL_ACTION: + return Helpers.addItemToList(mRegionalActions, newAction); + default: + Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type)); + return null; + } + } + + public LiveData> getStrings(@NonNull StringType type) { + switch (type) { + case CONDITION_IMMUNITY: + return mConditionImmunities; + case DAMAGE_IMMUNITY: + return mDamageImmunities; + case DAMAGE_RESISTANCE: + return mDamageResistances; + case DAMAGE_VULNERABILITY: + return mDamageVulnerabilities; + case SENSE: + return mSenses; + default: + Logger.logUnimplementedFeature(String.format("Unrecognized StringType: %s", type)); + return null; + } + } + + public void removeString(@NonNull StringType type, int position) { + switch (type) { + case CONDITION_IMMUNITY: + Helpers.removeFromList(mConditionImmunities, position); + break; + case DAMAGE_IMMUNITY: + Helpers.removeFromList(mDamageImmunities, position); + break; + case DAMAGE_RESISTANCE: + Helpers.removeFromList(mDamageResistances, position); + break; + case DAMAGE_VULNERABILITY: + Helpers.removeFromList(mDamageVulnerabilities, position); + break; + case SENSE: + Helpers.removeFromList(mSenses, position); + break; + default: + Logger.logUnimplementedFeature(String.format("Unrecognized StringType: %s", type)); + break; + } + } + + public String addNewString(@NonNull StringType type) { + String newString = ""; + switch (type) { + case CONDITION_IMMUNITY: + return Helpers.addItemToList(mConditionImmunities, newString); + case DAMAGE_IMMUNITY: + return Helpers.addItemToList(mDamageImmunities, newString); + case DAMAGE_RESISTANCE: + return Helpers.addItemToList(mDamageResistances, newString); + case DAMAGE_VULNERABILITY: + return Helpers.addItemToList(mDamageVulnerabilities, newString); + case SENSE: + return Helpers.addItemToList(mSenses, newString); + default: + Logger.logUnimplementedFeature(String.format("Unrecognized StringType: %s", type)); + return null; + } + } + + public void replaceString(@NonNull StringType type, String oldValue, String newValue) { + switch (type) { + case CONDITION_IMMUNITY: + Helpers.replaceItemInList(mConditionImmunities, oldValue, newValue); + break; + case DAMAGE_IMMUNITY: + Helpers.replaceItemInList(mDamageImmunities, oldValue, newValue); + break; + case DAMAGE_RESISTANCE: + Helpers.replaceItemInList(mDamageResistances, oldValue, newValue); + break; + case DAMAGE_VULNERABILITY: + Helpers.replaceItemInList(mDamageVulnerabilities, oldValue, newValue); + break; + case SENSE: + Helpers.replaceItemInList(mSenses, oldValue, newValue); + break; + default: + Logger.logUnimplementedFeature(String.format("Unrecognized StringType: %s", type)); + } + } + + public boolean moveTrait(@NonNull TraitType type, int from, int to) { + switch (type) { + case ABILITY: + return Helpers.moveItemInList(mAbilities, from, to); + case ACTION: + return Helpers.moveItemInList(mActions, from, to); + case LAIR_ACTION: + return Helpers.moveItemInList(mLairActions, from, to); + case LEGENDARY_ACTION: + return Helpers.moveItemInList(mLegendaryActions, from, to); + case REACTIONS: + return Helpers.moveItemInList(mReactions, from, to); + case REGIONAL_ACTION: + return Helpers.moveItemInList(mRegionalActions, from, to); + default: + Logger.logUnimplementedFeature(String.format("Unrecognized TraitType: %s", type)); + return false; + } + + + } + + @SuppressWarnings("SameParameterValue") + private static class Helpers { + static T addItemToList(MutableLiveData> listData, T newItem) { + return addItemToList(listData, newItem, null); + } + + static T addItemToList(@NonNull MutableLiveData> listData, T newItem, Comparator comparator) { + ArrayList newList = new ArrayList<>(listData.getValue()); + newList.add(newItem); + if (comparator != null) { + Collections.sort(newList, comparator); + } + listData.setValue(newList); + return newItem; + } + + static void removeFromList(@NonNull MutableLiveData> listData, int position) { + List oldList = listData.getValue(); + ArrayList newList = new ArrayList<>(oldList); + newList.remove(position); + listData.setValue(newList); + } + + static void replaceItemInList(@NonNull MutableLiveData> listData, int position, T newItem, Comparator comparator) { + List oldList = listData.getValue(); + if (oldList == null) { + oldList = new ArrayList<>(); + } + int size = oldList.size(); + boolean hasReplaced = false; + ArrayList newList = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + if (index == position) { + newList.add(newItem); + hasReplaced = true; + } else { + newList.add(oldList.get(index)); + } + } + if (!hasReplaced) { + newList.add(newItem); + } + if (comparator != null) { + Collections.sort(newList, comparator); + } + listData.setValue(newList); + } + + @SuppressWarnings("unused") + static void replaceItemInList(MutableLiveData> listData, int position, T newItem) { + replaceItemInList(listData, position, newItem, null); + } + + static void replaceItemInList(@NonNull MutableLiveData> listData, T oldItem, T newItem, Comparator comparator) { + List oldList = listData.getValue(); + if (oldList == null) { + oldList = new ArrayList<>(); + } + boolean hasReplaced = false; + ArrayList newList = new ArrayList<>(oldList.size()); + for (T item : oldList) { + if (!hasReplaced && Objects.equals(item, oldItem)) { + newList.add(newItem); + hasReplaced = true; + } else { + newList.add(item); + } + } + if (!hasReplaced) { + newList.add(newItem); + } + if (comparator != null) { + Collections.sort(newList, comparator); + } + listData.setValue(newList); + } + + static void replaceItemInList(MutableLiveData> listData, T oldItem, T newItem) { + replaceItemInList(listData, oldItem, newItem, null); + } + + static int unboxInteger(Integer value, int defaultIfNull) { + if (value == null) { + return defaultIfNull; + } + return value; + } + + static boolean moveItemInList(@NonNull ChangeTrackedLiveData> listData, int from, int to) { + List oldList = listData.getValue(); + if (oldList == null) { + oldList = new ArrayList<>(); + } + ArrayList newList = new ArrayList<>(oldList); + T item = oldList.get(from); + if (from > to) { + from = from + 1; + } else if (to > from) { + to = to + 1; + } + newList.add(to, item); + newList.remove(from); + listData.setValue(newList); + return true; + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSavingThrowsFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSavingThrowsFragment.java new file mode 100644 index 0000000..340e9f5 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSavingThrowsFragment.java @@ -0,0 +1,94 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.ui.components.AdvantagePicker; +import com.majinnaibu.monstercards.ui.components.ProficiencyPicker; +import com.majinnaibu.monstercards.ui.shared.MCFragment; + +public class EditSavingThrowsFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mViewHolder; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_saving_throws, container, false); + mViewHolder = new ViewHolder(root); + + mViewHolder.strengthProficiency.setValue(mViewModel.getStrengthProficiency().getValue()); + mViewHolder.strengthProficiency.setOnValueChangedListener(value -> mViewModel.setStrengthProficiency(value)); + mViewHolder.strengthAdvantage.setValue(mViewModel.getStrengthAdvantage().getValue()); + mViewHolder.strengthAdvantage.setOnValueChangedListener(value -> mViewModel.setStrengthAdvantage(value)); + + mViewHolder.dexterityProficiency.setValue(mViewModel.getDexterityProficiency().getValue()); + mViewHolder.dexterityProficiency.setOnValueChangedListener(value -> mViewModel.setDexterityProficiency(value)); + mViewHolder.dexterityAdvantage.setValue(mViewModel.getDexterityAdvantage().getValue()); + mViewHolder.dexterityAdvantage.setOnValueChangedListener(value -> mViewModel.setDexterityAdvantage(value)); + + mViewHolder.constitutionProficiency.setValue(mViewModel.getConstitutionProficiency().getValue()); + mViewHolder.constitutionProficiency.setOnValueChangedListener(value -> mViewModel.setConstitutionProficiency(value)); + mViewHolder.constitutionAdvantage.setValue(mViewModel.getConstitutionAdvantage().getValue()); + mViewHolder.constitutionAdvantage.setOnValueChangedListener(value -> mViewModel.setConstitutionAdvantage(value)); + + mViewHolder.intelligenceProficiency.setValue(mViewModel.getIntelligenceProficiency().getValue()); + mViewHolder.intelligenceProficiency.setOnValueChangedListener(value -> mViewModel.setIntelligenceProficiency(value)); + mViewHolder.intelligenceAdvantage.setValue(mViewModel.getIntelligenceAdvantage().getValue()); + mViewHolder.intelligenceAdvantage.setOnValueChangedListener(value -> mViewModel.setIntelligenceAdvantage(value)); + + mViewHolder.wisdomProficiency.setValue(mViewModel.getWisdomProficiency().getValue()); + mViewHolder.wisdomProficiency.setOnValueChangedListener(value -> mViewModel.setWisdomProficiency(value)); + mViewHolder.wisdomAdvantage.setValue(mViewModel.getWisdomAdvantage().getValue()); + mViewHolder.wisdomAdvantage.setOnValueChangedListener(value -> mViewModel.setWisdomAdvantage(value)); + + mViewHolder.charismaProficiency.setValue(mViewModel.getCharismaProficiency().getValue()); + mViewHolder.charismaProficiency.setOnValueChangedListener(value -> mViewModel.setCharismaProficiency(value)); + mViewHolder.charismaAdvantage.setValue(mViewModel.getCharismaAdvantage().getValue()); + mViewHolder.charismaAdvantage.setOnValueChangedListener(value -> mViewModel.setCharismaAdvantage(value)); + + return root; + } + + private static class ViewHolder { + AdvantagePicker strengthAdvantage; + ProficiencyPicker strengthProficiency; + AdvantagePicker dexterityAdvantage; + ProficiencyPicker dexterityProficiency; + AdvantagePicker constitutionAdvantage; + ProficiencyPicker constitutionProficiency; + AdvantagePicker intelligenceAdvantage; + ProficiencyPicker intelligenceProficiency; + AdvantagePicker wisdomAdvantage; + ProficiencyPicker wisdomProficiency; + AdvantagePicker charismaAdvantage; + ProficiencyPicker charismaProficiency; + + ViewHolder(@NonNull View root) { + strengthAdvantage = root.findViewById(R.id.strengthAdvantage); + strengthProficiency = root.findViewById(R.id.strengthProficiency); + dexterityAdvantage = root.findViewById(R.id.dexterityAdvantage); + dexterityProficiency = root.findViewById(R.id.dexterityProficiency); + constitutionAdvantage = root.findViewById(R.id.constitutionAdvantage); + constitutionProficiency = root.findViewById(R.id.constitutionProficiency); + intelligenceAdvantage = root.findViewById(R.id.intelligenceAdvantage); + intelligenceProficiency = root.findViewById(R.id.intelligenceProficiency); + wisdomAdvantage = root.findViewById(R.id.wisdomAdvantage); + wisdomProficiency = root.findViewById(R.id.wisdomProficiency); + charismaAdvantage = root.findViewById(R.id.charismaAdvantage); + charismaProficiency = root.findViewById(R.id.charismaProficiency); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillFragment.java new file mode 100644 index 0000000..0e5a93e --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillFragment.java @@ -0,0 +1,99 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.models.Skill; +import com.majinnaibu.monstercards.ui.components.AbilityScorePicker; +import com.majinnaibu.monstercards.ui.components.AdvantagePicker; +import com.majinnaibu.monstercards.ui.components.ProficiencyPicker; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditSkillFragment extends MCFragment { + private EditMonsterViewModel mEditMonsterViewModel; + private EditSkillViewModel mViewModel; + private ViewHolder mHolder; + private Skill mOldSkill; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + mViewModel = new ViewModelProvider(this).get(EditSkillViewModel.class); + if (getArguments() != null) { + EditSkillFragmentArgs args = EditSkillFragmentArgs.fromBundle(getArguments()); + mOldSkill = new Skill(args.getName(), args.getAbilityScore(), args.getAdvantage(), args.getProficiency()); + mViewModel.copyFromSkill(mOldSkill); + } else { + Logger.logWTF("EditSkillFragment needs arguments."); + mOldSkill = null; + } + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_skill, container, false); + mHolder = new ViewHolder(root); + + mHolder.abilityScore.setValue(mViewModel.getAbilityScore().getValue()); + mHolder.abilityScore.setOnValueChangedListener(value -> mViewModel.setAbilityScore(value)); + + mHolder.advantage.setValue(mViewModel.getAdvantage().getValue()); + mHolder.advantage.setOnValueChangedListener(value -> mViewModel.setAdvantage(value)); + + mHolder.proficiency.setValue(mViewModel.getProficiency().getValue()); + mHolder.proficiency.setOnValueChangedListener(value -> mViewModel.setProficiency(value)); + + mHolder.name.setText(mViewModel.getName().getValue()); + mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString()))); + + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mViewModel.hasChanges()) { + mEditMonsterViewModel.replaceSkill(mViewModel.getSkill().getValue(), mOldSkill); + } + Navigation.findNavController(requireView()).navigateUp(); + } + }); + + return root; + } + + @Override + public void onStart() { + super.onStart(); + mHolder.name.requestFocus(); + } + + private static class ViewHolder { + AbilityScorePicker abilityScore; + AdvantagePicker advantage; + ProficiencyPicker proficiency; + EditText name; + + ViewHolder(@NonNull View root) { + abilityScore = root.findViewById(R.id.abilityScore); + advantage = root.findViewById(R.id.advantage); + proficiency = root.findViewById(R.id.proficiency); + name = root.findViewById(R.id.name); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillViewModel.java new file mode 100644 index 0000000..dd53c77 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillViewModel.java @@ -0,0 +1,81 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import com.majinnaibu.monstercards.data.enums.AbilityScore; +import com.majinnaibu.monstercards.data.enums.AdvantageType; +import com.majinnaibu.monstercards.data.enums.ProficiencyType; +import com.majinnaibu.monstercards.models.Skill; +import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel; +import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData; + +public class EditSkillViewModel extends ChangeTrackedViewModel { + private final ChangeTrackedLiveData mAbilityScore; + private final ChangeTrackedLiveData mAdvantageType; + private final ChangeTrackedLiveData mProficiencyType; + private final ChangeTrackedLiveData mName; + private final ChangeTrackedLiveData mSkill; + + public EditSkillViewModel() { + super(); + mAbilityScore = new ChangeTrackedLiveData<>(AbilityScore.STRENGTH, this::makeDirty); + mAdvantageType = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty); + mProficiencyType = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty); + mName = new ChangeTrackedLiveData<>("New Skill", this::makeDirty); + mSkill = new ChangeTrackedLiveData<>(makeSkill(), this::makeDirty); + } + + public void copyFromSkill(@NonNull Skill skill) { + mAbilityScore.resetValue(skill.abilityScore); + mAdvantageType.resetValue(skill.advantageType); + mProficiencyType.resetValue(skill.proficiencyType); + mName.resetValue(skill.name); + makeClean(); + } + + public LiveData getSkill() { + return mSkill; + } + + public LiveData getAbilityScore() { + return mAbilityScore; + } + + public void setAbilityScore(AbilityScore value) { + mAbilityScore.setValue(value); + mSkill.setValue(makeSkill()); + } + + public LiveData getAdvantage() { + return mAdvantageType; + } + + public void setAdvantage(AdvantageType value) { + mAdvantageType.setValue(value); + mSkill.setValue(makeSkill()); + } + + public LiveData getProficiency() { + return mProficiencyType; + } + + public void setProficiency(ProficiencyType value) { + mProficiencyType.setValue(value); + mSkill.setValue(makeSkill()); + } + + public LiveData getName() { + return mName; + } + + public void setName(String value) { + mName.setValue(value); + mSkill.setValue(makeSkill()); + } + + @NonNull + private Skill makeSkill() { + return new Skill(mName.getValue(), mAbilityScore.getValue(), mAdvantageType.getValue(), mProficiencyType.getValue()); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillsFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillsFragment.java new file mode 100644 index 0000000..e4ccf85 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillsFragment.java @@ -0,0 +1,92 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.models.Skill; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; +import com.majinnaibu.monstercards.utils.Logger; + +/** + * A fragment representing a list of Items. + */ +public class EditSkillsFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + private void navigateToEditSkill(@NonNull Skill skill) { + NavDirections action = EditSkillsFragmentDirections.actionEditSkillsFragmentToEditSkillFragment(skill.name, skill.abilityScore, skill.proficiencyType, skill.advantageType); + Navigation.findNavController(requireView()).navigate(action); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_skills_list, container, false); + mHolder = new ViewHolder(root); + setupRecyclerView(mHolder.list); + setupAddSkillButton(mHolder.addSkill); + + return root; + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { + Context context = requireContext(); + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(layoutManager); + + mViewModel.getSkills().observe(getViewLifecycleOwner(), skills -> { + EditSkillsRecyclerViewAdapter adapter = new EditSkillsRecyclerViewAdapter(mViewModel.getSkillsArray(), skill -> { + if (skill != null) { + navigateToEditSkill(skill); + } else { + Logger.logError("Can't navigate to EditSkill with a null skill"); + } + }); + recyclerView.setAdapter(adapter); + }); + + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> mViewModel.removeSkill(position), null)); + itemTouchHelper.attachToRecyclerView(recyclerView); + } + + private void setupAddSkillButton(@NonNull FloatingActionButton fab) { + fab.setOnClickListener(view -> { + Skill newSkill = mViewModel.addNewSkill(); + navigateToEditSkill(newSkill); + }); + } + + private static class ViewHolder { + RecyclerView list; + FloatingActionButton addSkill; + + ViewHolder(@NonNull View root) { + this.list = root.findViewById(R.id.list); + this.addSkill = root.findViewById(R.id.add_skill); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillsRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillsRecyclerViewAdapter.java new file mode 100644 index 0000000..f91b8ef --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSkillsRecyclerViewAdapter.java @@ -0,0 +1,68 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.databinding.FragmentEditSkillsListItemBinding; +import com.majinnaibu.monstercards.models.Skill; + +import java.util.List; + +/** + * {@link RecyclerView.Adapter} that can display a {@link Skill}. + */ +public class EditSkillsRecyclerViewAdapter extends RecyclerView.Adapter { + private final List mValues; + private final ItemCallback mOnClick; + + public EditSkillsRecyclerViewAdapter(List items, ItemCallback onClick) { + mValues = items; + mOnClick = onClick; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(FragmentEditSkillsListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { + holder.mItem = mValues.get(position); + holder.mContentView.setText(mValues.get(position).name); + holder.itemView.setOnClickListener(v -> { + if (mOnClick != null) { + mOnClick.onItemCallback(holder.mItem); + } + }); + } + + @Override + public int getItemCount() { + return mValues.size(); + } + + public interface ItemCallback { + void onItemCallback(Skill skill); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public final TextView mContentView; + public Skill mItem; + + public ViewHolder(@NonNull FragmentEditSkillsListItemBinding binding) { + super(binding.getRoot()); + mContentView = binding.content; + } + + @NonNull + @Override + public String toString() { + return super.toString() + " '" + mContentView.getText() + "'"; + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSpeedFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSpeedFragment.java new file mode 100644 index 0000000..0893635 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditSpeedFragment.java @@ -0,0 +1,90 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.SwitchCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.ui.components.Stepper; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditSpeedFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_speed, container, false); + mHolder = new ViewHolder(root); + + mHolder.baseSpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setWalkSpeed(newValue)); + mHolder.baseSpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value)); + mViewModel.getWalkSpeed().observe(getViewLifecycleOwner(), value -> mHolder.baseSpeed.setValue(value)); + + mHolder.burrowSpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setBurrowSpeed(newValue)); + mHolder.burrowSpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value)); + mViewModel.getBurrowSpeed().observe(getViewLifecycleOwner(), value -> mHolder.burrowSpeed.setValue(value)); + + mHolder.climbSpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setClimbSpeed(newValue)); + mHolder.climbSpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value)); + mViewModel.getClimbSpeed().observe(getViewLifecycleOwner(), value -> mHolder.climbSpeed.setValue(value)); + + mHolder.flySpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setFlySpeed(newValue)); + mHolder.flySpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value)); + mViewModel.getFlySpeed().observe(getViewLifecycleOwner(), value -> mHolder.flySpeed.setValue(value)); + + mHolder.swimSpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setSwimSpeed(newValue)); + mHolder.swimSpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value)); + mViewModel.getSwimSpeed().observe(getViewLifecycleOwner(), value -> mHolder.swimSpeed.setValue(value)); + + mViewModel.getCanHover().observe(getViewLifecycleOwner(), value -> mHolder.canHover.setChecked(value)); + mHolder.canHover.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setCanHover(isChecked)); + + mViewModel.getHasCustomSpeed().observe(getViewLifecycleOwner(), value -> mHolder.hasCustomSpeed.setChecked(value)); + mHolder.hasCustomSpeed.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setHasCustomSpeed(isChecked)); + + //mViewModel.getCustomSpeed().observe(getViewLifecycleOwner(), value -> mHolder.customSpeed.setText(value)); + mHolder.customSpeed.setText(mViewModel.getCustomSpeed().getValue()); + mHolder.customSpeed.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomSpeed(s.toString()))); + + return root; + } + + private static class ViewHolder { + + final Stepper baseSpeed; + final Stepper burrowSpeed; + final Stepper climbSpeed; + final Stepper flySpeed; + final Stepper swimSpeed; + final SwitchCompat canHover; + final SwitchCompat hasCustomSpeed; + final EditText customSpeed; + + ViewHolder(@NonNull View root) { + baseSpeed = root.findViewById(R.id.baseSpeed); + burrowSpeed = root.findViewById(R.id.burrowSpeed); + climbSpeed = root.findViewById(R.id.climbSpeed); + flySpeed = root.findViewById(R.id.flySpeed); + swimSpeed = root.findViewById(R.id.swimSpeed); + canHover = root.findViewById(R.id.canHover); + hasCustomSpeed = root.findViewById(R.id.hasCustomSpeed); + customSpeed = root.findViewById(R.id.customSpeed); + } + } + +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringFragment.java new file mode 100644 index 0000000..54e4ee4 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringFragment.java @@ -0,0 +1,102 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.StringType; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditStringFragment extends MCFragment { + private EditMonsterViewModel mEditMonsterViewModel; + private EditStringViewModel mViewModel; + private ViewHolder mHolder; + private String mOldValue; + private StringType mStringType; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + mViewModel = new ViewModelProvider(this).get(EditStringViewModel.class); + if (getArguments() != null) { + EditStringFragmentArgs args = EditStringFragmentArgs.fromBundle(getArguments()); + mOldValue = args.getValue(); + mViewModel.setValue(mOldValue); + mStringType = args.getStringType(); + } else { + Logger.logWTF("EditStringFragment needs arguments"); + mOldValue = null; + } + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_string, container, false); + mHolder = new ViewHolder(root); + setTitle(getTitleForStringType(mStringType)); + + mHolder.description.setText(mViewModel.getValueAsString()); + mHolder.description.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setValue(s.toString()))); + + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mViewModel.hasChanges()) { + mEditMonsterViewModel.replaceString(mStringType, mOldValue, mViewModel.getValueAsString()); + } + Navigation.findNavController(requireView()).navigateUp(); + } + }); + + return root; + } + + @NonNull + private String getTitleForStringType(@NonNull StringType type) { + switch (type) { + case CONDITION_IMMUNITY: + return getString(R.string.title_editConditionImmunity); + case DAMAGE_IMMUNITY: + return getString(R.string.title_editDamageImmunity); + case DAMAGE_RESISTANCE: + return getString(R.string.title_editDamageResistance); + case DAMAGE_VULNERABILITY: + return getString(R.string.title_editDamageVulnerability); + case SENSE: + return getString(R.string.title_editSense); + default: + return getString(R.string.title_editString); + } + } + + @Override + public void onStart() { + super.onStart(); + mHolder.description.requestFocus(); + } + + private static class ViewHolder { + EditText description; + + ViewHolder(@NonNull View root) { + description = root.findViewById(R.id.description); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringViewModel.java new file mode 100644 index 0000000..897ef53 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringViewModel.java @@ -0,0 +1,32 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import androidx.lifecycle.LiveData; + +import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel; +import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData; + +public class EditStringViewModel extends ChangeTrackedViewModel { + private final ChangeTrackedLiveData mValue; + + public EditStringViewModel() { + super(); + mValue = new ChangeTrackedLiveData<>("", this::makeDirty); + } + + public LiveData getValue() { + return mValue; + } + + public void setValue(String value) { + mValue.setValue(value); + } + + public String getValueAsString() { + return mValue.getValue(); + } + + public void resetValue(String value) { + makeClean(); + mValue.resetValue(value); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsFragment.java new file mode 100644 index 0000000..b3bfaa5 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsFragment.java @@ -0,0 +1,129 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.StringType; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.List; + +public class EditStringsFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + private StringType mStringType; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + Bundle arguments = getArguments(); + if (arguments != null) { + EditStringsFragmentArgs args = EditStringsFragmentArgs.fromBundle(arguments); + mStringType = args.getStringType(); + } else { + Logger.logWTF("EditStringsFragment needs arguments"); + } + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_strings_list, container, false); + mHolder = new ViewHolder(root); + setTitle(getTitleForStringType(mStringType)); + setupRecyclerView(mHolder.list); + setupAddButton(mHolder.addItem); + return root; + } + + @NonNull + private String getTitleForStringType(StringType type) { + switch (type) { + case CONDITION_IMMUNITY: + return getString(R.string.title_editConditionImmunities); + case DAMAGE_IMMUNITY: + return getString(R.string.title_editDamageImmunities); + case DAMAGE_RESISTANCE: + return getString(R.string.title_editDamageResistances); + case DAMAGE_VULNERABILITY: + return getString(R.string.title_editDamageVulnerabilities); + case SENSE: + return getString(R.string.title_editSenses); + default: + return getString(R.string.title_editStrings); + } + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { + Context context = requireContext(); + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(layoutManager); + + LiveData> stringsData = mViewModel.getStrings(mStringType); + if (stringsData != null) { + stringsData.observe(getViewLifecycleOwner(), strings -> { + EditStringsRecyclerViewAdapter adapter = new EditStringsRecyclerViewAdapter(strings, value -> { + if (value != null) { + navigateToEditString(value); + } else { + Logger.logError("Can't navigate to EditStringFragment with a null trait"); + } + }); + recyclerView.setAdapter(adapter); + }); + } + + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> mViewModel.removeString(mStringType, position), null)); + itemTouchHelper.attachToRecyclerView(recyclerView); + } + + private void setupAddButton(@NonNull FloatingActionButton fab) { + fab.setOnClickListener(view -> { + String newValue = mViewModel.addNewString(mStringType); + if (newValue != null) { + navigateToEditString(newValue); + } + }); + } + + protected void navigateToEditString(String value) { + NavDirections action = EditStringsFragmentDirections.actionEditStringsFragmentToEditStringFragment(mStringType, value); + Navigation.findNavController(requireView()).navigate(action); + } + + private static class ViewHolder { + RecyclerView list; + FloatingActionButton addItem; + + ViewHolder(@NonNull View root) { + list = root.findViewById(R.id.list); + addItem = root.findViewById(R.id.add_item); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsRecyclerViewAdapter.java new file mode 100644 index 0000000..fda532c --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsRecyclerViewAdapter.java @@ -0,0 +1,64 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.databinding.FragmentEditStringsListItemBinding; + +import java.util.List; + +public class EditStringsRecyclerViewAdapter extends RecyclerView.Adapter { + private final List mValues; + private final ItemCallback mOnClick; + + public EditStringsRecyclerViewAdapter(List items, ItemCallback onClick) { + mValues = items; + mOnClick = onClick; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(FragmentEditStringsListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { + holder.mItem = mValues.get(position); + holder.mContentView.setText(mValues.get(position)); + holder.itemView.setOnClickListener(v -> { + if (mOnClick != null) { + mOnClick.onItemCallback(holder.mItem); + } + }); + } + + @Override + public int getItemCount() { + return mValues.size(); + } + + public interface ItemCallback { + void onItemCallback(String value); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public final TextView mContentView; + public String mItem; + + public ViewHolder(@NonNull FragmentEditStringsListItemBinding binding) { + super(binding.getRoot()); + mContentView = binding.content; + } + + @NonNull + @Override + public String toString() { + return super.toString() + " '" + mContentView.getText() + "'"; + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitFragment.java new file mode 100644 index 0000000..b73ece1 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitFragment.java @@ -0,0 +1,110 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.TraitType; +import com.majinnaibu.monstercards.models.Trait; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditTraitFragment extends MCFragment { + private EditMonsterViewModel mEditMonsterViewModel; + private EditTraitViewModel mViewModel; + private EditTraitFragment.ViewHolder mHolder; + private Trait mOldValue; + private TraitType mTraitType; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + mViewModel = new ViewModelProvider(this).get(EditTraitViewModel.class); + if (getArguments() != null) { + EditTraitFragmentArgs args = EditTraitFragmentArgs.fromBundle(getArguments()); + mOldValue = new Trait(args.getName(), args.getDescription()); + mViewModel.copyFromTrait(mOldValue); + mTraitType = args.getTraitType(); + } else { + Logger.logWTF("EditTraitFragment needs arguments"); + mOldValue = null; + } + super.onCreate(savedInstanceState); + } + + @Nullable + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_trait, container, false); + mHolder = new EditTraitFragment.ViewHolder(root); + setTitle(getTitleForTraitType(mTraitType)); + + mHolder.name.setText(mViewModel.getNameAsString()); + mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString()))); + + mHolder.description.setText(mViewModel.getDescriptionAsString()); + mHolder.description.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setDescription(s.toString()))); + + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mViewModel.hasChanges()) { + mEditMonsterViewModel.replaceTrait(mTraitType, mOldValue, mViewModel.getAbilityValue()); + } + Navigation.findNavController(requireView()).navigateUp(); + } + }); + + return root; + } + + @Override + public void onStart() { + super.onStart(); + mHolder.name.requestFocus(); + } + + private String getTitleForTraitType(TraitType type) { + switch (type) { + case ABILITY: + return getString(R.string.title_editAbility); + case ACTION: + return getString(R.string.title_editAction); + case LAIR_ACTION: + return getString(R.string.title_editLairAction); + case LEGENDARY_ACTION: + return getString(R.string.title_editLegendaryAction); + case REACTIONS: + return getString(R.string.title_editReaction); + case REGIONAL_ACTION: + return getString(R.string.title_editRegionalAction); + default: + return getString(R.string.title_editTrait); + } + } + + private static class ViewHolder { + EditText description; + EditText name; + + ViewHolder(@NonNull View root) { + description = root.findViewById(R.id.description); + name = root.findViewById(R.id.name); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitViewModel.java new file mode 100644 index 0000000..6d37eb6 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitViewModel.java @@ -0,0 +1,63 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.majinnaibu.monstercards.models.Trait; +import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel; +import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData; + +public class EditTraitViewModel extends ChangeTrackedViewModel { + private final ChangeTrackedLiveData mName; + private final ChangeTrackedLiveData mDescription; + private final MutableLiveData mAbility; + + public EditTraitViewModel() { + super(); + mName = new ChangeTrackedLiveData<>("", this::makeDirty); + mDescription = new ChangeTrackedLiveData<>("", this::makeDirty); + mAbility = new MutableLiveData<>(makeAbility()); + } + + public LiveData getName() { + return mName; + } + + public void setName(String name) { + mName.setValue(name); + mAbility.setValue(makeAbility()); + } + + public String getNameAsString() { + return mName.getValue(); + } + + public LiveData getDescription() { + return mDescription; + } + + public void setDescription(String description) { + mDescription.setValue(description); + mAbility.setValue(makeAbility()); + } + + public String getDescriptionAsString() { + return mDescription.getValue(); + } + + public Trait getAbilityValue() { + return mAbility.getValue(); + } + + public void copyFromTrait(@NonNull Trait trait) { + makeClean(); + mName.resetValue(trait.name); + mDescription.resetValue(trait.description); + } + + @NonNull + private Trait makeAbility() { + return new Trait(mName.getValue(), mDescription.getValue()); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitsFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitsFragment.java new file mode 100644 index 0000000..ab6b0de --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitsFragment.java @@ -0,0 +1,131 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.enums.TraitType; +import com.majinnaibu.monstercards.models.Trait; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.List; + +public class EditTraitsFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + private TraitType mTraitType; + private EditTraitsRecyclerViewAdapter mAdapter; + + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + if (getArguments() != null) { + EditTraitsFragmentArgs args = EditTraitsFragmentArgs.fromBundle(getArguments()); + mTraitType = args.getTraitType(); + } else { + Logger.logWTF("EditTraitFragment needs arguments"); + } + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + View root = inflater.inflate(R.layout.fragment_edit_traits_list, container, false); + mHolder = new ViewHolder(root); + setTitle(getTitleForTraitType(mTraitType)); + setupRecyclerView(mHolder.list); + setupAddButton(mHolder.addTrait); + return root; + } + + @NonNull + private String getTitleForTraitType(TraitType type) { + switch (type) { + case ABILITY: + return getString(R.string.title_editAbilities); + case ACTION: + return getString(R.string.title_editActions); + case LAIR_ACTION: + return getString(R.string.title_editLairActions); + case LEGENDARY_ACTION: + return getString(R.string.title_editLegendaryActions); + case REACTIONS: + return getString(R.string.title_editReactions); + case REGIONAL_ACTION: + return getString(R.string.title_editRegionalActions); + default: + return getString(R.string.title_editTraits); + } + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { + Context context = requireContext(); + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(layoutManager); + + LiveData> traitData = mViewModel.getTraits(mTraitType); + mAdapter = new EditTraitsRecyclerViewAdapter(trait -> { + if (trait != null) { + navigateToEditTrait(trait); + } else { + Logger.logError("Can't navigate to EditTraitFragment with a null trait"); + } + }); + if (traitData != null) { + traitData.observe(getViewLifecycleOwner(), traits -> mAdapter.submitList(traits)); + } + recyclerView.setAdapter(mAdapter); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> mViewModel.removeTrait(mTraitType, position), (from, to) -> mViewModel.moveTrait(mTraitType, from, to))); + itemTouchHelper.attachToRecyclerView(recyclerView); + } + + private void setupAddButton(@NonNull FloatingActionButton fab) { + fab.setOnClickListener(view -> { + Trait newTrait = mViewModel.addNewTrait(mTraitType); + if (newTrait != null) { + navigateToEditTrait(newTrait); + } + }); + } + + protected void navigateToEditTrait(@NonNull Trait trait) { + NavDirections action = EditTraitsFragmentDirections.actionEditTraitListFragmentToEditTraitFragment(trait.description, trait.name, mTraitType); + Navigation.findNavController(requireView()).navigate(action); + } + + private static class ViewHolder { + RecyclerView list; + FloatingActionButton addTrait; + + ViewHolder(@NonNull View root) { + list = root.findViewById(R.id.list); + addTrait = root.findViewById(R.id.add_trait); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitsRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitsRecyclerViewAdapter.java new file mode 100644 index 0000000..b7fc0c6 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditTraitsRecyclerViewAdapter.java @@ -0,0 +1,71 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.databinding.FragmentEditTraitsListItemBinding; +import com.majinnaibu.monstercards.models.Trait; + +public class EditTraitsRecyclerViewAdapter extends ListAdapter { + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + + @Override + public boolean areItemsTheSame(@NonNull Trait oldItem, @NonNull Trait newItem) { + return oldItem.equals(newItem); + } + + @Override + public boolean areContentsTheSame(@NonNull Trait oldItem, @NonNull Trait newItem) { + return oldItem.equals(newItem); + } + }; + private final ItemCallback mOnClick; + + protected EditTraitsRecyclerViewAdapter(ItemCallback onClick) { + super(DIFF_CALLBACK); + mOnClick = onClick; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(FragmentEditTraitsListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { + holder.mItem = getItem(position); + holder.mContentView.setText(holder.mItem.name); + holder.itemView.setOnClickListener(v -> { + if (mOnClick != null) { + mOnClick.onItemCallback(holder.mItem); + } + }); + } + + public interface ItemCallback { + void onItemCallback(Trait trait); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public final TextView mContentView; + public Trait mItem; + + public ViewHolder(@NonNull FragmentEditTraitsListItemBinding binding) { + super(binding.getRoot()); + mContentView = binding.content; + } + + @NonNull + @Override + public String toString() { + return super.toString() + " '" + mContentView.getText() + "'"; + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java new file mode 100644 index 0000000..0499a93 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java @@ -0,0 +1,121 @@ +package com.majinnaibu.monstercards.ui.library; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; +import com.majinnaibu.monstercards.utils.Logger; + +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 LibraryFragment extends MCFragment { + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_library, container, false); + + FloatingActionButton fab = root.findViewById(R.id.fab); + assert fab != null; + setupAddMonsterButton(fab); + + final RecyclerView recyclerView = root.findViewById(R.id.monster_list); + assert recyclerView != null; + setupRecyclerView(recyclerView); + + return root; + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { + Context context = requireContext(); + MonsterRepository repository = this.getMonsterRepository(); + + LibraryRecyclerViewAdapter adapter = new LibraryRecyclerViewAdapter( + context, + repository.getMonsters(), + (monster) -> navigateToMonsterDetail(monster.id), + (monster) -> repository + .deleteMonster(monster) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new DisposableCompletableObserver() { + @Override + public void onComplete() { + } + + @Override + public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { + Logger.logError(e); + } + })); + recyclerView.setAdapter(adapter); + + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(layoutManager); + + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(requireContext(), (position, direction) -> adapter.deleteItem(position), null)); + itemTouchHelper.attachToRecyclerView(recyclerView); + } + + private void setupAddMonsterButton(@NonNull FloatingActionButton fab) { + fab.setOnClickListener(view -> { + Monster monster = new Monster(); + monster.name = getString(R.string.default_monster_name); + MonsterRepository repository = this.getMonsterRepository(); + repository.addMonster(monster) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + new DisposableCompletableObserver() { + @Override + public void onComplete() { + View view = getView(); + assert view != null; + Snackbar.make( + view, + getString(R.string.snackbar_monster_created, monster.name), + Snackbar.LENGTH_LONG) + .setAction("Action", (_view) -> navigateToMonsterDetail(monster.id)) + .show(); + } + + @Override + public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { + Logger.logError("Error creating monster", e); + View view = getView(); + assert view != null; + Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + } + }); + }); + } + + protected void navigateToMonsterDetail(@NonNull UUID monsterId) { + NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString()); + Navigation.findNavController(requireView()).navigate(action); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryRecyclerViewAdapter.java new file mode 100644 index 0000000..82a80da --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryRecyclerViewAdapter.java @@ -0,0 +1,115 @@ +package com.majinnaibu.monstercards.ui.library; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.models.Monster; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class LibraryRecyclerViewAdapter extends RecyclerView.Adapter { + private final Context mContext; + private final ItemCallback mOnDelete; + private final ItemCallback mOnClick; + private final Flowable> mItemsObservable; + private final View.OnClickListener mOnClickListener = new View.OnClickListener() { + @Override + public void onClick(@NonNull View view) { + Monster monster = (Monster) view.getTag(); + if (mOnClick != null) { + mOnClick.onItemCallback(monster); + } + } + }; + private List mValues; + private Disposable mDisposable; + + public LibraryRecyclerViewAdapter(Context context, + Flowable> itemsObservable, + ItemCallback onClick, + ItemCallback onDelete) { + mItemsObservable = itemsObservable; + mValues = new ArrayList<>(); + mContext = context; + mOnDelete = onDelete; + mOnClick = onClick; + mDisposable = null; + } + + @Override + @NonNull + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.monster_list_content, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final @NonNull ViewHolder holder, int position) { + Monster monster = mValues.get(position); + holder.mContentView.setText(monster.name); + holder.itemView.setTag(monster); + holder.itemView.setOnClickListener(mOnClickListener); + } + + @Override + public int getItemCount() { + return mValues.size(); + } + + public Context getContext() { + return mContext; + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + // TODO: consider moving this subscription out of the adapter and make the subscriber call setItems on the adapter + mDisposable = mItemsObservable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(monsters -> { + mValues = monsters; + notifyDataSetChanged(); + }); + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + mDisposable.dispose(); + } + + public void deleteItem(int position) { + if (mOnDelete != null) { + Monster monster = mValues.get(position); + mOnDelete.onItemCallback(monster); + } + } + + public interface ItemCallback { + void onItemCallback(Monster monster); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + final TextView mContentView; + + ViewHolder(View view) { + super(view); + mContentView = view.findViewById(R.id.content); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java new file mode 100644 index 0000000..f910dfa --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java @@ -0,0 +1,256 @@ +package com.majinnaibu.monstercards.ui.monster; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +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.lifecycle.ViewModelProvider; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.helpers.CommonMarkHelper; +import com.majinnaibu.monstercards.helpers.StringHelper; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.List; +import java.util.UUID; + +import io.reactivex.rxjava3.observers.DisposableSingleObserver; + +public class MonsterDetailFragment extends MCFragment { + private ViewHolder mHolder; + + private MonsterDetailViewModel mViewModel; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + MonsterRepository repository = getMonsterRepository(); + Bundle arguments = getArguments(); + assert arguments != null; + UUID monsterId = UUID.fromString(MonsterDetailFragmentArgs.fromBundle(arguments).getMonsterId()); + setHasOptionsMenu(true); + mViewModel = new ViewModelProvider(this).get(MonsterDetailViewModel.class); + repository.getMonster(monsterId).toObservable() + .firstOrError() + .subscribe(new DisposableSingleObserver() { + @Override + public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) { + mViewModel.setMonster(monster); + dispose(); + } + + @Override + public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { + Logger.logError(e); + dispose(); + } + }); + View root = inflater.inflate(R.layout.fragment_monster, container, false); + mHolder = new ViewHolder(root); + + mViewModel.getName().observe(getViewLifecycleOwner(), name -> { + mHolder.name.setText(name); + setTitle(getString(R.string.title_monsterDetails_fmt, name)); + }); + mViewModel.getMeta().observe(getViewLifecycleOwner(), mHolder.meta::setText); + mViewModel.getArmorClass().observe(getViewLifecycleOwner(), armorText -> setupLabeledTextView(mHolder.armorClass, armorText, R.string.label_armor_class)); + mViewModel.getHitPoints().observe(getViewLifecycleOwner(), hitPoints -> setupLabeledTextView(mHolder.hitPoints, hitPoints, R.string.label_hit_points)); + mViewModel.getSpeed().observe(getViewLifecycleOwner(), speed -> setupLabeledTextView(mHolder.speed, speed, R.string.label_speed)); + mViewModel.getStrength().observe(getViewLifecycleOwner(), mHolder.strength::setText); + mViewModel.getDexterity().observe(getViewLifecycleOwner(), mHolder.dexterity::setText); + mViewModel.getConstitution().observe(getViewLifecycleOwner(), mHolder.constitution::setText); + mViewModel.getIntelligence().observe(getViewLifecycleOwner(), mHolder.intelligence::setText); + mViewModel.getWisdom().observe(getViewLifecycleOwner(), mHolder.wisdom::setText); + mViewModel.getCharisma().observe(getViewLifecycleOwner(), mHolder.charisma::setText); + mViewModel.getSavingThrows().observe(getViewLifecycleOwner(), savingThrows -> setupOptionalTextView(mHolder.savingThrows, savingThrows, R.string.label_saving_throws)); + mViewModel.getSkills().observe(getViewLifecycleOwner(), skills -> setupOptionalTextView(mHolder.skills, skills, R.string.label_skills)); + mViewModel.getDamageVulnerabilities().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageVulnerabilities, damageTypes, R.string.label_damage_vulnerabilities)); + mViewModel.getDamageResistances().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageResistances, damageTypes, R.string.label_damage_resistances)); + mViewModel.getDamageImmunities().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageImmunities, damageTypes, R.string.label_damage_immunities)); + mViewModel.getConditionImmunities().observe(getViewLifecycleOwner(), conditionImmunities -> setupOptionalTextView(mHolder.conditionImmunities, conditionImmunities, R.string.label_condition_immunities)); + mViewModel.getSenses().observe(getViewLifecycleOwner(), senses -> setupOptionalTextView(mHolder.senses, senses, R.string.label_senses)); + mViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> setupOptionalTextView(mHolder.languages, languages, R.string.label_languages)); + mViewModel.getChallenge().observe(getViewLifecycleOwner(), challengeRating -> setupLabeledTextView(mHolder.challenge, challengeRating, R.string.label_challenge_rating)); + mViewModel.getAbilities().observe(getViewLifecycleOwner(), abilities -> setupTraitList(mHolder.abilities, abilities)); + mViewModel.getActions().observe(getViewLifecycleOwner(), actions -> setupTraitList(mHolder.actions, actions, mHolder.actions_label, mHolder.actions_divider)); + mViewModel.getReactions().observe(getViewLifecycleOwner(), reactions -> setupTraitList(mHolder.reactions, reactions, mHolder.reactions_label, mHolder.reactions_divider)); + mViewModel.getRegionalEffects().observe(getViewLifecycleOwner(), regionalEffects -> setupTraitList(mHolder.regionalEffects, regionalEffects, mHolder.regionalEffects_label, mHolder.regionalEffects_divider)); + mViewModel.getLairActions().observe(getViewLifecycleOwner(), lairActions -> setupTraitList(mHolder.lairActions, lairActions, mHolder.lairActions_label, mHolder.lairActions_divider)); + mViewModel.getLegendaryActions().observe(getViewLifecycleOwner(), legendaryActions -> setupTraitList(mHolder.legendaryActions, legendaryActions, mHolder.legendaryActions_label, mHolder.legendaryActions_divider)); + + return root; + } + + private void setupLabeledTextView(@NonNull 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; + Context context = getContext(); + DisplayMetrics displayMetrics = null; + if (context != null) { + Resources resources = context.getResources(); + if (resources != null) { + displayMetrics = resources.getDisplayMetrics(); + } + } + root.removeAllViews(); + for (String action : traits) { + TextView tvAction = new TextView(getContext()); + // 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 void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.monster_detail_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.menu_action_edit_monster) { + UUID monsterId = mViewModel.getId().getValue(); + if (monsterId != null) { + NavDirections action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString()); + Navigation.findNavController(requireView()).navigate(action); + } else { + Logger.logWTF("monsterId cannot be null."); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + private static class ViewHolder { + 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(@NonNull View 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/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailViewModel.java new file mode 100644 index 0000000..38e5d13 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailViewModel.java @@ -0,0 +1,214 @@ +package com.majinnaibu.monstercards.ui.monster; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.majinnaibu.monstercards.models.Monster; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class MonsterDetailViewModel extends ViewModel { + + private final MutableLiveData> mAbilities; + private final MutableLiveData> mActions; + private final MutableLiveData mArmorClass; + private final MutableLiveData mChallenge; + private final MutableLiveData mCharisma; + private final MutableLiveData mConditionImmunities; + private final MutableLiveData mConstitution; + private final MutableLiveData mDamageResistances; + private final MutableLiveData mDamageImmunities; + private final MutableLiveData mDamageVulnerabilities; + private final MutableLiveData mDexterity; + private final MutableLiveData mHitPoints; + private final MutableLiveData mIntelligence; + private final MutableLiveData> mLairActions; + private final MutableLiveData mLanguages; + private final MutableLiveData> mLegendaryActions; + private final MutableLiveData mMeta; + private final MutableLiveData mName; + private final MutableLiveData> mReactions; + private final MutableLiveData> mRegionalEffects; + private final MutableLiveData mSavingThrows; + private final MutableLiveData mSenses; + private final MutableLiveData mSkills; + private final MutableLiveData mSpeed; + private final MutableLiveData mStrength; + private final MutableLiveData mWisdom; + private final MutableLiveData mMonsterId; + private Monster mMonster; + + public MonsterDetailViewModel() { + mMonster = null; + mAbilities = new MutableLiveData<>(new ArrayList<>()); + mActions = new MutableLiveData<>(new ArrayList<>()); + mArmorClass = new MutableLiveData<>(""); + mChallenge = new MutableLiveData<>(""); + mCharisma = new MutableLiveData<>(""); + mConditionImmunities = new MutableLiveData<>(""); + mConstitution = new MutableLiveData<>(""); + mDamageImmunities = new MutableLiveData<>(""); + mDamageResistances = new MutableLiveData<>(""); + mDamageVulnerabilities = new MutableLiveData<>(""); + mDexterity = new MutableLiveData<>(""); + mHitPoints = new MutableLiveData<>(""); + mIntelligence = new MutableLiveData<>(""); + mLairActions = new MutableLiveData<>(new ArrayList<>()); + mLanguages = new MutableLiveData<>(""); + mLegendaryActions = new MutableLiveData<>(new ArrayList<>()); + mMeta = new MutableLiveData<>(""); + mName = new MutableLiveData<>(""); + mReactions = new MutableLiveData<>(new ArrayList<>()); + mRegionalEffects = new MutableLiveData<>(new ArrayList<>()); + mSavingThrows = new MutableLiveData<>(""); + mSenses = new MutableLiveData<>(""); + mSkills = new MutableLiveData<>(""); + mSpeed = new MutableLiveData<>(""); + mStrength = new MutableLiveData<>(""); + mWisdom = new MutableLiveData<>(""); + mMonsterId = new MutableLiveData<>(UUID.fromString("00000000-0000-0000-0000-000000000000")); + } + + public LiveData> getAbilities() { + return mAbilities; + } + + public LiveData> getActions() { + return mActions; + } + + public LiveData> getReactions() { + return mReactions; + } + + public LiveData> getLegendaryActions() { + return mLegendaryActions; + } + + public LiveData> getLairActions() { + return mLairActions; + } + + public LiveData> getRegionalEffects() { + return mRegionalEffects; + } + + public LiveData getArmorClass() { + return mArmorClass; + } + + public LiveData getChallenge() { + return mChallenge; + } + + public LiveData getCharisma() { + return mCharisma; + } + + public LiveData getConditionImmunities() { + return mConditionImmunities; + } + + public LiveData getConstitution() { + return mConstitution; + } + + public LiveData getDamageResistances() { + return mDamageResistances; + } + + public LiveData getDamageImmunities() { + return mDamageImmunities; + } + + public LiveData getDamageVulnerabilities() { + return mDamageVulnerabilities; + } + + public LiveData getDexterity() { + return mDexterity; + } + + public LiveData getHitPoints() { + return mHitPoints; + } + + public LiveData getIntelligence() { + return mIntelligence; + } + + public LiveData getLanguages() { + return mLanguages; + } + + public LiveData getMeta() { + return mMeta; + } + + public LiveData getName() { + return mName; + } + + public LiveData getSavingThrows() { + return mSavingThrows; + } + + public LiveData getSenses() { + return mSenses; + } + + public LiveData getSkills() { + return mSkills; + } + + public LiveData getSpeed() { + return mSpeed; + } + + public LiveData getStrength() { + return mStrength; + } + + public LiveData getWisdom() { + return mWisdom; + } + + public LiveData getId() { + return mMonsterId; + } + + public void setMonster(@NonNull Monster monster) { + mMonster = monster; + mAbilities.setValue(mMonster.getAbilityDescriptions()); + mActions.setValue(mMonster.getActionDescriptions()); + mArmorClass.setValue(mMonster.getArmorClass()); + mChallenge.setValue(mMonster.getChallengeRatingDescription()); + mCharisma.setValue(monster.getCharismaDescription()); + mConditionImmunities.setValue(mMonster.getConditionImmunitiesDescription()); + mConstitution.setValue(monster.getConstitutionDescription()); + mDamageImmunities.setValue(mMonster.getDamageImmunitiesDescription()); + mDamageResistances.setValue(mMonster.getDamageResistancesDescription()); + mDamageVulnerabilities.setValue(mMonster.getDamageVulnerabilitiesDescription()); + mDexterity.setValue(monster.getDexterityDescription()); + mHitPoints.setValue(mMonster.getHitPoints()); + mIntelligence.setValue(monster.getIntelligenceDescription()); + mLairActions.setValue(mMonster.getLairActionDescriptions()); + mLanguages.setValue(mMonster.getLanguagesDescription()); + mLegendaryActions.setValue(mMonster.getLegendaryActionDescriptions()); + mMeta.setValue(mMonster.getMeta()); + mMonsterId.setValue(mMonster.id); + mName.setValue(mMonster.name); + mReactions.setValue(monster.getReactionDescriptions()); + mRegionalEffects.setValue(monster.getRegionalActionDescriptions()); + mSavingThrows.setValue(monster.getSavingThrowsDescription()); + mSenses.setValue(monster.getSensesDescription()); + mSkills.setValue(monster.getSkillsDescription()); + mSpeed.setValue(mMonster.getSpeedText()); + mStrength.setValue(monster.getStrengthDescription()); + mWisdom.setValue(monster.getWisdomDescription()); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java new file mode 100644 index 0000000..a4912f7 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportFragment.java @@ -0,0 +1,278 @@ +package com.majinnaibu.monstercards.ui.monster; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +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.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; + +import com.google.android.material.snackbar.Snackbar; +import com.majinnaibu.monstercards.MonsterCardsApplication; +import com.majinnaibu.monstercards.R; +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.library.LibraryFragmentDirections; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.List; +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 MonsterImportFragment extends MCFragment { + private ViewHolder mHolder; + private MonsterImportViewModel mViewModel; + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Logger.logDebug("MonsterCards: loading monster for import"); + Bundle arguments = getArguments(); + assert arguments != null; + String json = MonsterImportFragmentArgs.fromBundle(arguments).getJson(); + setHasOptionsMenu(true); + Monster monster = MonsterImportHelper.fromJSON(json); + mViewModel = new ViewModelProvider(this).get(MonsterImportViewModel.class); + mViewModel.setMonster(monster); + View root = inflater.inflate(R.layout.fragment_monster, container, false); + mHolder = new ViewHolder(root); + + mViewModel.getName().observe(getViewLifecycleOwner(), mHolder.name::setText); + mViewModel.getMeta().observe(getViewLifecycleOwner(), mHolder.meta::setText); + mViewModel.getArmorClass().observe(getViewLifecycleOwner(), armorText -> setupLabeledTextView(mHolder.armorClass, armorText, R.string.label_armor_class)); + mViewModel.getHitPoints().observe(getViewLifecycleOwner(), hitPoints -> setupLabeledTextView(mHolder.hitPoints, hitPoints, R.string.label_hit_points)); + mViewModel.getSpeed().observe(getViewLifecycleOwner(), speed -> setupLabeledTextView(mHolder.speed, speed, R.string.label_speed)); + mViewModel.getStrength().observe(getViewLifecycleOwner(), mHolder.strength::setText); + mViewModel.getDexterity().observe(getViewLifecycleOwner(), mHolder.dexterity::setText); + mViewModel.getConstitution().observe(getViewLifecycleOwner(), mHolder.constitution::setText); + mViewModel.getIntelligence().observe(getViewLifecycleOwner(), mHolder.intelligence::setText); + mViewModel.getWisdom().observe(getViewLifecycleOwner(), mHolder.wisdom::setText); + mViewModel.getCharisma().observe(getViewLifecycleOwner(), mHolder.charisma::setText); + mViewModel.getSavingThrows().observe(getViewLifecycleOwner(), savingThrows -> setupOptionalTextView(mHolder.savingThrows, savingThrows, R.string.label_saving_throws)); + mViewModel.getSkills().observe(getViewLifecycleOwner(), skills -> setupOptionalTextView(mHolder.skills, skills, R.string.label_skills)); + mViewModel.getDamageVulnerabilities().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageVulnerabilities, damageTypes, R.string.label_damage_vulnerabilities)); + mViewModel.getDamageResistances().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageResistances, damageTypes, R.string.label_damage_resistances)); + mViewModel.getDamageImmunities().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageImmunities, damageTypes, R.string.label_damage_immunities)); + mViewModel.getConditionImmunities().observe(getViewLifecycleOwner(), conditionImmunities -> setupOptionalTextView(mHolder.conditionImmunities, conditionImmunities, R.string.label_condition_immunities)); + mViewModel.getSenses().observe(getViewLifecycleOwner(), senses -> setupOptionalTextView(mHolder.senses, senses, R.string.label_senses)); + mViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> setupOptionalTextView(mHolder.languages, languages, R.string.label_languages)); + mViewModel.getChallenge().observe(getViewLifecycleOwner(), challengeRating -> setupLabeledTextView(mHolder.challenge, challengeRating, R.string.label_challenge_rating)); + mViewModel.getAbilities().observe(getViewLifecycleOwner(), abilities -> setupTraitList(mHolder.abilities, abilities)); + mViewModel.getActions().observe(getViewLifecycleOwner(), actions -> setupTraitList(mHolder.actions, actions, mHolder.actions_label, mHolder.actions_divider)); + mViewModel.getReactions().observe(getViewLifecycleOwner(), reactions -> setupTraitList(mHolder.reactions, reactions, mHolder.reactions_label, mHolder.reactions_divider)); + mViewModel.getRegionalEffects().observe(getViewLifecycleOwner(), regionalEffects -> setupTraitList(mHolder.regionalEffects, regionalEffects, mHolder.regionalEffects_label, mHolder.regionalEffects_divider)); + mViewModel.getLairActions().observe(getViewLifecycleOwner(), lairActions -> setupTraitList(mHolder.lairActions, lairActions, mHolder.lairActions_label, mHolder.lairActions_divider)); + mViewModel.getLegendaryActions().observe(getViewLifecycleOwner(), legendaryActions -> setupTraitList(mHolder.legendaryActions, legendaryActions, mHolder.legendaryActions_label, mHolder.legendaryActions_divider)); + + return root; + } + + private void setupLabeledTextView(@NonNull 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; + Context context = getContext(); + DisplayMetrics displayMetrics = null; + if (context != null) { + Resources resources = context.getResources(); + if (resources != null) { + displayMetrics = resources.getDisplayMetrics(); + } + } + root.removeAllViews(); + for (String action : traits) { + TextView tvAction = new TextView(getContext()); + // 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 void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.import_monster, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @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(@NonNull UUID monsterId) { + NavController navController = Navigation.findNavController(requireView()); + NavDirections action; + action = MonsterImportFragmentDirections.actionMonsterImportFragmentToNavigationLibrary(); + navController.navigate(action); + action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString()); + navController.navigate(action); + action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString()); + navController.navigate(action); + } + + 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(@NonNull 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/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportViewModel.java new file mode 100644 index 0000000..ae13d15 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterImportViewModel.java @@ -0,0 +1,217 @@ +package com.majinnaibu.monstercards.ui.monster; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.majinnaibu.monstercards.models.Monster; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class MonsterImportViewModel extends ViewModel { + private final MutableLiveData> mAbilities; + private final MutableLiveData> mActions; + private final MutableLiveData mArmorClass; + private final MutableLiveData mChallenge; + private final MutableLiveData mCharisma; + private final MutableLiveData mConditionImmunities; + private final MutableLiveData mConstitution; + private final MutableLiveData mDamageResistances; + private final MutableLiveData mDamageImmunities; + private final MutableLiveData mDamageVulnerabilities; + private final MutableLiveData mDexterity; + private final MutableLiveData mHitPoints; + private final MutableLiveData mIntelligence; + private final MutableLiveData> mLairActions; + private final MutableLiveData mLanguages; + private final MutableLiveData> mLegendaryActions; + private final MutableLiveData mMeta; + private final MutableLiveData mName; + private final MutableLiveData> mReactions; + private final MutableLiveData> mRegionalEffects; + private final MutableLiveData mSavingThrows; + private final MutableLiveData mSenses; + private final MutableLiveData mSkills; + private final MutableLiveData mSpeed; + private final MutableLiveData mStrength; + private final MutableLiveData mWisdom; + private final MutableLiveData mMonsterId; + private Monster mMonster; + + public MonsterImportViewModel() { + mMonster = null; + mAbilities = new MutableLiveData<>(new ArrayList<>()); + mActions = new MutableLiveData<>(new ArrayList<>()); + mArmorClass = new MutableLiveData<>(""); + mChallenge = new MutableLiveData<>(""); + mCharisma = new MutableLiveData<>(""); + mConditionImmunities = new MutableLiveData<>(""); + mConstitution = new MutableLiveData<>(""); + mDamageImmunities = new MutableLiveData<>(""); + mDamageResistances = new MutableLiveData<>(""); + mDamageVulnerabilities = new MutableLiveData<>(""); + mDexterity = new MutableLiveData<>(""); + mHitPoints = new MutableLiveData<>(""); + mIntelligence = new MutableLiveData<>(""); + mLairActions = new MutableLiveData<>(new ArrayList<>()); + mLanguages = new MutableLiveData<>(""); + mLegendaryActions = new MutableLiveData<>(new ArrayList<>()); + mMeta = new MutableLiveData<>(""); + mName = new MutableLiveData<>(""); + mReactions = new MutableLiveData<>(new ArrayList<>()); + mRegionalEffects = new MutableLiveData<>(new ArrayList<>()); + mSavingThrows = new MutableLiveData<>(""); + mSenses = new MutableLiveData<>(""); + mSkills = new MutableLiveData<>(""); + mSpeed = new MutableLiveData<>(""); + mStrength = new MutableLiveData<>(""); + mWisdom = new MutableLiveData<>(""); + mMonsterId = new MutableLiveData<>(UUID.fromString("00000000-0000-0000-0000-000000000000")); + } + + public LiveData> getAbilities() { + return mAbilities; + } + + public LiveData> getActions() { + return mActions; + } + + public LiveData> getReactions() { + return mReactions; + } + + public LiveData> getLegendaryActions() { + return mLegendaryActions; + } + + public LiveData> getLairActions() { + return mLairActions; + } + + public LiveData> getRegionalEffects() { + return mRegionalEffects; + } + + public LiveData getArmorClass() { + return mArmorClass; + } + + public LiveData getChallenge() { + return mChallenge; + } + + public LiveData getCharisma() { + return mCharisma; + } + + public LiveData getConditionImmunities() { + return mConditionImmunities; + } + + public LiveData getConstitution() { + return mConstitution; + } + + public LiveData getDamageResistances() { + return mDamageResistances; + } + + public LiveData getDamageImmunities() { + return mDamageImmunities; + } + + public LiveData getDamageVulnerabilities() { + return mDamageVulnerabilities; + } + + public LiveData getDexterity() { + return mDexterity; + } + + public LiveData getHitPoints() { + return mHitPoints; + } + + public LiveData getIntelligence() { + return mIntelligence; + } + + public LiveData getLanguages() { + return mLanguages; + } + + public LiveData getMeta() { + return mMeta; + } + + public LiveData getName() { + return mName; + } + + public LiveData getSavingThrows() { + return mSavingThrows; + } + + public LiveData getSenses() { + return mSenses; + } + + public LiveData getSkills() { + return mSkills; + } + + public LiveData getSpeed() { + return mSpeed; + } + + public LiveData getStrength() { + return mStrength; + } + + public LiveData getWisdom() { + return mWisdom; + } + + public LiveData getId() { + return mMonsterId; + } + + public Monster getMonster() { + return mMonster; + } + + public void setMonster(@NonNull Monster monster) { + mMonster = monster; + mAbilities.setValue(mMonster.getAbilityDescriptions()); + mActions.setValue(mMonster.getActionDescriptions()); + mArmorClass.setValue(mMonster.getArmorClass()); + mChallenge.setValue(mMonster.getChallengeRatingDescription()); + mCharisma.setValue(monster.getCharismaDescription()); + mConditionImmunities.setValue(mMonster.getConditionImmunitiesDescription()); + mConstitution.setValue(monster.getConstitutionDescription()); + mDamageImmunities.setValue(mMonster.getDamageImmunitiesDescription()); + mDamageResistances.setValue(mMonster.getDamageResistancesDescription()); + mDamageVulnerabilities.setValue(mMonster.getDamageVulnerabilitiesDescription()); + mDexterity.setValue(monster.getDexterityDescription()); + mHitPoints.setValue(mMonster.getHitPoints()); + mIntelligence.setValue(monster.getIntelligenceDescription()); + mLairActions.setValue(mMonster.getLairActionDescriptions()); + mLanguages.setValue(mMonster.getLanguagesDescription()); + mLegendaryActions.setValue(mMonster.getLegendaryActionDescriptions()); + mMeta.setValue(mMonster.getMeta()); + mMonsterId.setValue(mMonster.id); + mName.setValue(mMonster.name); + mReactions.setValue(monster.getReactionDescriptions()); + mRegionalEffects.setValue(monster.getRegionalActionDescriptions()); + mSavingThrows.setValue(monster.getSavingThrowsDescription()); + mSenses.setValue(monster.getSensesDescription()); + mSkills.setValue(monster.getSkillsDescription()); + mSpeed.setValue(mMonster.getSpeedText()); + mStrength.setValue(monster.getStrengthDescription()); + mWisdom.setValue(monster.getWisdomDescription()); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchFragment.java new file mode 100644 index 0000000..12ab602 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchFragment.java @@ -0,0 +1,53 @@ +package com.majinnaibu.monstercards.ui.search; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.ui.shared.MCFragment; + +public class SearchFragment extends MCFragment { + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_search, container, false); + MonsterRepository repository = this.getMonsterRepository(); + SearchResultsRecyclerViewAdapter adapter = new SearchResultsRecyclerViewAdapter(repository, null); + final RecyclerView recyclerView = root.findViewById(R.id.monster_list); + assert recyclerView != null; + setupRecyclerView(recyclerView, adapter); + + final TextView textView = root.findViewById(R.id.search_query); + textView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void afterTextChanged(Editable editable) { + adapter.doSearch(textView.getText().toString()); + } + }); + + return root; + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView, @NonNull SearchResultsRecyclerViewAdapter adapter) { + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchResultsRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchResultsRecyclerViewAdapter.java new file mode 100644 index 0000000..93c546a --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/search/SearchResultsRecyclerViewAdapter.java @@ -0,0 +1,90 @@ +package com.majinnaibu.monstercards.ui.search; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.utils.Logger; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.Disposable; + +public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter { + private final MonsterRepository mRepository; + private final ItemCallback mOnClickHandler; + private String mSearchText; + private List mValues; + private Disposable mSubscriptionHandler; + + public SearchResultsRecyclerViewAdapter(MonsterRepository repository, + ItemCallback onClick) { + mRepository = repository; + mSearchText = ""; + mValues = new ArrayList<>(); + mOnClickHandler = onClick; + mSubscriptionHandler = null; + + doSearch(mSearchText); + } + + public void doSearch(String searchText) { + if (mSubscriptionHandler != null && !mSubscriptionHandler.isDisposed()) { + mSubscriptionHandler.dispose(); + } + mSearchText = searchText; + Flowable> foundMonsters = mRepository.searchMonsters(mSearchText); + mSubscriptionHandler = foundMonsters.subscribe(monsters -> { + mValues = monsters; + notifyDataSetChanged(); + }, + throwable -> Logger.logError("Error performing search", throwable)); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.monster_list_content, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { + Monster monster = mValues.get(position); + holder.mContentView.setText(monster.name); + holder.itemView.setTag(monster); + holder.itemView.setOnClickListener(view -> { + if (mOnClickHandler != null) { + mOnClickHandler.onItem(monster); + } + }); + } + + @Override + public int getItemCount() { + return mValues.size(); + } + + public interface ItemCallback { + void onItem(Monster monster); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + final TextView mContentView; + + ViewHolder(View view) { + super(view); + mContentView = view.findViewById(R.id.content); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/shared/ChangeTrackedViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/shared/ChangeTrackedViewModel.java new file mode 100644 index 0000000..85c2134 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/shared/ChangeTrackedViewModel.java @@ -0,0 +1,25 @@ +package com.majinnaibu.monstercards.ui.shared; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +public class ChangeTrackedViewModel extends ViewModel { + private final MutableLiveData mHasChanges; + + public ChangeTrackedViewModel() { + mHasChanges = new MutableLiveData<>(false); + } + + public boolean hasChanges() { + Boolean value = mHasChanges.getValue(); + return value != null && value; + } + + protected void makeDirty() { + mHasChanges.setValue(true); + } + + protected void makeClean() { + mHasChanges.setValue(false); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java new file mode 100644 index 0000000..45c3ec7 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java @@ -0,0 +1,35 @@ +package com.majinnaibu.monstercards.ui.shared; + +import android.app.Activity; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; + +import com.majinnaibu.monstercards.MonsterCardsApplication; +import com.majinnaibu.monstercards.data.MonsterRepository; + +public class MCFragment extends Fragment { + public MonsterCardsApplication getApplication() { + return (MonsterCardsApplication) requireActivity().getApplication(); + } + + protected MonsterRepository getMonsterRepository() { + return this.getApplication().getMonsterRepository(); + } + + public AppCompatActivity requireAppCompatActivity() { + return (AppCompatActivity) requireActivity(); + } + + public void setTitle(CharSequence title) { + Activity activity = requireActivity(); + if (activity instanceof AppCompatActivity) { + AppCompatActivity appCompatActivity = (AppCompatActivity) activity; + ActionBar supportActionBar = appCompatActivity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setTitle(title); + } + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/shared/SwipeToDeleteCallback.java b/app/src/main/java/com/majinnaibu/monstercards/ui/shared/SwipeToDeleteCallback.java new file mode 100644 index 0000000..4fd376c --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/shared/SwipeToDeleteCallback.java @@ -0,0 +1,98 @@ +package com.majinnaibu.monstercards.ui.shared; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.R; + +public class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback { + + private final Drawable icon; + private final ColorDrawable background; + private final Paint clearPaint; + private final OnSwipeCallback mOnDelete; + private final OnMoveCallback mOnMove; + private final Context mContext; + + public SwipeToDeleteCallback(@NonNull Context context, OnSwipeCallback onDelete, OnMoveCallback onMove) { + super(onMove == null ? 0 : ItemTouchHelper.UP | ItemTouchHelper.DOWN, onDelete == null ? 0 : ItemTouchHelper.LEFT); + mOnDelete = onDelete; + mOnMove = onMove; + mContext = context; + icon = ContextCompat.getDrawable(mContext, R.drawable.ic_delete_white_36); + background = new ColorDrawable(context.getResources().getColor(R.color.red)); + clearPaint = new Paint(); + clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + + @Override + public boolean onMove( + @NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target + ) { + if (mOnMove != null) { + int from = viewHolder.getAdapterPosition(); + int to = target.getAdapterPosition(); + return mOnMove.onMove(from, to); + } + return false; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + if (mOnDelete != null) { + int position = viewHolder.getAdapterPosition(); + mOnDelete.onSwipe(position, direction); + } + } + + @Override + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + View itemView = viewHolder.itemView; + int itemHeight = itemView.getBottom() - itemView.getTop(); + boolean isCancelled = dX == 0 && !isCurrentlyActive; + + if (isCancelled) { + c.drawRect(itemView.getRight() + dX, itemView.getTop(), itemView.getRight(), itemView.getBottom(), clearPaint); + return; + } + // Draw the red delete background + background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom()); + background.draw(c); + + // Calculate position of delete icon + int iconHeight = icon.getIntrinsicHeight(); + int iconWidth = icon.getIntrinsicWidth(); + int iconTop = itemView.getTop() + (itemHeight - iconHeight) / 2; + int iconMargin = (itemHeight - iconHeight) / 2; + int iconLeft = itemView.getRight() - iconMargin - iconWidth; + int iconRight = itemView.getRight() - iconMargin; + int iconBottom = iconTop + iconHeight; + + // Draw the icon + icon.setBounds(iconLeft, iconTop, iconRight, iconBottom); + icon.draw(c); + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + + public interface OnSwipeCallback { + void onSwipe(int position, int direction); + } + + public interface OnMoveCallback { + boolean onMove(int from, int to); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/utils/ChangeTrackedLiveData.java b/app/src/main/java/com/majinnaibu/monstercards/utils/ChangeTrackedLiveData.java new file mode 100644 index 0000000..01a2a0d --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/utils/ChangeTrackedLiveData.java @@ -0,0 +1,65 @@ +package com.majinnaibu.monstercards.utils; + +import androidx.lifecycle.MutableLiveData; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class ChangeTrackedLiveData extends MutableLiveData { + private final OnValueChangedCallback mOnValueChangedCallback; + private final OnValueDirtiedCallback mOnValueDirtiedCallback; + private T mReferenceValue; + + public ChangeTrackedLiveData(T initialValue, OnValueChangedCallback onValueChanged, OnValueDirtiedCallback onValueDirtied) { + super(initialValue); + mReferenceValue = initialValue; + mOnValueChangedCallback = onValueChanged; + if (mOnValueChangedCallback != null) { + mOnValueChangedCallback.onValueChanged(initialValue); + } + mOnValueDirtiedCallback = onValueDirtied; + } + + public ChangeTrackedLiveData(T initialValue, OnValueChangedCallback callback) { + this(initialValue, callback, null); + } + + public ChangeTrackedLiveData(T initialValue, OnValueDirtiedCallback callback) { + this(initialValue, null, callback); + } + + public void setReferenceValue(T referenceValue) { + mReferenceValue = referenceValue; + } + + public void setCurrentValueAsReference() { + mReferenceValue = getValue(); + } + + public void resetValue(T value) { + mReferenceValue = value; + setValue(value); + } + + @Override + public void setValue(T value) { + if (!Objects.equals(getValue(), value)) { + super.setValue(value); + + if (mOnValueChangedCallback != null) { + mOnValueChangedCallback.onValueChanged(value); + } + if (!Objects.equals(mReferenceValue, value) && mOnValueDirtiedCallback != null) { + mOnValueDirtiedCallback.onValueDirtied(); + } + } + } + + public interface OnValueDirtiedCallback { + void onValueDirtied(); + } + + public interface OnValueChangedCallback { + void onValueChanged(T value); + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/utils/Logger.java b/app/src/main/java/com/majinnaibu/monstercards/utils/Logger.java new file mode 100644 index 0000000..535b0d1 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/utils/Logger.java @@ -0,0 +1,139 @@ +package com.majinnaibu.monstercards.utils; + +import android.util.Log; + +@SuppressWarnings("unused") +public class Logger { + public static final String LOG_TAG = "MonsterCards"; + + public static void logUnimplementedMethod() { + Exception ex = new Exception(); + StackTraceElement[] stackTrace = ex.getStackTrace(); + + String location = stackTrace[1].getClassName() + "." + stackTrace[1].getMethodName() + ":" + stackTrace[1].getLineNumber(); + logDebug("Method not yet implemented " + location); + } + + public static void logUnhandledError(Throwable e) { + StackTraceElement stackTraceElement = e.getStackTrace()[0]; + + String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber(); + logDebug("Exception was caught but not properly handled " + location); + } + + public static void logUnimplementedFeature(String featureDescription) { + Exception ex = new Exception(); + StackTraceElement[] stackTrace = ex.getStackTrace(); + + String location = stackTrace[1].getClassName() + "." + stackTrace[1].getMethodName() + ":" + stackTrace[1].getLineNumber(); + logDebug("Feature not yet implemented " + featureDescription + " at " + location); + } + + //region WTF + public static void logWTF(String message) { + Log.wtf(LOG_TAG, message); + } + + public static void logWTF(Throwable throwable) { + StackTraceElement stackTraceElement = throwable.getStackTrace()[0]; + + String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber(); + String message = String.format("Unexpected error occurred at %s.", location); + Log.wtf(LOG_TAG, message, throwable); + } + + public static void logWTF(String message, Throwable throwable) { + Log.wtf(LOG_TAG, message, throwable); + } + //endregion + + //region Error + public static void logError(String message) { + Log.e(LOG_TAG, message); + } + + public static void logError(Throwable throwable) { + StackTraceElement stackTraceElement = throwable.getStackTrace()[0]; + + String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber(); + String message = String.format("Unexpected error occurred at %s.", location); + Log.e(LOG_TAG, message, throwable); + } + + public static void logError(String message, Throwable throwable) { + Log.e(LOG_TAG, message, throwable); + } + //endregion + + //region Warning + public static void logWarning(String message) { + Log.w(LOG_TAG, message); + } + + public static void logWarning(Throwable throwable) { + StackTraceElement stackTraceElement = throwable.getStackTrace()[0]; + + String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber(); + String message = String.format("Unexpected error occurred at %s.", location); + Log.w(LOG_TAG, message, throwable); + } + + public static void logWarning(String message, Throwable throwable) { + Log.w(LOG_TAG, message, throwable); + } + //endregion + + //region Info + public static void logInfo(String message) { + Log.i(LOG_TAG, message); + } + + public static void logInfo(Throwable throwable) { + StackTraceElement stackTraceElement = throwable.getStackTrace()[0]; + + String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber(); + String message = String.format("Unexpected error occurred at %s.", location); + Log.i(LOG_TAG, message, throwable); + } + + public static void logInfo(String message, Throwable throwable) { + Log.i(LOG_TAG, message, throwable); + } + //endregion + + //region Debug + public static void logDebug(String message) { + Log.d(LOG_TAG, message); + } + + public static void logDebug(Throwable throwable) { + StackTraceElement stackTraceElement = throwable.getStackTrace()[0]; + + String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber(); + String message = String.format("Unexpected error occurred at %s.", location); + Log.d(LOG_TAG, message, throwable); + } + + public static void logDebug(String message, Throwable throwable) { + Log.d(LOG_TAG, message, throwable); + } + //endregion + + //region Verbose + public static void logVerbose(String message) { + Log.v(LOG_TAG, message); + } + + public static void logVerbose(Throwable throwable) { + StackTraceElement stackTraceElement = throwable.getStackTrace()[0]; + + String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber(); + String message = String.format("Unexpected error occurred at %s.", location); + Log.v(LOG_TAG, message, throwable); + } + + public static void logVerbose(String message, Throwable throwable) { + Log.v(LOG_TAG, message, throwable); + } + //endregion +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/utils/TextChangedListener.java b/app/src/main/java/com/majinnaibu/monstercards/utils/TextChangedListener.java new file mode 100644 index 0000000..8034dd3 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/utils/TextChangedListener.java @@ -0,0 +1,75 @@ +package com.majinnaibu.monstercards.utils; + +import android.text.Editable; +import android.text.TextWatcher; + +@SuppressWarnings("unused") +public class TextChangedListener implements TextWatcher { + + private final BeforeTextChangedCallback mBeforeTextChangedCallback; + private final OnTextChangedCallback mOnTextChangedCallback; + private final AfterTextChangedCallback mAfterTextChangedCallback; + + public TextChangedListener(BeforeTextChangedCallback beforeTextChangedCallback, OnTextChangedCallback onTextChangedCallback, AfterTextChangedCallback afterTextChangedCallback) { + mBeforeTextChangedCallback = beforeTextChangedCallback; + mOnTextChangedCallback = onTextChangedCallback; + mAfterTextChangedCallback = afterTextChangedCallback; + } + + public TextChangedListener(OnTextChangedCallback callback) { + this(null, callback, null); + } + + public TextChangedListener(BeforeTextChangedCallback callback) { + this(callback, null, null); + } + + public TextChangedListener(AfterTextChangedCallback callback) { + this(null, null, callback); + } + + public TextChangedListener(BeforeTextChangedCallback beforeTextChangedCallback, OnTextChangedCallback onTextChangedCallback) { + this(beforeTextChangedCallback, onTextChangedCallback, null); + } + + public TextChangedListener(BeforeTextChangedCallback beforeTextChangedCallback, AfterTextChangedCallback afterTextChangedCallback) { + this(beforeTextChangedCallback, null, afterTextChangedCallback); + } + + public TextChangedListener(OnTextChangedCallback onTextChangedCallback, AfterTextChangedCallback afterTextChangedCallback) { + this(null, onTextChangedCallback, afterTextChangedCallback); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (mBeforeTextChangedCallback != null) { + mBeforeTextChangedCallback.beforeTextChanged(s, start, count, after); + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (mOnTextChangedCallback != null) { + mOnTextChangedCallback.onTextChanged(s, start, before, count); + } + } + + @Override + public void afterTextChanged(Editable s) { + if (mAfterTextChangedCallback != null) { + mAfterTextChangedCallback.afterTextChanged(s); + } + } + + public interface BeforeTextChangedCallback { + void beforeTextChanged(CharSequence s, int start, int count, int after); + } + + public interface OnTextChangedCallback { + void onTextChanged(CharSequence s, int start, int before, int count); + } + + public interface AfterTextChangedCallback { + void afterTextChanged(Editable s); + } +} diff --git a/app/src/main/res/color/radio_button_text.xml b/app/src/main/res/color/radio_button_text.xml new file mode 100644 index 0000000..2b573b3 --- /dev/null +++ b/app/src/main/res/color/radio_button_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_24.xml b/app/src/main/res/drawable/ic_add_24.xml new file mode 100644 index 0000000..eb23254 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_right_24.xml b/app/src/main/res/drawable/ic_chevron_right_24.xml new file mode 100644 index 0000000..fd2878a --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_collections_black_24dp.xml b/app/src/main/res/drawable/ic_collections_black_24dp.xml new file mode 100644 index 0000000..fcff54e --- /dev/null +++ b/app/src/main/res/drawable/ic_collections_black_24dp.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml new file mode 100644 index 0000000..46fc8de --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_white_36.xml b/app/src/main/res/drawable/ic_delete_white_36.xml new file mode 100644 index 0000000..282594c --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_white_36.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_24.xml b/app/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 0000000..2844baf --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_library_add_24.xml b/app/src/main/res/drawable/ic_library_add_24.xml new file mode 100644 index 0000000..1dba3d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_library_add_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_library_black_24dp.xml b/app/src/main/res/drawable/ic_library_black_24dp.xml new file mode 100644 index 0000000..5ca45e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_library_black_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_black_24dp.xml b/app/src/main/res/drawable/ic_search_black_24dp.xml new file mode 100644 index 0000000..07b76d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_black_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_section_divider.xml b/app/src/main/res/drawable/ic_section_divider.xml new file mode 100644 index 0000000..22a9637 --- /dev/null +++ b/app/src/main/res/drawable/ic_section_divider.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/radio_button_checked.xml b/app/src/main/res/drawable/radio_button_checked.xml new file mode 100644 index 0000000..d5548cd --- /dev/null +++ b/app/src/main/res/drawable/radio_button_checked.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/radio_button_selector.xml b/app/src/main/res/drawable/radio_button_selector.xml new file mode 100644 index 0000000..b9e7998 --- /dev/null +++ b/app/src/main/res/drawable/radio_button_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/radio_button_unchecked.xml b/app/src/main/res/drawable/radio_button_unchecked.xml new file mode 100644 index 0000000..dd01b1b --- /dev/null +++ b/app/src/main/res/drawable/radio_button_unchecked.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/rectangle_background.xml b/app/src/main/res/drawable/rectangle_background.xml new file mode 100644 index 0000000..a013c9a --- /dev/null +++ b/app/src/main/res/drawable/rectangle_background.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..eec5652 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_ability_score.xml b/app/src/main/res/layout/card_ability_score.xml new file mode 100644 index 0000000..7dbba97 --- /dev/null +++ b/app/src/main/res/layout/card_ability_score.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/card_action.xml b/app/src/main/res/layout/card_action.xml new file mode 100644 index 0000000..2fe0f4b --- /dev/null +++ b/app/src/main/res/layout/card_action.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/card_armor_class.xml b/app/src/main/res/layout/card_armor_class.xml new file mode 100644 index 0000000..b9baf69 --- /dev/null +++ b/app/src/main/res/layout/card_armor_class.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/layout/card_challenge_rating.xml b/app/src/main/res/layout/card_challenge_rating.xml new file mode 100644 index 0000000..a39efef --- /dev/null +++ b/app/src/main/res/layout/card_challenge_rating.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/layout/card_hit_points.xml b/app/src/main/res/layout/card_hit_points.xml new file mode 100644 index 0000000..c7cc63d --- /dev/null +++ b/app/src/main/res/layout/card_hit_points.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/layout/card_initiative.xml b/app/src/main/res/layout/card_initiative.xml new file mode 100644 index 0000000..fce9841 --- /dev/null +++ b/app/src/main/res/layout/card_initiative.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/layout/card_monster.xml b/app/src/main/res/layout/card_monster.xml new file mode 100644 index 0000000..3c5f7f4 --- /dev/null +++ b/app/src/main/res/layout/card_monster.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/card_monster_short.xml b/app/src/main/res/layout/card_monster_short.xml new file mode 100644 index 0000000..799f9aa --- /dev/null +++ b/app/src/main/res/layout/card_monster_short.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/component_ability_score_picker.xml b/app/src/main/res/layout/component_ability_score_picker.xml new file mode 100644 index 0000000..c47a653 --- /dev/null +++ b/app/src/main/res/layout/component_ability_score_picker.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/layout/component_advantage_picker.xml b/app/src/main/res/layout/component_advantage_picker.xml new file mode 100644 index 0000000..103a73a --- /dev/null +++ b/app/src/main/res/layout/component_advantage_picker.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/component_advantage_picker2.xml b/app/src/main/res/layout/component_advantage_picker2.xml new file mode 100644 index 0000000..1715eaa --- /dev/null +++ b/app/src/main/res/layout/component_advantage_picker2.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/component_proficiency_picker.xml b/app/src/main/res/layout/component_proficiency_picker.xml new file mode 100644 index 0000000..608f569 --- /dev/null +++ b/app/src/main/res/layout/component_proficiency_picker.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/component_stepper.xml b/app/src/main/res/layout/component_stepper.xml new file mode 100644 index 0000000..34aa568 --- /dev/null +++ b/app/src/main/res/layout/component_stepper.xml @@ -0,0 +1,77 @@ + + + + + + + + + + +