635 Commits

Author SHA1 Message Date
70b7a4e795 Makes DashboardFragment use its ViewModel 2025-07-06 20:12:45 -07:00
8b4d9b6b17 Fixes subscribing in the DashboardFragment. 2025-07-06 20:12:45 -07:00
6bad5a4ed5 Removes unneeded cast in LibraryFragment. 2025-07-06 20:12:44 -07:00
93bf987bb9 Extracts the ViewHolder for SimpleListItemBinding to SimpleListItemViewHolder<T>. 2025-07-06 20:12:44 -07:00
be433bf217 Makes the Library fragment use SimpleListItemBinding to build its ViewHolder. 2025-07-06 20:12:44 -07:00
1ae81b03b0 Makes search work. 2025-07-06 20:12:44 -07:00
4633a50bf4 Layout cleanup. 2025-07-06 20:12:43 -07:00
9ac73337af Replaces duplicated list item layouts with a single shared SimpleListItem. 2025-07-06 20:12:43 -07:00
25340cb7fd Adds generic ItemCallback interface. 2025-07-06 20:12:42 -07:00
be544f5304 Refactors Library to work like the other recycler views. 2025-07-06 20:12:41 -07:00
3dc39ad6a4 cleanup 2025-07-06 20:12:41 -07:00
92ea5b6a0d Moves AppDatabase initialization into the AppDatabase class. 2025-07-06 20:12:40 -07:00
a5eec5c4c8 Moves DiffCallback methods into Monster class. 2025-07-06 20:12:40 -07:00
e598b2984a Adds proguard config. 2025-07-06 20:12:39 -07:00
Tom Hicks
32ae8461b7 Adds functional dashboard based on recycler view that picks the number of columns based on screen width. 2025-07-06 20:12:39 -07:00
Tom Hicks
8686dbdaea Adds dashboard mockup. 2025-07-06 20:12:38 -07:00
Tom Hicks
a8df37325f Adds views for dashboard. 2025-07-06 20:12:38 -07:00
Tom Hicks
2743a8337b Makes radio buttons suck less.
Styles the advantage picker and proficiency picker.
Makes the saving throws screen nicer.
2025-07-06 20:12:37 -07:00
Tom Hicks
b4767676d2 Fixes lists to use similar sized items. 2025-07-06 20:12:37 -07:00
Tom Hicks
7c9037af84 Makes monster importing a fragment in the main activity.
Makes the edit action work when editing an imported monster.
2025-07-06 20:12:36 -07:00
Tom Hicks
c582ba5eaa Adds import monster activity. 2025-07-06 20:12:35 -07:00
Tom Hicks
8b2ab8f48f Adds OnMoveCallback to the SwipeToDeleteCallback class.
Makes traits orderable.
2025-07-06 20:12:34 -07:00
Tom Hicks
2de07c54cc View cleanup.
Makes more numeric fields Steppers.
Sets titles for fragments.
Sets focus on the first EditText on most edit pages.
Makes Checkbox be a SwitchCompat
Fixes monster list item view.
2025-07-06 20:12:33 -07:00
Tom Hicks
c50f79c273 Renames EditTraitListFragment to EditTraitsFragment to match the other list fragments. 2025-07-06 20:12:32 -07:00
Tom Hicks
921a02f953 Replaces condition immunities, damage immunities, damage resistances, damage vulnerabilities, and senses with a unified list of strings editor. 2025-07-06 20:12:30 -07:00
Tom Hicks
ec712842ca Adds editing support for other traits using some shared fragments. 2025-07-06 20:11:56 -07:00
Tom Hicks
2f9dec4d02 Adds ability editor. 2025-07-06 20:11:55 -07:00
Tom Hicks
cb1e3343c9 Adds ability to edit languages. 2025-07-06 20:11:54 -07:00
5bded410d2 ViewModel refactoring. 2025-07-06 20:11:53 -07:00
f6a032844c Fixes default values for array/set properties of monsters in db.
Removes individual senses properties and replaces with a Set<String>.
2025-07-06 20:11:52 -07:00
d214d615e5 Adds edit skill fragment to edit individual skills. 2025-07-06 20:11:51 -07:00
54f863ee5f Adds ability score picker. 2025-07-06 20:11:51 -07:00
d4d298fcc3 Disables annoying lint rule. 2025-07-06 20:11:50 -07:00
7f31d98d7e Adds "New Skill" button. 2025-07-06 20:11:49 -07:00
68348b18c2 Adds edit skills view. 2025-07-06 20:11:49 -07:00
42a2994f2c Adds edit challenge rating screen. 2025-07-06 20:11:49 -07:00
218f39f6c2 Adds better change tracking to know if a monster needs to be saved. 2025-07-06 20:11:48 -07:00
2512bd8d75 Adds saving throws editor. 2025-07-06 20:11:47 -07:00
370d416a23 Adds AdvantagePicker and ProficiencyPicker components. 2025-07-06 20:11:47 -07:00
60d139078b Adds a Stepper control and uses it for the steppers in the editor. 2025-07-06 20:11:46 -07:00
611fa6c323 Adds screen to edit ability scores. 2025-07-06 20:11:45 -07:00
ab5a3c7c67 Adds Edit Speed screen to the monster editor. 2025-07-06 20:11:44 -07:00
b889857e80 Adds Edit Armor screen to edit a monster's armor stats. 2025-07-06 20:11:44 -07:00
da0e072a45 Adds confirmation when going up from the edit monster screen to save, cancel, or discard changes. 2025-07-06 20:11:43 -07:00
e075fc4369 Adds edit basic info screen with most string fields.
Cleans up fonts/margins on edit screens.
Makes the EditMonsterViewModel shared between edit monster fragments.
2025-07-06 20:11:43 -07:00
0db46ebb51 Adds TextChangedListener helper class to make working with text inputs less verbose. 2025-07-06 20:11:42 -07:00
20318b0ad5 Adds top level items to the edit monster view. 2025-07-06 20:11:41 -07:00
6debb1eb27 Replaces EditMonsterFragment with a basic ConstraintLayout. 2025-07-06 20:11:41 -07:00
87c845bd0d Adds Edit Monster placeholder fragment. 2025-07-06 20:11:40 -07:00
f507f9d7cd Adds edit button to monster detail view. 2025-07-06 20:11:40 -07:00
8bb1f64e9f Makes the swipe to delete callback more generic. 2025-07-06 20:11:39 -07:00
f2d0e93911 Fixes a bunch of lint errors and enables Flipper navigation logging. 2025-07-06 20:11:38 -07:00
c9a7e028ae Renames MonsterFragment to MonsterDetailFragment to better explain its use. 2025-07-06 20:11:38 -07:00
807871fe5c Make AppCenter only included in debug builds.
Moves Flipper initialization to the same place as AppCenter.
2025-07-06 20:11:37 -07:00
acadf2170c Adds functional search using sqlite full text search syntax. 2025-07-06 20:11:36 -07:00
8215d2021c Adds DevContent class with dev resources. Specifically an example monster.
Adds a task.
2025-07-06 20:11:35 -07:00
0cbf6022c4 Adds swipe to delete monsters on the library screen. 2025-07-06 20:11:35 -07:00
eec695bfc8 Adds MonsterRepository to manage access to the RoomDB store. 2025-07-06 20:11:34 -07:00
8363912e53 Adds logger. 2025-07-06 20:11:33 -07:00
67db8d79d0 Adds application class. 2025-07-06 20:11:32 -07:00
b5834f3db2 Migrates Monster class to be storable in roomdb. 2025-07-06 20:11:25 -07:00
ca6a319bd9 Creates initial app database class and adds minimal DTO support for monsters. 2025-07-06 20:08:58 -07:00
95ba20b5c6 Adds RoomDB and rxjava3 dependencies. 2025-07-06 20:08:57 -07:00
ea65692b38 Adds abilities to monster cards.
Adds CommonMark dependency and CommonMarkHelper to render it to html.
2025-07-06 20:08:56 -07:00
d7cf01e30d Adds languages to monster cards. 2025-07-06 20:08:56 -07:00
30c6dc7ee5 Adds skills to monster cards. 2025-07-06 20:08:55 -07:00
f13be2c1ac Adds armor class and section divider to monster cards. 2025-07-06 20:08:54 -07:00
920344b5fd Adds monster meta (size, type, subtype/tag, and alignment) to monster cards. 2025-07-06 20:08:54 -07:00
7b3d6003d4 Adds monster name to monster cards. 2025-07-06 20:08:54 -07:00
c837c19b87 Adds Monster fragment to view a monster's card.
Adds query box and search button to search fragment.
Makes the search button show a monster card.
2025-07-06 20:08:53 -07:00
b9759f6364 Adds top level navigation and placeholder fragments. 2025-07-06 20:08:52 -07:00
f24f1d978c Adds debug and release directories to git ignore list. 2025-07-06 20:08:51 -07:00
2defd1fca1 Adds INTERNET permission to manifest. 2025-07-06 20:08:51 -07:00
07f81a5f6d Initial 2025-07-06 20:08:51 -07:00
985c2fb730 Updates Android Gradle Plugin to 7.0.1. 2025-07-06 20:05:13 -07:00
0dc96a8c45 Removes unneeded logging. 2025-07-06 20:05:13 -07:00
10bca503e5 Adds support for virtual file urls. 2025-07-06 20:05:13 -07:00
1d335e9a37 Updates to build on arm macs. 2025-07-06 20:05:13 -07:00
fbf119fb8a Updates manifest to clear warnings and errors for targeting os 12. 2025-07-06 20:05:13 -07:00
3d6adaad2c Updates dependencies to latest versions. 2025-07-06 20:05:13 -07:00
c3a972571a Upgrades Android Gradle plugin to 7.0.0. 2025-07-06 20:05:13 -07:00
4e45a547f4 Moves gradle dependency versions out of variables. 2025-07-06 20:05:13 -07:00
792628d4a4 Adds proguard config. 2025-07-06 20:05:13 -07:00
b6b669a0db Disables release build minification and obfuscation. 2025-07-06 20:05:12 -07:00
540a0474da Disables minification of release builds. 2025-07-06 20:05:12 -07:00
49734d5eef Fixed release flipper stub.
Updates dependency versions.
2025-07-06 20:05:12 -07:00
b2c3728e9a Upgrades android gradle plugin to 4.2.2. 2025-07-06 20:05:12 -07:00
39cab7f799 Removes dead code and cleans up annotations Nullable/NonNull annotations. 2025-07-06 20:05:12 -07:00
Tom Hicks
1e007a3553 Makes dashboard views use dimension and string resources. 2025-07-06 20:05:12 -07:00
Tom Hicks
1a487f950d Adds functional dashboard based on recycler view that picks the number of columns based on screen width. 2025-07-06 20:05:12 -07:00
Tom Hicks
ea13e38402 Adds dashboard mockup. 2025-07-06 20:05:12 -07:00
Tom Hicks
8e2372085d Adds views for dashboard. 2025-07-06 20:05:10 -07:00
Tom Hicks
129d910126 Makes radio buttons suck less.
Styles the advantage picker and proficiency picker.
Makes the saving throws screen nicer.
2025-07-06 20:05:09 -07:00
Tom Hicks
1a23e5e35a Fixes missing descriptions on imported traits. 2025-07-06 20:05:09 -07:00
Tom Hicks
660cf633da Fixes lists to use similar sized items. 2025-07-06 20:05:09 -07:00
Tom Hicks
1e7a7c68aa Fixes screen titles. 2025-07-06 20:05:08 -07:00
Tom Hicks
070fda0989 Makes monster importing a fragment in the main activity.
Makes the edit action work when editing an imported monster.
2025-07-06 20:05:08 -07:00
Tom Hicks
af05c41b75 Adds import monster activity. 2025-07-06 20:05:08 -07:00
Tom Hicks
efa4c2a299 Refactors monster helper methods into a separate class. 2025-07-06 20:05:07 -07:00
Tom Hicks
2f5918b7a2 Adds other traits to the monster detail fragment.
Cleans up the monster detail fragment to work like the other fragments.
2025-07-06 20:05:07 -07:00
Tom Hicks
cb6f7122ed Refactors monster detail to use a view holder and partially corrects Regional Actions to Regional Effects. 2025-07-06 20:05:07 -07:00
Tom Hicks
7cfc6d4f65 Removes unused methods from EditMonsterViewModel. 2025-07-06 20:05:07 -07:00
Tom Hicks
cdae6a8b39 Adds OnMoveCallback to the SwipeToDeleteCallback class.
Makes traits orderable.
2025-07-06 20:05:07 -07:00
Tom Hicks
9d46d1420e Removes todo. 2025-07-06 20:05:07 -07:00
Tom Hicks
6d43b0635c View cleanup.
Makes more numeric fields Steppers.
Sets titles for fragments.
Sets focus on the first EditText on most edit pages.
Makes Checkbox be a SwitchCompat
Fixes monster list item view.
2025-07-06 20:05:07 -07:00
Tom Hicks
4c138ee499 Fix Stepper initial values. 2025-07-06 20:05:07 -07:00
Tom Hicks
0fcfa7e782 Adds ScrollViews to the editors that were missing them. 2025-07-06 20:05:07 -07:00
Tom Hicks
791cf4164c Fixes search.
It now works the same as iOS. It case insensitive matches the entire search text in any of name, size, type, subtype, or alignment.
2025-07-06 20:05:07 -07:00
Tom Hicks
fb12deaa3e Fixes removing languages. 2025-07-06 20:05:07 -07:00
Tom Hicks
6e597462ef Adds some TODOs. 2025-07-06 20:05:07 -07:00
Tom Hicks
12ffc5b15f Fixes the size of the edit languages header. 2025-07-06 20:05:07 -07:00
Tom Hicks
9c81bd4905 Fixes selection bug in AbilityScorePicker. 2025-07-06 20:05:07 -07:00
Tom Hicks
f1cbc60857 Renames EditTraitListFragment to EditTraitsFragment to match the other list fragments. 2025-07-06 20:05:07 -07:00
Tom Hicks
a2798ddc82 Replaces condition immunities, damage immunities, damage resistances, damage vulnerabilities, and senses with a unified list of strings editor. 2025-07-06 20:05:06 -07:00
Tom Hicks
ecf2b01723 Auto formatted all files. 2025-07-06 20:05:05 -07:00
Tom Hicks
18d6f2a31e Adds editing support for other traits using some shared fragments. 2025-07-06 20:05:05 -07:00
Tom Hicks
7cbcf8d07c Adds ability editor. 2025-07-06 20:05:04 -07:00
Tom Hicks
dc487d238a Fixes name of "Add Damage Type" string. 2025-07-06 20:05:03 -07:00
Tom Hicks
b8c702f665 Code style cleanup.
Replaces Fragment with MCFragment.
2025-07-06 20:05:03 -07:00
Tom Hicks
259b59f519 Adds equals override to Trait objects.
Fixes trait comparisons.
2025-07-06 20:05:03 -07:00
Tom Hicks
5289bac908 Adds ability to edit languages. 2025-07-06 20:05:03 -07:00
Tom Hicks
34e11d97e5 Makes use of class compareTo methods when sorting skills and languages.
Fixes dirty state of skills when modifying them.
2025-07-06 20:05:03 -07:00
Tom Hicks
ca6684a093 Replaces manual view != null assertions with requireView. 2025-07-06 20:05:03 -07:00
Tom Hicks
6e48a6f455 Adds equals override for Language objects.
Makes languages sort group by can speaks.
2025-07-06 20:05:03 -07:00
b8af70406f Add damage resistances and damage vulnerabilities to the monster editor. 2025-07-06 20:05:03 -07:00
7eae6f820e Adds damage resistances to monster editor. 2025-07-06 20:05:03 -07:00
2076d53b11 ViewModel refactoring. 2025-07-06 20:05:03 -07:00
e20602cc3d Adds editing of condition immunities. 2025-07-06 20:05:03 -07:00
04dc066191 Adds getters and setters to EditMonsterViewModel for Languages, Abilities, Actions, Reactions, Lair Actions, Legendary Actions, and Regional Actions. 2025-07-06 20:05:03 -07:00
72b3df429f Adds getters and setters for string array properties of EditMonsterViewModel. 2025-07-06 20:05:03 -07:00
8d94afeb55 Adds edit sense fragment. 2025-07-06 20:05:03 -07:00
8bae59ed29 Edit skill cleanup. 2025-07-06 20:05:03 -07:00
c401b7919e Adds Edit Senses fragment. 2025-07-06 20:05:03 -07:00
9983ba10cb Removes unused code from edit skills. 2025-07-06 20:05:03 -07:00
171bc7436e Replaces usages of Fragment with MCFragment. 2025-07-06 20:05:03 -07:00
04a30aa766 Adds senses to EditMonsterViewModel. 2025-07-06 20:05:03 -07:00
2356726e3f Fixes default values for array/set properties of monsters in db.
Removes individual senses properties and replaces with a Set<String>.
2025-07-06 20:05:03 -07:00
6151ad889c Updates TODO in EditMonsterViewModel. 2025-07-06 20:05:03 -07:00
1f15d73573 Adds edit skill fragment to edit individual skills. 2025-07-06 20:05:03 -07:00
40589f171d Adds stubbed out method to navigate to the edit skill fragment.
Adds a click handler to the edit skills recycler view adapter.
Makes tapping a skill in the edit skills view navigate to the editor for that skill.
Makes adding a new skill immediately edit the skill.
2025-07-06 20:05:02 -07:00
a58c851240 Makes addNewSkill return the new skill.
Adds replaceSkill to replace an existing skill in the skills array.
2025-07-06 20:05:02 -07:00
2c6b514538 Adds an equals method to skills. 2025-07-06 20:05:02 -07:00
f6a8b83343 Makes AdvantagePicker work if you set the id of the advantage picker to advantage. 2025-07-06 20:05:02 -07:00
eb3fa5108f Adds ability score picker. 2025-07-06 20:05:02 -07:00
922db42322 Lint cleanup. 2025-07-06 20:05:01 -07:00
71177b92f9 Removes debug logging. 2025-07-06 20:05:01 -07:00
34e68443ae Disables annoying lint rule. 2025-07-06 20:05:01 -07:00
857733ec9c Adds ability to remove a skill. 2025-07-06 20:05:00 -07:00
7e78ad8b7d Keeps skills on the edit skills screen sorted. 2025-07-06 20:05:00 -07:00
430fa61be1 Fixes bug where adding a new skill wouldn't show up until leaving and returning to the edit skills screen. 2025-07-06 20:05:00 -07:00
70e05cbb21 Adds item dividers to the Library and Edit Skills screens.
Makes the two layouts consistent with each other.
2025-07-06 20:05:00 -07:00
b67073622f Makes Add Skill button a FloatingActionButton. 2025-07-06 20:05:00 -07:00
c90579903d Adds "New Skill" button. 2025-07-06 20:05:00 -07:00
1a02eab07a Adds edit skills view. 2025-07-06 20:05:00 -07:00
00463c8092 Adds edit challenge rating screen. 2025-07-06 20:05:00 -07:00
153c49fe7b Adds remaining monster properties to the edit monster view model. 2025-07-06 20:04:59 -07:00
8178ec6fd7 Adds better change tracking to know if a monster needs to be saved. 2025-07-06 20:04:59 -07:00
2e7e40554d Adds saving throws editor. 2025-07-06 20:04:58 -07:00
989440de83 Adds AdvantagePicker and ProficiencyPicker components. 2025-07-06 20:04:57 -07:00
3c4adacc17 Adds a Stepper control and uses it for the steppers in the editor. 2025-07-06 20:04:57 -07:00
c28e1cb8c5 Adds screen to edit ability scores. 2025-07-06 20:04:56 -07:00
48dab535e9 Adds missed change tracking to EditMonsterViewModel. 2025-07-06 20:04:55 -07:00
ff26cb64b7 Adds Edit Speed screen to the monster editor. 2025-07-06 20:04:55 -07:00
28f0787020 Adds Edit Armor screen to edit a monster's armor stats. 2025-07-06 20:04:55 -07:00
71da064423 Adds hit dice and has custom HP toggle to the edit basic info screen. 2025-07-06 20:04:54 -07:00
23bcdc237d Adds confirmation when going up from the edit monster screen to save, cancel, or discard changes. 2025-07-06 20:04:54 -07:00
b56a662c9e Adds setTitle helper method to MCFragment. 2025-07-06 20:04:53 -07:00
dcce64f91a Adds edit basic info screen with most string fields.
Cleans up fonts/margins on edit screens.
Makes the EditMonsterViewModel shared between edit monster fragments.
2025-07-06 20:04:53 -07:00
595ee0c6fb Adds TextChangedListener helper class to make working with text inputs less verbose. 2025-07-06 20:04:52 -07:00
d52102d430 Makes EditMonsterFragment load the monster based with the id passed in and show it's name in the title. 2025-07-06 20:04:52 -07:00
6a4abdd547 Fixes passing the monster id when navigating to the edit monster screen. 2025-07-06 20:04:52 -07:00
bd3741af2d Extracts library strings to resources. 2025-07-06 20:04:52 -07:00
e254adfdce Changes to code format settings. 2025-07-06 20:04:52 -07:00
9c973ef348 Adds helper method to MCFragment to get the AppCompatActivity for the fragment. 2025-07-06 20:04:52 -07:00
dc9a0827d4 Adds edit monster view holder and placeholder for basic info tap handler. 2025-07-06 20:04:52 -07:00
b27274928e Adds top level items to the edit monster view. 2025-07-06 20:04:52 -07:00
6bb1e419c8 Replaces EditMonsterFragment with a basic ConstraintLayout. 2025-07-06 20:04:52 -07:00
3cda90eedd Adds shared preferences flipper plugin. 2025-07-06 20:04:51 -07:00
98a7dc5eeb Migrates from jcenter to maven central because of the jcenter shutdown. 2025-07-06 20:04:51 -07:00
e384e29570 Upgrades Android Gradle Plugin to 4.2.1 and Gradle to 6.7.1 2025-07-06 20:04:51 -07:00
7aa6419ece Adds Edit Monster placeholder fragment. 2025-07-06 20:04:51 -07:00
6b953e320d Adds edit button to monster detail view. 2025-07-06 20:04:50 -07:00
9f56f0283a Fixes the width of items in the library view. 2025-07-06 20:04:50 -07:00
b5f92afae9 Project file updates. 2025-07-06 20:04:50 -07:00
f58243ef6b Library refactoring. 2025-07-06 20:04:50 -07:00
886778ee78 Makes the swipe to delete callback more generic. 2025-07-06 20:04:50 -07:00
0a85324734 Fixes a bunch of lint errors and enables Flipper navigation logging. 2025-07-06 20:04:49 -07:00
b374dbfe71 Replaces fragment tag with FragmentContainerView. 2025-07-06 20:04:48 -07:00
21af6e20ba Renames MonsterFragment to MonsterDetailFragment to better explain its use. 2025-07-06 20:04:48 -07:00
dc9066daca Make AppCenter only included in debug builds.
Moves Flipper initialization to the same place as AppCenter.
2025-07-06 20:04:47 -07:00
e8e19d5371 Removes release build dependencies on Flipper and SoLoader. 2025-07-06 20:04:47 -07:00
c5242b5206 ViewModel cleanup
Removes unused view models.
Fixes warnings int MonsterViewModel.
2025-07-06 20:04:47 -07:00
ac2e37e494 Cleans up compiler warnings. 2025-07-06 20:04:47 -07:00
c5d857435d Removes unused type converter. 2025-07-06 20:04:47 -07:00
a1fab9d399 Adds functional search using sqlite full text search syntax. 2025-07-06 20:04:46 -07:00
e02e4ec399 Adds Flipper support to debug DB issues. 2025-07-06 20:04:46 -07:00
8706240fb4 Adds DevContent class with dev resources. Specifically an example monster.
Adds a task.
2025-07-06 20:04:46 -07:00
e17c492baf Adds swipe to delete monsters on the library screen. 2025-07-06 20:04:45 -07:00
2df11701e6 Adds floating action button to the library screen to create new monsters.
Adds a snackbar on successful muonster creation to view the new monster.
2025-07-06 20:04:44 -07:00
c6c0e4f758 Makes clicking a monster on the library screen show that monster in the detail screen. 2025-07-06 20:04:44 -07:00
8c233a3bc7 Adds list of monsters to Library screen. 2025-07-06 20:04:44 -07:00
e98b72ad7d Adds MCFragment and makes it the base fragment class. 2025-07-06 20:04:44 -07:00
1fb8dc3a86 Adds MonsterRepository to manage access to the RoomDB store. 2025-07-06 20:04:44 -07:00
8b52b0c3e5 Removes dev navigation on search button clicks. 2025-07-06 20:04:43 -07:00
ee065d7b39 Fixes back button in detail views. 2025-07-06 20:04:43 -07:00
59b319c27d Adds default values to Monster constructor. 2025-07-06 20:04:43 -07:00
cdcb7a60d4 Adds logger. 2025-07-06 20:04:43 -07:00
79106ec9f3 Adds application class. 2025-07-06 20:04:42 -07:00
bb4cbbb98b Upgrades android gradle plugin. 2025-07-06 20:04:41 -07:00
84b0fee261 Migrates Monster class to be storable in roomdb. 2025-07-06 20:04:41 -07:00
0c3ab6dc39 Creates initial app database class and adds minimal DTO support for monsters. 2025-07-06 20:04:37 -07:00
a694205c74 Fixes monster hp calculation so the first hit die is averaged like the others. Characters rolled as players will need to use custom HP. 2025-07-06 20:04:36 -07:00
793987c3fb Adds RoomDB and rxjava3 dependencies. 2025-07-06 20:04:36 -07:00
7d5f9c89a9 Updates build sdk. 2025-07-06 20:04:22 -07:00
6da0bfe70a Code reformat 2025-07-06 20:04:22 -07:00
a44893bca8 Adds actions to monster cards. 2025-07-06 20:04:22 -07:00
67375292a5 Adds abilities to monster cards.
Adds CommonMark dependency and CommonMarkHelper to render it to html.
2025-07-06 20:04:22 -07:00
145c827417 Adds challenge rating to monster cards. 2025-07-06 20:01:01 -07:00
8ff1cb8779 Adds languages to monster cards. 2025-07-06 20:00:59 -07:00
706b58fd2c Adds senses to monster cards. 2025-07-06 20:00:58 -07:00
15973a79f0 Adds condition immunities to monster cards. 2025-07-06 20:00:58 -07:00
94edc44044 Adds damage types to monster cards.
Adds oxfordJoin to StringHelper.
2025-07-06 20:00:58 -07:00
5396b7b014 Adds skills to monster cards. 2025-07-06 20:00:57 -07:00
e0cc8560d1 Adds saving throws to monster card. 2025-07-06 20:00:55 -07:00
c627bb0873 Adds ability scores to monster card. 2025-07-06 20:00:55 -07:00
6d8ec92012 Adds speeds to monster card. 2025-07-06 20:00:55 -07:00
212358e41d Adds hit points to monster card. 2025-07-06 20:00:55 -07:00
5fca394f0e Adds very basic readme. 2025-07-06 20:00:55 -07:00
6c914fb947 Adds armor class and section divider to monster cards. 2025-07-06 20:00:55 -07:00
5a283b8dae Adds monster meta (size, type, subtype/tag, and alignment) to monster cards. 2025-07-06 20:00:54 -07:00
407987e410 Adds monster name to monster cards. 2025-07-06 20:00:50 -07:00
27a1dd7580 Fixes build.gradle to generate safeargs directions for navigation. 2025-07-06 20:00:24 -07:00
775fbf3d9b Fixes deprecation warnings about ViewModelProviders.of(...). 2025-07-06 20:00:24 -07:00
2a75de4bce Adds Monster fragment to view a monster's card.
Adds query box and search button to search fragment.
Makes the search button show a monster card.
2025-07-06 20:00:23 -07:00
72502b3d03 Enables obfuscation and minification for release builds. 2025-07-06 20:00:18 -07:00
2b8a178c05 Adds top level navigation and placeholder fragments. 2025-07-06 20:00:16 -07:00
ab306289bd Sets better color scheme. 2025-07-06 19:59:12 -07:00
74b0c6695f Adds AppCenter config. 2025-07-06 19:58:55 -07:00
44d90ff5ea Adds INTERNET permission to manifest. 2025-07-06 19:58:55 -07:00
5113283550 Initial 2025-07-06 19:58:28 -07:00
Tom Hicks
2a963cea0a Cleans up monorepo documentation. 2025-07-06 19:25:13 -07:00
Tom Hicks
ab81917eb2 Merge remote-tracking branch 'ios-filtered/main' 2025-06-30 12:57:07 -07:00
Tom Hicks
48f71d1a3a Add 'iOS/' from commit '938f0fb75860d3637b998bdd0c27dcffd9fc9451'
git-subtree-dir: iOS
git-subtree-mainline: c4bb775af4
git-subtree-split: 938f0fb758
2025-06-30 12:55:22 -07:00
Tom Hicks
c4bb775af4 Merge commit 'd1e3c3f5f313057e5a81a4333906ef5d79adea83' as 'Android' 2025-06-30 12:23:51 -07:00
Tom Hicks
d1e3c3f5f3 Squashed 'Android/' content from commit 7a63a11
git-subtree-dir: Android
git-subtree-split: 7a63a11e93
2025-06-30 12:23:51 -07:00
addda6f54f Initial commit. 2024-07-29 22:27:56 -07:00
Tom Hicks
019c2fea60 Fixes opening files from outside the app's sandbox. 2021-06-27 13:51:03 -07:00
Tom Hicks
938f0fb758 Fixes opening files from outside the app's sandbox. 2021-06-27 13:51:03 -07:00
Maj
2da820b980 Adds .zshrc files to gitignore. 2021-05-22 00:39:56 -07:00
Maj
43004a3a0a Adds .zshrc files to gitignore. 2021-05-22 00:39:56 -07:00
Tom Hicks
1ebf6e5348 Fixes numbers and skills when importing. 2021-05-11 23:48:42 -07:00
Tom Hicks
4396336cae Fixes numbers and skills when importing. 2021-05-11 23:48:42 -07:00
Tom Hicks
dc543660be Adds footer links to privacy page and terms & conditions page. Adds header build status badges. 2021-04-15 22:29:03 -07:00
Tom Hicks
833131c123 Adds footer links to privacy page and terms & conditions page. Adds header build status badges. 2021-04-15 22:29:03 -07:00
Tom Hicks
f665bf86f1 Merge branch 'develop' of github.com:headhunter45/MonsterCards-iOS into develop 2021-04-15 21:46:17 -07:00
Tom Hicks
85e61dbb14 Merge branch 'develop' of github.com:headhunter45/MonsterCards-iOS into develop 2021-04-15 21:46:17 -07:00
Tom Hicks
a57c43a7e4 Updates docs and readme to include a description of the app features. 2021-04-15 21:45:52 -07:00
Tom Hicks
0fd2cb4e9a Updates docs and readme to include a description of the app features. 2021-04-15 21:45:52 -07:00
e8358bfb59 Maybe fixed signing for the preview extension. 2021-04-10 23:09:24 -07:00
163cbe7bb3 Maybe fixed signing for the preview extension. 2021-04-10 23:09:24 -07:00
750bb2543d Sets version label to 0.6. 2021-04-10 21:34:54 -07:00
c7f4b4c137 Sets version label to 0.6. 2021-04-10 21:34:54 -07:00
8558cacc83 Adds CloudKit support to the monster library between devices. 2021-04-08 21:58:54 -07:00
739dea8db7 Adds CloudKit support to the monster library between devices. 2021-04-08 21:58:54 -07:00
563df6ca28 Adds QuickLook preview for monster files. It shows how the monster will appear after being imported. 2021-04-08 19:18:27 -07:00
ebf01e2bb0 Adds QuickLook preview for monster files. It shows how the monster will appear after being imported. 2021-04-08 19:18:27 -07:00
5ba59bbdf3 Moves MonsterDetailWrapper into its own file to separate the core data dependencies. 2021-04-08 19:18:27 -07:00
346e3c1957 Moves MonsterDetailWrapper into its own file to separate the core data dependencies. 2021-04-08 19:18:27 -07:00
5304c66b0b Model cleanup.
Separates core data transport stuff to extensions so we can use the view models without a core data dependency.
2021-04-08 19:18:27 -07:00
d3a1878656 Model cleanup.
Separates core data transport stuff to extensions so we can use the view models without a core data dependency.
2021-04-08 19:18:27 -07:00
e741d4fb33 Makes DTOs implement Codable instead of just Encodable and Decodable.
Adds MonsterDocument to load/save .monster files.
2021-04-08 19:18:27 -07:00
9ff561dd9d Makes DTOs implement Codable instead of just Encodable and Decodable.
Adds MonsterDocument to load/save .monster files.
2021-04-08 19:18:27 -07:00
cc963f547f Enables document browser support and loading files in place so we can open monster files for import. 2021-04-08 19:18:27 -07:00
090e6d5263 Enables document browser support and loading files in place so we can open monster files for import. 2021-04-08 19:18:27 -07:00
f04932899a Adds some import tasks. 2021-04-05 01:01:41 -07:00
f31e89aacb Adds some import tasks. 2021-04-05 01:01:41 -07:00
52f37b494e Adds the ability to delete a monster from your library. 2021-04-05 00:46:44 -07:00
d212808ee1 Adds the ability to delete a monster from your library. 2021-04-05 00:46:44 -07:00
c7fdb7ecc5 Adds importing a character from a share. You can now save the monster as previewed. 2021-04-05 00:33:11 -07:00
0f187fdbe2 Adds importing a character from a share. You can now save the monster as previewed. 2021-04-05 00:33:11 -07:00
e7ccc0e1ab Adds reactions, lair actions, and regional actions to the editor and monster display. 2021-04-04 17:48:06 -07:00
8006ee4b66 Adds reactions, lair actions, and regional actions to the editor and monster display. 2021-04-04 17:48:06 -07:00
0ac780c188 Merge pull request #1 from headhunter45/add-file-import
Add Opening monster files from tetra cube's generator
2021-04-04 03:31:19 -07:00
0d77e79b0f Merge pull request #1 from headhunter45/add-file-import
Add Opening monster files from tetra cube's generator
2021-04-04 03:31:19 -07:00
ab218fbe34 Reorganizes the project tree and adds a task. 2021-04-04 03:28:20 -07:00
ef05ac1c5a Reorganizes the project tree and adds a task. 2021-04-04 03:28:20 -07:00
a43d2f11ea Adds monster import helper. 2021-04-04 03:27:50 -07:00
e11bf1455d Adds monster import helper. 2021-04-04 03:27:50 -07:00
b519b80209 Adds lair actions, regional actions, and reactions to the data model. Renames baseSpeed to walkSpeed. 2021-04-04 03:27:00 -07:00
885b0e08f7 Adds lair actions, regional actions, and reactions to the data model. Renames baseSpeed to walkSpeed. 2021-04-04 03:27:00 -07:00
f9647eaf97 Fixes toggling a shield in the editor not affecting the displayed AC. 2021-04-04 03:13:19 -07:00
c55b6e8f0d Fixes toggling a shield in the editor not affecting the displayed AC. 2021-04-04 03:13:19 -07:00
2e6b8bb377 Loads the imported monster from file and sets the new monster's name from the loaded data. 2021-04-04 03:11:34 -07:00
96a967f0d9 Loads the imported monster from file and sets the new monster's name from the loaded data. 2021-04-04 03:11:34 -07:00
c09c332758 Adds DTO classes to help load a monster file from https://tetra-cube.com/dnd/dnd-statblock.html 2021-04-04 01:41:41 -07:00
6a5c7b4384 Adds DTO classes to help load a monster file from https://tetra-cube.com/dnd/dnd-statblock.html 2021-04-04 01:41:41 -07:00
cd2be8490f Adds a view to show the monster being imported and confirm the user wants to import that monster. 2021-04-04 01:38:53 -07:00
1b0f2ee0df Adds a view to show the monster being imported and confirm the user wants to import that monster. 2021-04-04 01:38:53 -07:00
6eca7efb0c Adds an import view that shows when the app is opening a .monster file. 2021-04-04 01:18:00 -07:00
f8b6c1c45c Adds an import view that shows when the app is opening a .monster file. 2021-04-04 01:18:00 -07:00
9076f5896d Creates new document type for .monster files.
Sets the app as the default editor for .monster files.
2021-04-04 01:12:10 -07:00
7022739c55 Creates new document type for .monster files.
Sets the app as the default editor for .monster files.
2021-04-04 01:12:10 -07:00
94a3ceb9e0 Removes a bunch of commented out code. 2021-04-04 01:04:40 -07:00
072feb5c52 Removes a bunch of commented out code. 2021-04-04 01:04:40 -07:00
46682e1de7 Adds the monster name to MonsterDetail. 2021-04-04 00:31:18 -07:00
a7c068fd45 Adds the monster name to MonsterDetail. 2021-04-04 00:31:18 -07:00
68a6051dca Makes MonsterDetail use a MonsterViewModel instead of the core data type Monster. 2021-04-04 00:30:55 -07:00
f3544a581b Makes MonsterDetail use a MonsterViewModel instead of the core data type Monster. 2021-04-04 00:30:55 -07:00
568485a62e Adds calculated fields from Monster to MonsterViewModel. 2021-04-04 00:19:49 -07:00
027fdbd53e Adds calculated fields from Monster to MonsterViewModel. 2021-04-04 00:19:49 -07:00
7d6bf6ec34 Adds titles to terms and privacy pages. 2021-03-25 23:51:03 -07:00
8002147b91 Adds titles to terms and privacy pages. 2021-03-25 23:51:03 -07:00
4506e76a1a Attempt 2 to fix the broken theme. 2021-03-25 23:47:18 -07:00
98b59ff445 Attempt 2 to fix the broken theme. 2021-03-25 23:47:18 -07:00
ef75c7d0ce Set theme jekyll-theme-tactile 2021-03-25 23:42:23 -07:00
dd4fd92393 Set theme jekyll-theme-tactile 2021-03-25 23:42:23 -07:00
5b6b185da4 Attempt to fix the broken theme. 2021-03-25 23:41:20 -07:00
aaba70761b Attempt to fix the broken theme. 2021-03-25 23:41:20 -07:00
66a4eab5a1 Adds the default jekyll site to docs.
This was from the guide to setup github-pages.
2021-03-25 23:38:06 -07:00
85810a1ebb Adds the default jekyll site to docs.
This was from the guide to setup github-pages.
2021-03-25 23:38:06 -07:00
25377a495e Adds license link to terms. 2021-03-25 21:35:05 -07:00
8676bc8e82 Adds license link to terms. 2021-03-25 21:35:05 -07:00
7bcfc7898c Upadtes terms, privacy, and license. 2021-03-25 21:32:56 -07:00
fafdc952ac Upadtes terms, privacy, and license. 2021-03-25 21:32:56 -07:00
5e48cb976b Adds generated privacy policy and terms and conditions. 2021-03-25 21:23:13 -07:00
4bd749ce9c Adds generated privacy policy and terms and conditions. 2021-03-25 21:23:13 -07:00
8ffa6231c2 Set theme jekyll-theme-tactile 2021-03-25 20:02:43 -07:00
781e6cfb9e Set theme jekyll-theme-tactile 2021-03-25 20:02:43 -07:00
59bbec7952 Adds app icon. 2021-03-25 19:49:04 -07:00
27893bff24 Adds app icon. 2021-03-25 19:49:04 -07:00
3d68d93789 Adds proficiency bonus to monster detail. 2021-03-25 17:32:40 -07:00
46e4b9a1f6 Adds proficiency bonus to monster detail. 2021-03-25 17:32:40 -07:00
6625039561 Fixes the display of the actions label. 2021-03-25 16:56:31 -07:00
f70390d585 Fixes the display of the actions label. 2021-03-25 16:56:31 -07:00
0fdb054234 Removes the + from the passive perception display. 2021-03-25 16:56:04 -07:00
9d7add9774 Removes the + from the passive perception display. 2021-03-25 16:56:04 -07:00
0a335b372a Adds legendary actions. 2021-03-25 16:55:03 -07:00
c686582184 Adds legendary actions. 2021-03-25 16:55:03 -07:00
b5dc107766 Fixes the layout of the trait editor. 2021-03-25 16:17:38 -07:00
59d2b9769a Fixes the layout of the trait editor. 2021-03-25 16:17:38 -07:00
ebd60fbb2e Fixing whitespace and adding tasks. 2021-03-25 16:17:13 -07:00
f62641bd21 Fixing whitespace and adding tasks. 2021-03-25 16:17:13 -07:00
45b9959ef4 Renames the ability editor to trait editor since it's not just for abilities any more. 2021-03-25 15:53:38 -07:00
6b30b8d12e Renames the ability editor to trait editor since it's not just for abilities any more. 2021-03-25 15:53:38 -07:00
55e0ef65fd Changes languages so you can remove them. 2021-03-25 15:46:38 -07:00
71a90a8851 Changes languages so you can remove them. 2021-03-25 15:46:38 -07:00
8d9908369e Adds actions. 2021-03-25 15:45:33 -07:00
5aede274c8 Adds actions. 2021-03-25 15:45:33 -07:00
a9ad7a7fa8 Adds MarkdownUI dependency and abilities. 2021-03-25 14:31:35 -07:00
429eb7aca8 Adds MarkdownUI dependency and abilities. 2021-03-25 14:31:35 -07:00
9fd4c1f71d Makes the SizeType initializer prefer proper case but fall back to case insensitive. 2021-03-25 01:21:45 -07:00
89376e85a1 Makes the SizeType initializer prefer proper case but fall back to case insensitive. 2021-03-25 01:21:45 -07:00
cd559abbb0 Fixes how passive perception is calculated.
The base value of 10 was left out.
2021-03-25 01:20:35 -07:00
2e46676ecd Fixes how passive perception is calculated.
The base value of 10 was left out.
2021-03-25 01:20:35 -07:00
d6286006b8 Removes unneeded parameters passed to oxfordJoin 2021-03-25 01:19:44 -07:00
6d95a5f094 Removes unneeded parameters passed to oxfordJoin 2021-03-25 01:19:44 -07:00
c970dfc4ed Adds tasks to show the proficiency bonus and to sort damage types better. 2021-03-25 01:18:35 -07:00
4426ed52eb Adds tasks to show the proficiency bonus and to sort damage types better. 2021-03-25 01:18:35 -07:00
d408edfdeb Switches HP calculations to be NPC style instead of PC style.
A PC would get full HP at level 1, but a 1 hit die creature would only get the average roll. This will be a configurable option later.
2021-03-25 01:16:54 -07:00
ba7a680cfd Switches HP calculations to be NPC style instead of PC style.
A PC would get full HP at level 1, but a 1 hit die creature would only get the average roll. This will be a configurable option later.
2021-03-25 01:16:54 -07:00
9fd5f55e9d Adds challenge rating and proficiency bonus to the monster editor. 2021-03-25 00:30:58 -07:00
4ea630ecca Adds challenge rating and proficiency bonus to the monster editor. 2021-03-25 00:30:58 -07:00
33e2b52dc3 Language editor layout fixes. 2021-03-25 00:19:44 -07:00
70e5f5edf0 Language editor layout fixes. 2021-03-25 00:19:44 -07:00
627f02409c Adds languages to the editor. 2021-03-24 22:29:54 -07:00
5e0b998b70 Adds languages to the editor. 2021-03-24 22:29:54 -07:00
07f59788a3 Makes oxfordJoin use standard english separators as defaults. 2021-03-24 22:27:13 -07:00
af1cd55bc9 Makes oxfordJoin use standard english separators as defaults. 2021-03-24 22:27:13 -07:00
3ec62789c6 Adds senses and passive perception.
Also makes modifier calculations return Int instead of Int64.
2021-03-24 17:46:04 -07:00
5c1a524875 Adds senses and passive perception.
Also makes modifier calculations return Int instead of Int64.
2021-03-24 17:46:04 -07:00
596186deaa Refactors DamageTypes to String since we using it as a generic list of strings editor. 2021-03-24 17:44:53 -07:00
935ab63899 Refactors DamageTypes to String since we using it as a generic list of strings editor. 2021-03-24 17:44:53 -07:00
055a8b28cc Expands fields searched in the monster search. 2021-03-24 16:00:26 -07:00
baf28534e1 Expands fields searched in the monster search. 2021-03-24 16:00:26 -07:00
3fa4ae0bdb Removes unused handler function. 2021-03-24 15:46:00 -07:00
7ddf4932f8 Removes unused handler function. 2021-03-24 15:46:00 -07:00
9e3b36da69 Adds titles to the sub views that were missing them. 2021-03-24 15:44:30 -07:00
7de324e922 Adds titles to the sub views that were missing them. 2021-03-24 15:44:30 -07:00
ed44cd9947 Sets default capitalization of text fields to make sense. 2021-03-24 15:40:44 -07:00
ab6dfea689 Sets default capitalization of text fields to make sense. 2021-03-24 15:40:44 -07:00
0c3821dbe6 Sets default names for skills and damage types to an empty string. 2021-03-24 15:37:10 -07:00
fc2c908043 Sets default names for skills and damage types to an empty string. 2021-03-24 15:37:10 -07:00
06069e89ba Sets default names for skills and damage types to an empty string. 2021-03-24 15:36:52 -07:00
3599d8b2c1 Sets default names for skills and damage types to an empty string. 2021-03-24 15:36:52 -07:00
08fb3745c8 Reorders condition immunities in the editor. 2021-03-23 23:17:38 -07:00
96eddbbaa0 Reorders condition immunities in the editor. 2021-03-23 23:17:38 -07:00
af7e3666de Adding missed file for previous commit
> Stops saving the raw core data objects in view models and makes them optional in the constructor.
2021-03-23 23:16:15 -07:00
a549e908be Adding missed file for previous commit
> Stops saving the raw core data objects in view models and makes them optional in the constructor.
2021-03-23 23:16:15 -07:00
d27a7ca5c5 Stops saving the raw core data objects in view models and makes them optional in the constructor. 2021-03-23 23:15:02 -07:00
6459ad0125 Stops saving the raw core data objects in view models and makes them optional in the constructor. 2021-03-23 23:15:02 -07:00
749da2151e Adds missing file to last commit.
> Adds damage types and condition immunities to core data so they are saved now.
2021-03-23 23:12:52 -07:00
23a33f7ffe Adds missing file to last commit.
> Adds damage types and condition immunities to core data so they are saved now.
2021-03-23 23:12:52 -07:00
85e2529289 Adds damage types and condition immunities to core data so they are saved now. 2021-03-23 23:12:01 -07:00
0f6454ea41 Adds damage types and condition immunities to core data so they are saved now. 2021-03-23 23:12:01 -07:00
d4e93a92a7 Adds shared scheme. 2021-03-23 21:34:45 -07:00
5f4902305c Adds shared scheme. 2021-03-23 21:34:45 -07:00
2b6741b5dc Adds damage vulnerabilities to the monster editor. 2021-03-22 18:06:50 -07:00
423ee475f4 Adds damage vulnerabilities to the monster editor. 2021-03-22 18:06:50 -07:00
f90227bc29 Adds damage resistances to the monster editor. 2021-03-22 18:06:30 -07:00
79ef9a50d1 Adds damage resistances to the monster editor. 2021-03-22 18:06:30 -07:00
09a16c85b7 Adds damage immunities to the monster editor. 2021-03-22 18:05:05 -07:00
ae287d0551 Adds damage immunities to the monster editor. 2021-03-22 18:05:05 -07:00
c8f18a00dd Adds condition immunities to the monster editor. 2021-03-22 18:00:05 -07:00
69b207177c Adds condition immunities to the monster editor. 2021-03-22 18:00:05 -07:00
e23b35f75e Reorganized the MonsterDetail view to get around the 10 items per group limit.
Adds layout for resistances, immunities, and languages.
2021-03-22 01:02:21 -07:00
67c3ec480d Reorganized the MonsterDetail view to get around the 10 items per group limit.
Adds layout for resistances, immunities, and languages.
2021-03-22 01:02:21 -07:00
2cd9e6d92d Adjusts the monster detail view so it makes better use of small screens. 2021-03-21 20:28:05 -07:00
a59571aae8 Adjusts the monster detail view so it makes better use of small screens. 2021-03-21 20:28:05 -07:00
f5c3ce57de Adds skills display to the monster detail view. 2021-03-21 19:49:03 -07:00
3439dbda42 Adds skills display to the monster detail view. 2021-03-21 19:49:03 -07:00
0299213dfa Sorts skills specifically for the EditSkills view, but also more generally when creating a MonsterViewModel. 2021-03-21 18:28:04 -07:00
eb7199d5a9 Sorts skills specifically for the EditSkills view, but also more generally when creating a MonsterViewModel. 2021-03-21 18:28:04 -07:00
7073e3d952 Fixes to skill saving 2021-03-21 18:27:17 -07:00
bad8a374af Fixes to skill saving 2021-03-21 18:27:17 -07:00
44b585aab8 Adds EditSkill view to allow editing a specific skill. 2021-03-21 17:25:28 -07:00
e2a54c7b89 Adds EditSkill view to allow editing a specific skill. 2021-03-21 17:25:28 -07:00
106b41d2ee Adds MCAbilityScorePicker. 2021-03-21 17:25:00 -07:00
6565d0a82d Adds MCAbilityScorePicker. 2021-03-21 17:25:00 -07:00
861bae24d6 Adds EditSkills view bound to the monster view model's skills. 2021-03-21 16:18:04 -07:00
99298d3f28 Adds EditSkills view bound to the monster view model's skills. 2021-03-21 16:18:04 -07:00
e25e37c871 Moves saving throws from EditMonster to a sub view. 2021-03-21 14:47:15 -07:00
3a7125ee88 Moves saving throws from EditMonster to a sub view. 2021-03-21 14:47:15 -07:00
21eae233f3 Moves ability scores from EditMonster to a sub view. 2021-03-21 14:42:32 -07:00
5dee63fe54 Moves ability scores from EditMonster to a sub view. 2021-03-21 14:42:32 -07:00
7bef443ead Moves speed info from EditMonster to a sub view. 2021-03-21 14:38:13 -07:00
5a9a1e87d3 Moves speed info from EditMonster to a sub view. 2021-03-21 14:38:13 -07:00
ecfdf7ae58 Fixes the fetched monster on MonsterDetail not updating when the EditMonster view saves. 2021-03-21 14:37:11 -07:00
920c3ca15e Fixes the fetched monster on MonsterDetail not updating when the EditMonster view saves. 2021-03-21 14:37:11 -07:00
d90c32691a Changes the NavigationView to stack navigation style so the save button on EditMonster takes you back to MonsterDetail instead of the root of the NavigationView. 2021-03-21 14:23:38 -07:00
c1bf39aba7 Changes the NavigationView to stack navigation style so the save button on EditMonster takes you back to MonsterDetail instead of the root of the NavigationView. 2021-03-21 14:23:38 -07:00
b83a88c1f2 Moves editing armor to a sub view of EditMonster. 2021-03-21 14:22:30 -07:00
c204a6baaf Moves editing armor to a sub view of EditMonster. 2021-03-21 14:22:30 -07:00
9f0896943f Moves Basic Info section of the monster editor to a sub view. 2021-03-21 14:13:10 -07:00
c3feaf4f64 Moves Basic Info section of the monster editor to a sub view. 2021-03-21 14:13:10 -07:00
a6ad738d48 Fixes some bugs with how the editing monster is passed around.
Removes the custom cancel since we don't need it now.
2021-03-21 14:07:11 -07:00
15e3cd3810 Fixes some bugs with how the editing monster is passed around.
Removes the custom cancel since we don't need it now.
2021-03-21 14:07:11 -07:00
0e551ce01b Fixes HP display. 2021-03-21 00:46:23 -07:00
cd813285d1 Fixes HP display. 2021-03-21 00:46:23 -07:00
46372268d4 Makes EditMonster use MonsterViewModel instead of binding directly to the Core Data types. 2021-03-21 00:43:24 -07:00
30b32592ed Makes EditMonster use MonsterViewModel instead of binding directly to the Core Data types. 2021-03-21 00:43:24 -07:00
cee4f24e93 Started adding skills. 2021-02-13 20:53:38 -08:00
137d6f7c43 Started adding skills. 2021-02-13 20:53:38 -08:00
da74b68a9c Hides elements on monster detail if they don't have values to show.
Adds TODO to hide dividers when applicable.
2021-02-07 13:00:48 -08:00
a5344abeb0 Hides elements on monster detail if they don't have values to show.
Adds TODO to hide dividers when applicable.
2021-02-07 13:00:48 -08:00
fe431475a2 Adds comments with other picker types to the advantage picker for refreence. 2021-02-07 12:45:27 -08:00
3bb52bc368 Adds comments with other picker types to the advantage picker for refreence. 2021-02-07 12:45:27 -08:00
9d185d27a5 Makes saving throw proficiencies and advantages use enums instead of raw strings. 2021-02-07 12:45:03 -08:00
518ff49907 Makes saving throw proficiencies and advantages use enums instead of raw strings. 2021-02-07 12:45:03 -08:00
f960df1424 Makes armor type a picker instead of a string. 2021-02-07 12:43:23 -08:00
b5cb02937b Makes armor type a picker instead of a string. 2021-02-07 12:43:23 -08:00
f6ef6a7f3d Convertes to Swift and SwiftUI 2021-01-18 00:30:45 -08:00
8be1479357 Convertes to Swift and SwiftUI 2021-01-18 00:30:45 -08:00
3d54342687 Fixes a bug editing a new monster. 2020-10-09 22:04:28 -07:00
b79cb36335 Fixes a bug editing a new monster. 2020-10-09 22:04:28 -07:00
5da0ee2549 Updates storyboard to fix display on iPad.
Disables dark mode until we can come up with a dark mode color scheme that looks good
2020-10-04 01:12:25 -07:00
65afc41197 Updates storyboard to fix display on iPad.
Disables dark mode until we can come up with a dark mode color scheme that looks good
2020-10-04 01:12:25 -07:00
d0bd26c7e0 Fixes how we check which saving throws to show. 2020-10-04 00:46:12 -07:00
ec921aa705 Fixes how we check which saving throws to show. 2020-10-04 00:46:12 -07:00
692bfdd943 Adds advantage/disadvantage to saving throw display. 2020-10-04 00:37:11 -07:00
e057f6f12e Adds advantage/disadvantage to saving throw display. 2020-10-04 00:37:11 -07:00
0e800dfd1c Adds saving throws to monster card display.
Adds proficiencyBonus implementation to Monster. The proficiency bonus relies on CR and defaults to 0 until the CR fields are implemented
2020-10-04 00:19:04 -07:00
9ced8e3407 Adds saving throws to monster card display.
Adds proficiencyBonus implementation to Monster. The proficiency bonus relies on CR and defaults to 0 until the CR fields are implemented
2020-10-04 00:19:04 -07:00
3c3ed3c94b Adds Saving Throws to the data model and monster editor. 2020-10-03 22:44:15 -07:00
757f75657f Adds Saving Throws to the data model and monster editor. 2020-10-03 22:44:15 -07:00
23b840f3ff Disables row selection in the edit form table. 2020-10-03 22:22:12 -07:00
fae10b17ed Disables row selection in the edit form table. 2020-10-03 22:22:12 -07:00
f4c981ab36 Fixes JSON initializer and tests. 2020-09-26 23:10:24 -07:00
15f012a65a Fixes JSON initializer and tests. 2020-09-26 23:10:24 -07:00
2a9b936d0d Adds ability scores to monster cards. 2020-09-26 23:06:03 -07:00
308cb755fe Adds ability scores to monster cards. 2020-09-26 23:06:03 -07:00
0912ac0fd8 Adds select field with picker as TextField inputView. 2020-09-26 22:18:04 -07:00
b2c21e0542 Adds select field with picker as TextField inputView. 2020-09-26 22:18:04 -07:00
57bf1f2e3a Renames armorName to armorType.
Sets default values for core data fields.
Moves hit dice and hp related fields into the basic info section of the editor.
2020-09-26 17:15:43 -07:00
828d65ad44 Renames armorName to armorType.
Sets default values for core data fields.
Moves hit dice and hp related fields into the basic info section of the editor.
2020-09-26 17:15:43 -07:00
ec7f827123 Fixes initial state of integer fields. 2020-09-26 16:37:01 -07:00
b8eb935d9a Fixes initial state of integer fields. 2020-09-26 16:37:01 -07:00
caa1be50cf Cleans up code that generates HTML labels.
Adds Label for speed.
Makes the Monster Card refresh the monster from CoreData when the view is shown.
2020-09-26 15:09:46 -07:00
533677baff Cleans up code that generates HTML labels.
Adds Label for speed.
Makes the Monster Card refresh the monster from CoreData when the view is shown.
2020-09-26 15:09:46 -07:00
d041105e1e Adds speed properties to Core Data and monster editor. 2020-09-26 01:22:11 -07:00
7bbe86c901 Adds speed properties to Core Data and monster editor. 2020-09-26 01:22:11 -07:00
868bc86143 Adds Hit Dice and Custom HP to monster edit form. 2020-09-25 04:47:23 -07:00
f23fe12e7b Adds Hit Dice and Custom HP to monster edit form. 2020-09-25 04:47:23 -07:00
5e00722c3b Adds boolean field to MCFormFields. 2020-09-25 04:46:50 -07:00
6341081fde Adds boolean field to MCFormFields. 2020-09-25 04:46:50 -07:00
b2eed1ffc7 Adds HP to monster card. 2020-09-25 03:52:44 -07:00
5a2e98d35b Adds HP to monster card. 2020-09-25 03:52:44 -07:00
82e5545904 Adds HP related fields to core data.
Implements hitDieForSize and hitPointsDescription in Monster.
Adds tests.
2020-09-20 03:21:34 -07:00
16aa24150e Adds HP related fields to core data.
Implements hitDieForSize and hitPointsDescription in Monster.
Adds tests.
2020-09-20 03:21:34 -07:00
29f5ef991e Exposes constants used by Monster internally for values. 2020-09-20 03:19:24 -07:00
44354f7c8f Exposes constants used by Monster internally for values. 2020-09-20 03:19:24 -07:00
edb9449fbc Fixes EditMonsterViewController tests to use the new cell reuse identifier. 2020-09-20 03:17:28 -07:00
f94c7863b7 Fixes EditMonsterViewController tests to use the new cell reuse identifier. 2020-09-20 03:17:28 -07:00
28c1e271ab Adds ability scores (strength, dexterity, constitution, intelligence, wisdom, and charisma) to the edit monster form. 2020-09-18 01:00:30 -07:00
93399a8bda Adds ability scores (strength, dexterity, constitution, intelligence, wisdom, and charisma) to the edit monster form. 2020-09-18 01:00:30 -07:00
a6c33fb803 Adds a label to the integer form field.
Makes the string value and both string and integer label update the underlying controls when set.
2020-09-18 00:47:48 -07:00
1ecd4a4327 Adds a label to the integer form field.
Makes the string value and both string and integer label update the underlying controls when set.
2020-09-18 00:47:48 -07:00
71cd2572a2 Partial fixes to tests to run with Xcode 12. 2020-09-18 00:27:31 -07:00
800e7ef6f1 Partial fixes to tests to run with Xcode 12. 2020-09-18 00:27:31 -07:00
f973a618c6 Refactors form field cell creation into separate reusable methods. 2020-09-18 00:16:38 -07:00
84b0d246b5 Refactors form field cell creation into separate reusable methods. 2020-09-18 00:16:38 -07:00
893559baa6 Renames old form field class and delegate.
Adds new form field for integers.
2020-09-17 23:45:05 -07:00
e21c755e62 Renames old form field class and delegate.
Adds new form field for integers.
2020-09-17 23:45:05 -07:00
3dc1707f3c Updates to Xcode 12.
Drops the iOS version in both projects to 13 from 13.0 and 13.7.
2020-09-17 20:24:12 -07:00
da6a03144a Updates to Xcode 12.
Drops the iOS version in both projects to 13 from 13.0 and 13.7.
2020-09-17 20:24:12 -07:00
544c19c959 Updates comment explaining the format of the monster meta string. 2020-09-17 13:04:28 -07:00
b41b138f93 Updates comment explaining the format of the monster meta string. 2020-09-17 13:04:28 -07:00
d1a3a1d247 Makes Monster initializer use new JSONHelper methods to make parsing more expressive. 2020-09-17 13:03:48 -07:00
c58f0909bb Makes Monster initializer use new JSONHelper methods to make parsing more expressive. 2020-09-17 13:03:48 -07:00
7514237a84 Adds JSONHelper methods to make parsing json from strings and NSData objects easier. 2020-09-17 12:58:59 -07:00
c224c51f84 Adds JSONHelper methods to make parsing json from strings and NSData objects easier. 2020-09-17 12:58:59 -07:00
74745f6d54 Adds JSONHelper methods to read arrays. 2020-09-17 12:37:03 -07:00
8f52940d98 Adds JSONHelper methods to read arrays. 2020-09-17 12:37:03 -07:00
81726e9554 Adds JSONHelper methods to read dictionaries. 2020-09-17 01:32:52 -07:00
555efac0c4 Adds JSONHelper methods to read dictionaries. 2020-09-17 01:32:52 -07:00
a4774c2401 Adds methods to JSONHelper to read boolean values. 2020-09-17 00:50:27 -07:00
9bf1595f29 Adds methods to JSONHelper to read boolean values. 2020-09-17 00:50:27 -07:00
c9b15a21a5 Adds JSONHelper methods to read numbers as ints. 2020-09-17 00:30:22 -07:00
4a1145fd28 Adds JSONHelper methods to read numbers as ints. 2020-09-17 00:30:22 -07:00
2ef6c06e32 Adds JSONHelper methods to read numbers as NSNumber objects. 2020-09-17 00:27:40 -07:00
82625d4548 Adds JSONHelper methods to read numbers as NSNumber objects. 2020-09-17 00:27:40 -07:00
e821656871 Adds JSONHelper methods to read strings. (+1 squashed commit)
Squashed commits:
[30b0a71] Adds JSONHelper methods to read strings.
2020-09-17 00:23:28 -07:00
bc23b55429 Adds JSONHelper methods to read strings. (+1 squashed commit)
Squashed commits:
[30b0a71] Adds JSONHelper methods to read strings.
2020-09-17 00:23:28 -07:00
0fe24d767c Adds armor class to monster cards. 2020-09-15 20:26:39 -07:00
42baec2a38 Adds armor class to monster cards. 2020-09-15 20:26:39 -07:00
6586b429b7 Adds HTMLHelper to convert from HTML in an NSString to a properly attributed NSAttributedString. 2020-09-15 20:18:08 -07:00
8029fb7540 Adds HTMLHelper to convert from HTML in an NSString to a properly attributed NSAttributedString. 2020-09-15 20:18:08 -07:00
a78b6e03c8 Adds armorClassDescription to Monster. 2020-09-15 20:00:15 -07:00
4ff6a28c67 Adds armorClassDescription to Monster. 2020-09-15 20:00:15 -07:00
88927c9ddc Adds shieldBonus to Monster entity.
Adds tests for shieldBonus.
2020-09-15 20:00:15 -07:00
3e93aa59b4 Adds shieldBonus to Monster entity.
Adds tests for shieldBonus.
2020-09-15 20:00:15 -07:00
700724ce5b Adds otherArmorDescription to Monster entity.
Adds tests for otherArmorDescription.
2020-09-15 20:00:15 -07:00
bead4f8ee5 Adds otherArmorDescription to Monster entity.
Adds tests for otherArmorDescription.
2020-09-15 20:00:15 -07:00
e309e15af4 Adds armorName to Monster entity.
Adds tests for armorName.
2020-09-15 20:00:15 -07:00
5aa88932f7 Adds armorName to Monster entity.
Adds tests for armorName.
2020-09-15 20:00:15 -07:00
f8d3a893ca Moves ability scores to Core Data entity. 2020-09-15 20:00:15 -07:00
a6f048cc2d Moves ability scores to Core Data entity. 2020-09-15 20:00:15 -07:00
d66e5140e7 Adds charismaScore and charismaModifier to Monster.
Adds tests for charismaScore and charismaModifier.
2020-09-15 20:00:15 -07:00
e541fdcae3 Adds charismaScore and charismaModifier to Monster.
Adds tests for charismaScore and charismaModifier.
2020-09-15 20:00:15 -07:00
efaf492e41 Adds wisdomScore and wisdomModifier to Monster.
Adds tests for wisdomScore and wisdomModifier.
2020-09-15 20:00:15 -07:00
a26031a04f Adds wisdomScore and wisdomModifier to Monster.
Adds tests for wisdomScore and wisdomModifier.
2020-09-15 20:00:15 -07:00
dc8299ade6 Adds intelligenceScore and intelligenceModifier to Monster.
Adds tests for intelligenceScore and intelligenceModifier.
2020-09-15 20:00:15 -07:00
05b3ad1280 Adds intelligenceScore and intelligenceModifier to Monster.
Adds tests for intelligenceScore and intelligenceModifier.
2020-09-15 20:00:15 -07:00
aca029955e Adds constitutionScore and constitutionModifier to Monster.
Adds tests for constitutionScore and constitutionModifier.
2020-09-15 20:00:15 -07:00
26f92e43f6 Adds constitutionScore and constitutionModifier to Monster.
Adds tests for constitutionScore and constitutionModifier.
2020-09-15 20:00:15 -07:00
58ce77e4df Adds dexterityScore and dexterityModifier to Monster.
Adds tests for dexterityScore and dexterityModifier.
2020-09-15 20:00:14 -07:00
d3dd60fb2c Adds dexterityScore and dexterityModifier to Monster.
Adds tests for dexterityScore and dexterityModifier.
2020-09-15 20:00:14 -07:00
c14b10a032 Adds strengthScore and strengthModifier to Monster.
Adds tests for strengthScore and strengthModifier.
2020-09-15 20:00:14 -07:00
91a99a5df3 Adds strengthScore and strengthModifier to Monster.
Adds tests for strengthScore and strengthModifier.
2020-09-15 20:00:14 -07:00
52a7ba871a Adds abilityModifierForScore and tests. 2020-09-15 19:58:58 -07:00
2fd50a9f68 Adds abilityModifierForScore and tests. 2020-09-15 19:58:58 -07:00
2aaca29741 Adds alignment to monsters.
Adds tests for editing alignment on monsters.
Adds tests for monster meta text when alignment is set.
2020-09-12 17:39:07 -07:00
513e2c3511 Adds alignment to monsters.
Adds tests for editing alignment on monsters.
Adds tests for monster meta text when alignment is set.
2020-09-12 17:39:07 -07:00
7643b98c01 Adds subtype to monster.
Adds EditMonsterViewController tests for editing subtype.
Adds tests for meta property of Monster.
2020-09-12 17:16:36 -07:00
0c1deecb5b Adds subtype to monster.
Adds EditMonsterViewController tests for editing subtype.
Adds tests for meta property of Monster.
2020-09-12 17:16:36 -07:00
91df63802a Adds monster type to editor.
Sets all entity attributes for monster to default to empty string instead of null.
Adds test for copyFromMonster.
Makes initWithMonster:andContext call copyFromMonster to ensure they use the same logic to clone the other monster.
2020-09-12 12:52:45 -07:00
42ddfbd52f Adds monster type to editor.
Sets all entity attributes for monster to default to empty string instead of null.
Adds test for copyFromMonster.
Makes initWithMonster:andContext call copyFromMonster to ensure they use the same logic to clone the other monster.
2020-09-12 12:52:45 -07:00
9396502b3d Adds meta string to monster cards.
Implements meta method in Monster to return a formatted meta string.
Adds size to edit monster.
Moves monster copy logic to copyFromMonster in the Monster class.
Fixes JSON parsing to set strings to an empty string if they're missing from the json blob.
Makes Monster.size default to an empty string instead of null.
Cleans up some raw strings to use NSLocalizedString instead.
2020-09-12 03:08:10 -07:00
ee9994c2c8 Adds meta string to monster cards.
Implements meta method in Monster to return a formatted meta string.
Adds size to edit monster.
Moves monster copy logic to copyFromMonster in the Monster class.
Fixes JSON parsing to set strings to an empty string if they're missing from the json blob.
Makes Monster.size default to an empty string instead of null.
Cleans up some raw strings to use NSLocalizedString instead.
2020-09-12 03:08:10 -07:00
edb1c672eb Makes search and library use the same monster detail view/controller. 2020-09-12 02:16:14 -07:00
dd05b39ea9 Makes search and library use the same monster detail view/controller. 2020-09-12 02:16:14 -07:00
fa553a447a Fixes tests.
Adds CoreData codegen categories to tests.
Updates initializers to pass coredata contexts.
2020-09-12 02:06:25 -07:00
68e2f84e21 Fixes tests.
Adds CoreData codegen categories to tests.
Updates initializers to pass coredata contexts.
2020-09-12 02:06:25 -07:00
c7d821e72f Makes the search bar redo the search when the view is reloaded. 2020-09-12 01:26:59 -07:00
8efb17a56c Makes the search bar redo the search when the view is reloaded. 2020-09-12 01:26:59 -07:00
e0fce50e1a Adds swipe action to delete a card from the library. 2020-09-12 01:25:56 -07:00
fade50eef5 Adds swipe action to delete a card from the library. 2020-09-12 01:25:56 -07:00
1e79bc5500 Makes views use monsters from CoreData instead of hard coded ones. 2020-09-12 01:16:56 -07:00
2e70466891 Makes views use monsters from CoreData instead of hard coded ones. 2020-09-12 01:16:56 -07:00
f61fdc0aba Makes Monster a CoreData entity 2020-09-12 00:55:49 -07:00
fb6418cc80 Makes Monster a CoreData entity 2020-09-12 00:55:49 -07:00
dd7f46f580 Adds monster name as editable in the edit monster view controller. 2020-09-12 00:21:49 -07:00
f0e804ce59 Adds monster name as editable in the edit monster view controller. 2020-09-12 00:21:49 -07:00
a8c88feb1f Adds string form field cell for use in table views to edit a string property. 2020-09-12 00:17:41 -07:00
0e1a07972b Adds string form field cell for use in table views to edit a string property. 2020-09-12 00:17:41 -07:00
e3384538a5 Makes nav back from edit monster to monster view update the monster. 2020-09-12 00:14:01 -07:00
25b9d06935 Makes nav back from edit monster to monster view update the monster. 2020-09-12 00:14:01 -07:00
5c18e815dd Adds a monster edit view. 2020-09-12 00:11:20 -07:00
5f58e9e41d Adds a monster edit view. 2020-09-12 00:11:20 -07:00
b1fbc169dc Makes the library view display a list of monsters.
Makes the library view ad search view share a view controller for their destination.
2020-09-11 23:42:23 -07:00
651a8e30d4 Makes the library view display a list of monsters.
Makes the library view ad search view share a view controller for their destination.
2020-09-11 23:42:23 -07:00
314906f74d Adds copy constructor to Monster initWithMonster. 2020-09-11 23:30:22 -07:00
327a640b83 Adds copy constructor to Monster initWithMonster. 2020-09-11 23:30:22 -07:00
2a20e7262d Removes some debug logging statements. 2020-09-07 22:37:57 -07:00
15a89785c6 Removes some debug logging statements. 2020-09-07 22:37:57 -07:00
0cff85092b Droppes deployment target to iOS 13.0 2020-09-07 18:13:59 -07:00
437e18d2cc Droppes deployment target to iOS 13.0 2020-09-07 18:13:59 -07:00
49cb07704b Makes the search screen actually do searches.
Makes the monster detail view set the title if there is no name label bound.
2020-09-07 16:26:51 -07:00
e18f44a136 Makes the search screen actually do searches.
Makes the monster detail view set the title if there is no name label bound.
2020-09-07 16:26:51 -07:00
6e76767c26 Removes default text in search bar. 2020-09-06 18:47:14 -07:00
48792ceebe Removes default text in search bar. 2020-09-06 18:47:14 -07:00
1342c55a1f Adds name to Monster detail view.
Adds json initializers for Monster.
2020-09-06 17:33:58 -07:00
0ff71193d6 Adds name to Monster detail view.
Adds json initializers for Monster.
2020-09-06 17:33:58 -07:00
32025eb4e7 Adds name to monster detail view.
Passes the selected monster from the search view to the monster detail view.
2020-09-06 16:42:40 -07:00
d338d42912 Adds name to monster detail view.
Passes the selected monster from the search view to the monster detail view.
2020-09-06 16:42:40 -07:00
2f57c10a5a Adds placeholder monsters to search view.
Adds navigation from search view to monster detail view.
2020-09-06 13:05:14 -07:00
3db7334ba4 Adds placeholder monsters to search view.
Adds navigation from search view to monster detail view.
2020-09-06 13:05:14 -07:00
84ce8ba80c Adds UITableView for search results.
Adds constraints to search view.
2020-09-06 12:48:03 -07:00
757f0ded69 Adds UITableView for search results.
Adds constraints to search view.
2020-09-06 12:48:03 -07:00
555ba4c007 Fixes default initializer of Skill model.
Adds tests for Skill model.
2020-09-05 23:25:40 -07:00
d6f12b302a Fixes default initializer of Skill model.
Adds tests for Skill model.
2020-09-05 23:25:40 -07:00
c3031fbc39 Adds cocoapods for libraries.
Adds OCMockito and OCHamcrest libs.
2020-09-05 22:06:51 -07:00
bab5a55c3b Adds cocoapods for libraries.
Adds OCMockito and OCHamcrest libs.
2020-09-05 22:06:51 -07:00
af47156557 Fixes default initializer of SavingThrow model.
Adds tests for SavingThrow model.
2020-09-05 21:15:07 -07:00
f688898d96 Fixes default initializer of SavingThrow model.
Adds tests for SavingThrow model.
2020-09-05 21:15:07 -07:00
5c5a0bb4f1 Fixes typing in Language initializer.
Fixes default initializer of Language model.
Adds tests for Language model.
2020-09-05 21:01:23 -07:00
8a758448a0 Fixes typing in Language initializer.
Fixes default initializer of Language model.
Adds tests for Language model.
2020-09-05 21:01:23 -07:00
70ddeeb5f1 Fixes default initializer of DamageType model.
Adds tests for DamageType model.
2020-09-05 20:01:53 -07:00
bfcef65da3 Fixes default initializer of DamageType model.
Adds tests for DamageType model.
2020-09-05 20:01:53 -07:00
e4f33e553a Fixes default initializer of Action model.
Adds tests for Action model.
2020-09-05 19:37:28 -07:00
937aba27b1 Fixes default initializer of Action model.
Adds tests for Action model.
2020-09-05 19:37:28 -07:00
d8ddde6e7a Fixes default initializer of Ability model.
Adds tests for Ability model.
2020-09-05 19:33:05 -07:00
90c28b6629 Fixes default initializer of Ability model.
Adds tests for Ability model.
2020-09-05 19:33:05 -07:00
473fa15f7c Adds StringHelper with isStringNilOrEmpty method. 2020-09-05 19:06:39 -07:00
06c1e1b880 Adds StringHelper with isStringNilOrEmpty method. 2020-09-05 19:06:39 -07:00
1e1abba82f Adds implementation for Skill model.
Adds stubbed out implementation of Monster model. all methods throw exceptions.
2020-09-05 19:04:56 -07:00
f12465222f Adds implementation for Skill model.
Adds stubbed out implementation of Monster model. all methods throw exceptions.
2020-09-05 19:04:56 -07:00
8871a57a2c Adds implementation of SavingThrow model. 2020-09-05 18:33:57 -07:00
55ed75e36a Adds implementation of SavingThrow model. 2020-09-05 18:33:57 -07:00
1e5e7b4cca Adds implementation of Language model. 2020-09-05 18:33:25 -07:00
b8db6daa94 Adds implementation of Language model. 2020-09-05 18:33:25 -07:00
e98443a5dd Adds implementation of DamageType model. 2020-09-05 18:19:32 -07:00
1d47e65b1e Adds implementation of DamageType model. 2020-09-05 18:19:32 -07:00
fbbecce6e6 Adds implementation of Action model. 2020-09-05 18:19:18 -07:00
687c215e73 Adds implementation of Action model. 2020-09-05 18:19:18 -07:00
bc947f5c10 Adds implementation of Ability model. 2020-09-05 18:18:53 -07:00
9a3bae2f5c Adds implementation of Ability model. 2020-09-05 18:18:53 -07:00
ea2539e21c Adds stubbed out model classes Ability, Action, DamageType, Language, Monster, SavingThrow, and Skill. 2020-09-05 18:16:24 -07:00
3b4808a360 Adds stubbed out model classes Ability, Action, DamageType, Language, Monster, SavingThrow, and Skill. 2020-09-05 18:16:24 -07:00
c7202763ec Adds top level nav elements to the tab bar. 2020-09-04 21:54:00 -07:00
4263645966 Adds top level nav elements to the tab bar. 2020-09-04 21:54:00 -07:00
afdbe20f80 Creates .gitignore and adds xcuserdata to it. 2020-09-03 17:21:19 -07:00
e017b2859d Adds space to app name. 2020-09-03 17:20:41 -07:00
d3f1e8dae2 Adds readme with build status badge. 2020-09-02 16:42:50 -07:00
3af7380f37 Makes default scheme a shared scheme. 2020-09-02 16:21:00 -07:00
58b30d59e9 Initial Commit 2020-09-02 16:13:17 -07:00
342 changed files with 22073 additions and 838 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.DS_Store
._*
thumbs.db

17
Android/.gitignore vendored Normal file
View File

@@ -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

1
Android/.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
MonsterCards

116
Android/.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,116 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

6
Android/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
</component>
</project>

20
Android/.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="FieldCanBeLocal" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="TrivialIf" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

25
Android/.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

67
Android/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="app/src/main/res/layout/dropdown_list_item.xml" value="0.12777777777777777" />
<entry key="app/src/main/res/layout/fragment_dashboard_list_item.xml" value="0.2210144927536232" />
<entry key="app/src/main/res/layout/fragment_edit_strings_list.xml" value="0.2210144927536232" />
<entry key="app/src/main/res/layout/fragment_edit_strings_list_item.xml" value="0.2210144927536232" />
<entry key="app/src/main/res/layout/fragment_edit_traits_list.xml" value="0.2210144927536232" />
<entry key="app/src/main/res/layout/fragment_edit_traits_list_item.xml" value="0.2210144927536232" />
<entry key="app/src/main/res/layout/fragment_search.xml" value="0.16875" />
<entry key="app/src/main/res/layout/simple_list_item.xml" value="0.2210144927536232" />
</map>
</option>
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="14">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="2" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
<item index="6" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="7" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="12" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
<item index="13" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="14">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="2" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="6" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="11" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
<item index="12" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
<item index="13" class="java.lang.String" itemvalue="lombok.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

12
Android/.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

16
Android/.idea/saveactions_settings.xml generated Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SaveActionSettings">
<option name="actions">
<set>
<option value="activate" />
<option value="activateOnShortcut" />
<option value="activateOnBatch" />
<option value="noActionIfCompileErrors" />
<option value="organizeImports" />
<option value="reformat" />
</set>
</option>
<option name="configurationPath" value="" />
</component>
</project>

6
Android/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

4
Android/README.md Normal file
View File

@@ -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

1
Android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

109
Android/app/build.gradle Normal file
View File

@@ -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.2'
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.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
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.1.0'
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.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.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"
}

32
Android/app/proguard-rules.pro vendored Normal file
View File

@@ -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 * {
<fields>;
public static **[] values();
public static ** valueOf(java.lang.String);
}

View File

@@ -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')"
]
}
}

View File

@@ -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')"
]
}
}

View File

@@ -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')"
]
}
}

View File

@@ -0,0 +1,73 @@
## Monster
id: UUID as TEXT // doesn't exist in the iOS model
abilities: Set<Trait> converted to JSON as TEXT
actions: Set<Trait> converted to JSON as TEXT
alignment: String as TEXT
armor_type: Enum<String> as TEXT
blindsight_range: int as INTEGER
burrow_speed: int as INTEGER
can_hover: boolean as INTEGER
challenge_rating: Enum<String> as TEXT
charisma_saving_throw_advantage
charisma_saving_throw_proficiency
charisma_score: int as INTEGER
climb_speed: int as INTEGER
condition_immunities: Set<String> 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<String> converted to JSON as TEXT
damage_resistances: Set<String> converted to JSON as TEXT
damage_vulnerabilities: Set<String> 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<Language> 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<Skill> 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"

View File

@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View File

@@ -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
);
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.majinnaibu.monstercards">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MonsterCardsApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<action android:name="android.intent.action.PICK" />
<action android:name="android.intent.action.INSERT" />
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.ALTERNATIVE" />
<category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="text/plain"
android:scheme="content" />
<data
android:mimeType="application/octet-stream"
android:scheme="content" />
<data
android:mimeType="text/plain"
android:scheme="file" />
</intent-filter>
<nav-graph android:value="@navigation/mobile_navigation" />
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,80 @@
package com.majinnaibu.monstercards;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
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.SetOfSavingThrowConverter;
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,
SetOfSavingThrowConverter.class,
SetOfSkillConverter.class,
SetOfStringConverter.class,
UUIDConverter.class,
})
public abstract class AppDatabase extends RoomDatabase {
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 static AppDatabase mDB = null;
public static AppDatabase getInstance(Context context) {
if (mDB == null) {
synchronized (AppDatabase.class) {
if (mDB == null) {
// .fallbackToDestructiveMigration()
mDB = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "monsters")
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.fallbackToDestructiveMigrationOnDowngrade()
// .fallbackToDestructiveMigration()
.build();
}
}
}
return mDB;
}
public abstract MonsterDAO monsterDAO();
}

View File

@@ -0,0 +1,117 @@
package com.majinnaibu.monstercards;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.init.AppCenterInitializer;
import com.majinnaibu.monstercards.init.FlipperInitializer;
import com.majinnaibu.monstercards.utils.Logger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Objects;
public class MainActivity extends AppCompatActivity {
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
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((controller, destination, arguments) -> {
FlipperInitializer.sendNavigationEvent(controller, destination, arguments);
});
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 = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
NavDirections action = MobileNavigationDirections.actionGlobalMonsterImportFragment(json);
navController.navigate(action);
}
}
private String readMonsterJSONFromIntent(Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
String type = intent.getType();
String json;
Uri uri = null;
if ("android.intent.action.SEND".equals(action) && "text/plain".equals(type)) {
uri = extras.getParcelable("android.intent.extra.STREAM");
} else if ("android.intent.action.VIEW".equals(action) && ("text/plain".equals(type) || "application/octet-stream".equals(type))) {
uri = intent.getData();
} else {
Logger.logError(String.format("unexpected launch configuration action: %s, type: %s, uri: %s", action, type, uri));
}
if (uri == null) {
return null;
}
json = readContentsOfUri(uri);
if (StringHelper.isNullOrEmpty(json)) {
return null;
}
return json;
}
private String readContentsOfUri(Uri uri) {
StringBuilder builder = new StringBuilder();
try (InputStream inputStream =
getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(
new InputStreamReader(Objects.requireNonNull(inputStream)))) {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} catch (IOException e) {
Logger.logError("error reading file", e);
return null;
}
return builder.toString();
}
}

View File

@@ -0,0 +1,48 @@
package com.majinnaibu.monstercards;
import android.app.Application;
import android.content.res.Configuration;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.init.FlipperInitializer;
public class MonsterCardsApplication extends Application {
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);
AppDatabase mDB = AppDatabase.getInstance(this);
m_monsterLibraryRepository = new MonsterRepository(mDB);
}
// 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();
}
}

View File

@@ -0,0 +1,110 @@
package com.majinnaibu.monstercards.data;
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 {
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;
}
}

View File

@@ -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<List<Monster>> getAll();
@Query("SELECT * FROM monsters WHERE id IN (:monsterIds)")
Flowable<List<Monster>> loadAllByIds(String[] monsterIds);
@Query("SELECT * FROM monsters WHERE name LIKE :name LIMIT 1")
Flowable<Monster> findByName(String name);
@Query("SELECT monsters.* FROM monsters JOIN monsters_fts ON monsters.oid = monsters_fts.docid WHERE monsters_fts MATCH :searchText")
Flowable<List<Monster>> search(String searchText);
@Insert
Completable insertAll(Monster... monsters);
@Insert(onConflict = OnConflictStrategy.REPLACE)
Completable save(Monster... monsters);
@Delete
Completable delete(Monster monster);
}

View File

@@ -0,0 +1,71 @@
package com.majinnaibu.monstercards.data;
import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.AppDatabase;
import com.majinnaibu.monstercards.models.Monster;
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;
public class MonsterRepository {
private AppDatabase m_db;
public MonsterRepository(@NonNull AppDatabase db) {
m_db = db;
}
public Flowable<List<Monster>> getMonsters() {
return m_db.monsterDAO()
.getAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<Monster>> searchMonsters(String searchText) {
return m_db.monsterDAO()
.search(searchText)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<Monster> getMonster(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;
}
}

View File

@@ -0,0 +1,18 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.room.TypeConverter;
import com.majinnaibu.monstercards.data.enums.ArmorType;
public class ArmorTypeConverter {
@TypeConverter
public static String fromArmorType(ArmorType armorType) {
return armorType.stringValue;
}
@TypeConverter
public static ArmorType armorTypeFromStringValue(String stringValue) {
return ArmorType.valueOfString(stringValue);
}
}

View File

@@ -0,0 +1,18 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.room.TypeConverter;
import com.majinnaibu.monstercards.data.enums.ChallengeRating;
public class ChallengeRatingConverter {
@TypeConverter
public static String fromChallengeRating(ChallengeRating challengeRating) {
return challengeRating.stringValue;
}
@TypeConverter
public static ChallengeRating challengeRatingFromStringValue(String stringValue) {
return ChallengeRating.valueOfString(stringValue);
}
}

View File

@@ -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<Trait> traits) {
Gson gson = new Gson();
return gson.toJson(traits);
}
@TypeConverter
public static List<Trait> listOfTraitsFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<ArrayList<Trait>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

@@ -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<Language> languages) {
Gson gson = new Gson();
return gson.toJson(languages);
}
@TypeConverter
public static Set<Language> setOfLanguageFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<HashSet<Language>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

@@ -0,0 +1,31 @@
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.SavingThrow;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Set;
public class SetOfSavingThrowConverter {
@TypeConverter
public static String fromSetOfSavingThrow(Set<SavingThrow> savingThrows) {
Gson gson = new Gson();
SavingThrow[] saves = new SavingThrow[savingThrows.size()];
savingThrows.toArray(saves);
return gson.toJson(saves);
}
@TypeConverter
public static Set<SavingThrow> setOfSavingThrowFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<HashSet<SavingThrow>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

@@ -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<Skill> skills) {
Gson gson = new Gson();
return gson.toJson(skills);
}
@TypeConverter
public static Set<Skill> setOfSkillFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<HashSet<Skill>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

@@ -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<String> strings) {
Gson gson = new Gson();
return gson.toJson(strings);
}
@TypeConverter
public static Set<String> setOfStringFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<HashSet<String>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

@@ -0,0 +1,18 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.room.TypeConverter;
import java.util.UUID;
public class UUIDConverter {
@TypeConverter
public static String fromUUID(UUID uuid) {
return uuid.toString();
}
@TypeConverter
public static UUID uuidFromString(String string) {
return UUID.fromString(string);
}
}

View File

@@ -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("intellligence", "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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,27 @@
package com.majinnaibu.monstercards.data.enums;
public enum ProficiencyType {
NONE("none", "None", ""),
PROFICIENT("proficient", "Proficient", "P"),
EXPERTISE("experties", "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;
}
}

View File

@@ -0,0 +1,9 @@
package com.majinnaibu.monstercards.data.enums;
public enum StringType {
CONDITION_IMMUNITY,
DAMAGE_IMMUNITY,
DAMAGE_RESISTANCE,
DAMAGE_VULNERABILITY,
SENSE
}

View File

@@ -0,0 +1,10 @@
package com.majinnaibu.monstercards.data.enums;
public enum TraitType {
ABILITY,
ACTION,
LAIR_ACTION,
LEGENDARY_ACTION,
REGIONAL_ACTION,
REACTIONS
}

View File

@@ -0,0 +1,15 @@
package com.majinnaibu.monstercards.helpers;
import java.util.Objects;
public final class ArrayHelper {
public static int indexOf(Object[] array, Object target) {
for (int index = 0; index < array.length; index++) {
if (Objects.equals(array[index], target)) {
return index;
}
}
return -1;
}
}

View File

@@ -0,0 +1,38 @@
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.NodeRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlNodeRendererFactory;
import org.commonmark.renderer.html.HtmlRenderer;
public final class CommonMarkHelper {
private static final class MyNodeRendererFactory implements HtmlNodeRendererFactory {
@Override
public NodeRenderer create(HtmlNodeRendererContext context) {
return null;
}
}
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);
}
}

View File

@@ -0,0 +1,293 @@
package com.majinnaibu.monstercards.helpers;
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 org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MonsterImportHelper {
public static Monster fromJSON(String json) {
JsonParser parser = new JsonParser();
JsonObject rootDict = parser.parse(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(@NotNull 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(@NotNull JsonObject dict, String name, int defaultValue) {
if (dict.has(name)) {
JsonElement element = dict.get(name);
if (element.isJsonPrimitive()) {
JsonPrimitive rawValue = element.getAsJsonPrimitive();//dict.getAsJsonPrimitive(name);
if (rawValue.isNumber()) {
return rawValue.getAsInt();
}
}
}
return defaultValue;
}
public static boolean getBool(JsonObject dict, String name) {
return getBool(dict, name, false);
}
public static boolean getBool(@NotNull 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();
}
}
}
return defaultValue;
}
@NotNull
public static String formatDistance(String name, int distance) {
return String.format("%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));
}
}
@NotNull
public static List<Trait> getListOfTraits(@NotNull JsonObject dict, String name) {
ArrayList<Trait> 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, "description");
Trait trait = new Trait(traitName, description);
traits.add(trait);
}
}
}
}
return traits;
}
public static void addSavingThrows(Monster monster, 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;
}
}
}
}
}
}
public static Set<Skill> getSetOfSkills(JsonObject root) {
HashSet<Skill> 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;
}
public static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name) {
return getSetOfDamageTypes(rootDict, name, null);
}
public static Set<String> getSetOfDamageTypes(JsonObject root, String name, String type) {
HashSet<String> 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;
}
public static Set<Language> getSetOfLanguages(JsonObject root, String name) {
HashSet<Language> 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;
}
}
}

View File

@@ -0,0 +1,16 @@
package com.majinnaibu.monstercards.helpers;
@SuppressWarnings({"BooleanMethodIsAlwaysInverted", "RedundantIfStatement"})
public final class StringHelper {
public static boolean isNullOrEmpty(CharSequence value) {
if (value == null) {
return true;
}
if ("".contentEquals(value)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,37 @@
package com.majinnaibu.monstercards.models;
import java.util.Comparator;
public class Language implements Comparator<Language>, Comparable<Language> {
public Language(String name, boolean speaks) {
mName = name;
mSpeaks = speaks;
}
private String mName;
public String getName() {
return mName;
}
public void setName(String value) {
mName = value;
}
private boolean mSpeaks;
public boolean getSpeaks() {
return mSpeaks;
}
public void setSpeaks(boolean value) {
mSpeaks = value;
}
@Override
public int compareTo(Language o) {
return this.getName().compareToIgnoreCase(o.getName());
}
@Override
public int compare(Language o1, Language o2) {
return o1.getName().compareToIgnoreCase(o2.getName());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -0,0 +1,69 @@
package com.majinnaibu.monstercards.models;
import android.annotation.SuppressLint;
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;
@SuppressLint("DefaultLocale")
public class Skill implements Comparator<Skill>, Comparable<Skill> {
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.substring(0, 1),
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);
}
}

View File

@@ -0,0 +1,12 @@
package com.majinnaibu.monstercards.models;
public class Trait {
public String name;
public String description;
public Trait(String name, String description) {
this.name = name;
this.description = description;
}
}

View File

@@ -0,0 +1,72 @@
package com.majinnaibu.monstercards.placeholder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helper class for providing sample content for user interfaces created by
* Android template wizards.
* <p>
* TODO: Replace all uses of this class before publishing your app.
*/
public class PlaceholderContent {
/**
* An array of sample (placeholder) items.
*/
public static final List<PlaceholderItem> ITEMS = new ArrayList<PlaceholderItem>();
/**
* A map of sample (placeholder) items, by ID.
*/
public static final Map<String, PlaceholderItem> ITEM_MAP = new HashMap<String, PlaceholderItem>();
private static final int COUNT = 25;
static {
// Add some sample items.
for (int i = 1; i <= COUNT; i++) {
addItem(createPlaceholderItem(i));
}
}
private static void addItem(PlaceholderItem item) {
ITEMS.add(item);
ITEM_MAP.put(item.id, item);
}
private static PlaceholderItem createPlaceholderItem(int position) {
return new PlaceholderItem(String.valueOf(position), "Item " + position, makeDetails(position));
}
private static String makeDetails(int position) {
StringBuilder builder = new StringBuilder();
builder.append("Details about Item: ").append(position);
for (int i = 0; i < position; i++) {
builder.append("\nMore details information here.");
}
return builder.toString();
}
/**
* A placeholder item representing a piece of content.
*/
public static class PlaceholderItem {
public final String id;
public final String content;
public final String details;
public PlaceholderItem(String id, String content, String details) {
this.id = id;
this.content = content;
this.details = details;
}
@Override
public String toString() {
return content;
}
}
}

View File

@@ -0,0 +1,119 @@
package com.majinnaibu.monstercards.ui;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.library.LibraryFragment;
import com.majinnaibu.monstercards.ui.library.LibraryFragmentDirections;
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.schedulers.Schedulers;
public class MonsterListRecyclerViewAdapter extends RecyclerView.Adapter<MonsterListRecyclerViewAdapter.ViewHolder> {
public interface ItemCallback {
void onItem(Monster monster);
}
// TODO: Replace SimpleItemRecyclerViewAdapter with something better like MonsterListRecyclerViewAdapter that can be reused in search
private final LibraryFragment mParentActivity;
private List<Monster> mValues;
private final boolean mTwoPane;
private final Context mContext;
private final ItemCallback mOnDelete;
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
Monster monster = (Monster) view.getTag();
// TODO: I would like to call navigateToMonsterDetail(item.id) here
if (mTwoPane) {
// TODO: Figure out how to navigate to a MonsterDetailFragment when in two pane view.
// Bundle arguments = new Bundle();
// arguments.putString(ItemDetailFragment.ARG_ITEM_ID, monster.id.toString());
// ItemDetailFragment fragment = new ItemDetailFragment();
// fragment.setArguments(arguments);
// mParentActivity.getSupportFragmentManager().beginTransaction()
// .replace(R.id.item_detail_container, fragment)
// .commit();
} else {
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monster.id.toString());
Navigation.findNavController(view).navigate(action);
}
}
};
public MonsterListRecyclerViewAdapter(LibraryFragment parent,
Flowable<List<Monster>> itemsObservable,
ItemCallback onDelete,
boolean twoPane) {
mValues = new ArrayList<>();
mParentActivity = parent;
mTwoPane = twoPane;
mContext = parent.getContext();
mOnDelete = onDelete;
itemsObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> {
mValues = monsters;
notifyDataSetChanged();
});
}
@Override
public ViewHolder onCreateViewHolder(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 ViewHolder holder, int position) {
holder.mIdView.setText(mValues.get(position).id.toString().substring(0, 6));
holder.mContentView.setText(mValues.get(position).name);
holder.itemView.setTag(mValues.get(position));
holder.itemView.setOnClickListener(mOnClickListener);
}
@Override
public int getItemCount() {
return mValues.size();
}
public Context getContext() {
return mContext;
}
class ViewHolder extends RecyclerView.ViewHolder {
final TextView mIdView;
final TextView mContentView;
ViewHolder(View view) {
super(view);
mIdView = view.findViewById(R.id.id_text);
mContentView = view.findViewById(R.id.content);
}
}
public void deleteItem(int position) {
if (mOnDelete != null) {
Monster monster = mValues.get(position);
mOnDelete.onItem(monster);
}
}
}

View File

@@ -0,0 +1,34 @@
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.annotation.Nullable;
import androidx.lifecycle.Observer;
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(), new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
textView.setText(s);
}
});
return root;
}
}

View File

@@ -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 MutableLiveData<String> mText;
public CollectionsViewModel() {
mText = new MutableLiveData<>();
mText.setValue("This is collections fragment");
}
public LiveData<String> getText() {
return mText;
}
}

View File

@@ -0,0 +1,132 @@
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.Stepper, 0, 0);
String label = a.getString(R.styleable.Stepper_label);
if (label != null) {
mLabel = label;
}
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<AbilityScore>(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) {
mSelectedValue = (AbilityScore) parent.getItemAtPosition(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
mSelectedValue = AbilityScore.STRENGTH;
}
});
mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), mSelectedValue));
setValue(AbilityScore.STRENGTH);
// TODO: listen for changes on the component to update mSelectedValue;
}
public AbilityScorePicker(@NonNull Context context) {
this(context, null);
}
public AbilityScore getValue() {
return mSelectedValue;
}
public void setValue(AbilityScore value) {
if (value != mSelectedValue) {
mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), mSelectedValue));
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(View root) {
spinner = root.findViewById(R.id.spinner);
label = root.findViewById(R.id.label);
}
}
}

View File

@@ -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.advantage == checkedId) {
setValue(AdvantageType.ADVANTAGE);
} else if (R.id.disadvantage == 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.advantage) {
mHolder.advantage.setChecked(true);
}
} else if (mSelectedValue == AdvantageType.DISADVANTAGE) {
if (checkedId != R.id.disadvantage) {
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(View root) {
group = root.findViewById(R.id.group);
none = root.findViewById(R.id.none);
advantage = root.findViewById(R.id.advantage);
disadvantage = root.findViewById(R.id.disadvantage);
}
}
}

View File

@@ -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(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);
}
}
}

View File

@@ -0,0 +1,144 @@
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 com.majinnaibu.monstercards.utils.Logger;
import java.util.Objects;
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);
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));
Logger.logDebug(String.format("Setting stepper value value: %d, oldValue: %d, newValue: %d", value, oldValue, newValue));
if (newValue != oldValue) {
this.mCurrentValue = newValue;
if (mOnValueChangeListener != null) {
mOnValueChangeListener.onChange(newValue, oldValue);
}
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;
}
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(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);
}
}
}

View File

@@ -0,0 +1,74 @@
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;
public class DashboardFragment extends MCFragment {
private DashboardViewModel mViewModel;
private ViewHolder mHolder;
private DashboardRecyclerViewAdapter mAdapter;
@Override
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);
return root;
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 396);
Context context = requireContext();
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
recyclerView.setLayoutManager(layoutManager);
LiveData<List<Monster>> 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);
}
}
}

View File

@@ -0,0 +1,348 @@
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.ItemCallback;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.Locale;
public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, DashboardRecyclerViewAdapter.ViewHolder> {
private static final DiffUtil.ItemCallback<Monster> DIFF_CALLBACK = new DiffUtil.ItemCallback<Monster>() {
@Override
public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Monster.areItemsTheSame(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Monster.areContentsTheSame(oldItem, newItem);
}
};
private final ItemCallback<Monster> mOnClick;
protected DashboardRecyclerViewAdapter(ItemCallback<Monster> 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) {
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.onItem(holder.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) {
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 "";
}
}
}
}

View File

@@ -0,0 +1,50 @@
package com.majinnaibu.monstercards.ui.dashboard;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.majinnaibu.monstercards.AppDatabase;
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.schedulers.Schedulers;
import io.reactivex.rxjava3.subscribers.DisposableSubscriber;
public class DashboardViewModel extends AndroidViewModel {
private final AppDatabase mDB;
private final MutableLiveData<List<Monster>> mMonsters;
public DashboardViewModel(Application application) {
super(application);
mDB = AppDatabase.getInstance(application);
mMonsters = new MutableLiveData<>(new ArrayList<>());
mDB.monsterDAO()
.getAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableSubscriber<List<Monster>>() {
@Override
public void onNext(List<Monster> monsters) {
mMonsters.setValue(monsters);
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
});
}
public LiveData<List<Monster>> getMonsters() {
return mMonsters;
}
}

View File

@@ -0,0 +1,81 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
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;
public class EditAbilityScoresFragment extends Fragment {
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(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);
setTitle(getString(R.string.title_edit_ability_scores));
mViewModel.getStrength().observe(getViewLifecycleOwner(), value -> mHolder.strength.setValue(value));
mHolder.strength.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setStrength(newValue));
mHolder.strength.setOnFormatValueCallback(value -> String.format(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(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(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(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(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(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(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);
}
}
}

View File

@@ -0,0 +1,105 @@
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(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);
setTitle(getString(R.string.title_edit_armor));
mHolder.armorType.setAdapter(new ArrayAdapter<ArmorType>(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(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);
}
}
}

View File

@@ -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.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(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);
setTitle(getString(R.string.title_edit_basic_info));
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(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);
}
}
}

View File

@@ -0,0 +1,92 @@
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(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);
setTitle(getString(R.string.title_edit_challenge_rating));
mHolder.challengeRating.setAdapter(new ArrayAdapter<ChallengeRating>(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(View root) {
challengeRating = root.findViewById(R.id.challengeRating);
customChallengeRatingDescription = root.findViewById(R.id.customChallengeRatingDescription);
customProficiencyBonus = root.findViewById(R.id.customProficiencyBonus);
}
}
}

View File

@@ -0,0 +1,89 @@
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 @org.jetbrains.annotations.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);
setTitle(getString(R.string.title_edit_language));
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(View root) {
name = root.findViewById(R.id.name);
canSpeak = root.findViewById(R.id.canSpeak);
}
}
}

View File

@@ -0,0 +1,66 @@
package com.majinnaibu.monstercards.ui.editmonster;
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<String> mName;
private final ChangeTrackedLiveData<Boolean> mCanSpeak;
private final ChangeTrackedLiveData<Language> 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(Language language) {
mName.resetValue(language.getName());
mCanSpeak.resetValue(language.getSpeaks());
makeClean();
}
public LiveData<Language> getLanguage() {
return mLanguage;
}
public LiveData<String> getName() {
return mName;
}
public void setName(String name) {
mName.setValue(name);
mLanguage.setValue(makeLanguage());
}
public LiveData<Boolean> 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);
}
private Language makeLanguage() {
Boolean boxedValue = mCanSpeak.getValue();
boolean canSpeak = boxedValue != null && boxedValue;
return new Language(mName.getValue(), canSpeak);
}
}

View File

@@ -0,0 +1,102 @@
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(Language language) {
NavDirections action = EditLanguagesFragmentDirections.actionEditLanguagesFragmentToEditLanguageFragment(language.getName(), language.getSpeaks());
Navigation.findNavController(requireView()).navigate(action);
}
@Override
public View onCreateView(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);
setTitle(getString(R.string.title_edit_languages));
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(View root) {
this.list = root.findViewById(R.id.list);
this.addLanguage = root.findViewById(R.id.add_language);
}
}
}

View File

@@ -0,0 +1,110 @@
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.models.Language;
import com.majinnaibu.monstercards.ui.components.Stepper;
import com.majinnaibu.monstercards.utils.ItemCallback;
import java.util.List;
import java.util.Locale;
public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<Language> mValues;
private final ItemCallback<Language> 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<Language> items, ItemCallback<Language> 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(com.majinnaibu.monstercards.databinding.SimpleListItemBinding.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.onItem(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 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 com.majinnaibu.monstercards.databinding.SimpleListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}

View File

@@ -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_edit_monster, 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<Monster>() {
@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_edit_monster, 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(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);
}
}
}

View File

@@ -0,0 +1,95 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
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 Fragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mViewHolder;
@Override
public View onCreateView(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);
setTitle(getString(R.string.title_edit_saving_throws));
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(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);
}
}
}

View File

@@ -0,0 +1,100 @@
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);
setTitle(getString(R.string.title_edit_skill));
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(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);
}
}
}

View File

@@ -0,0 +1,78 @@
package com.majinnaibu.monstercards.ui.editmonster;
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<AbilityScore> mAbilityScore;
private final ChangeTrackedLiveData<AdvantageType> mAdvantageType;
private final ChangeTrackedLiveData<ProficiencyType> mProficiencyType;
private final ChangeTrackedLiveData<String> mName;
private final ChangeTrackedLiveData<Skill> 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<>("Unknown Skill", this::makeDirty);
mSkill = new ChangeTrackedLiveData<>(makeSkill(), this::makeDirty);
}
public void copyFromSkill(Skill skill) {
mAbilityScore.resetValue(skill.abilityScore);
mAdvantageType.resetValue(skill.advantageType);
mProficiencyType.resetValue(skill.proficiencyType);
mName.resetValue(skill.name);
}
public LiveData<Skill> getSkill() {
return mSkill;
}
public LiveData<AbilityScore> getAbilityScore() {
return mAbilityScore;
}
public void setAbilityScore(AbilityScore value) {
mAbilityScore.setValue(value);
mSkill.setValue(makeSkill());
}
public LiveData<AdvantageType> getAdvantage() {
return mAdvantageType;
}
public void setAdvantage(AdvantageType value) {
mAdvantageType.setValue(value);
mSkill.setValue(makeSkill());
}
public LiveData<ProficiencyType> getProficiency() {
return mProficiencyType;
}
public void setProficiency(ProficiencyType value) {
mProficiencyType.setValue(value);
mSkill.setValue(makeSkill());
}
public LiveData<String> getName() {
return mName;
}
public void setName(String value) {
mName.setValue(value);
mSkill.setValue(makeSkill());
}
private Skill makeSkill() {
return new Skill(mName.getValue(), mAbilityScore.getValue(), mAdvantageType.getValue(), mProficiencyType.getValue());
}
}

View File

@@ -0,0 +1,95 @@
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(Skill skill) {
NavDirections action = EditSkillsFragmentDirections.actionEditSkillsFragmentToEditSkillFragment(skill.name, skill.abilityScore, skill.proficiencyType, skill.advantageType);
View view = getView();
assert view != null;
Navigation.findNavController(view).navigate(action);
}
@Override
public View onCreateView(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);
setTitle(getString(R.string.title_edit_skills));
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(View root) {
this.list = root.findViewById(R.id.list);
this.addSkill = root.findViewById(R.id.add_skill);
}
}
}

View File

@@ -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.models.Skill;
import com.majinnaibu.monstercards.utils.ItemCallback;
import java.util.List;
/**
* {@link RecyclerView.Adapter} that can display a {@link Skill}.
*/
public class EditSkillsRecyclerViewAdapter extends RecyclerView.Adapter<EditSkillsRecyclerViewAdapter.ViewHolder> {
private final List<Skill> mValues;
private final ItemCallback<Skill> mOnClick;
public EditSkillsRecyclerViewAdapter(List<Skill> items, ItemCallback<Skill> onClick) {
mValues = items;
mOnClick = onClick;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(com.majinnaibu.monstercards.databinding.SimpleListItemBinding.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.onItem(holder.mItem);
}
});
}
@Override
public int getItemCount() {
return mValues.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView mContentView;
public Skill mItem;
public ViewHolder(@NonNull com.majinnaibu.monstercards.databinding.SimpleListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}

View File

@@ -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.EditText;
import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.Fragment;
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 Fragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
@Override
public View onCreateView(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);
setTitle(getString(R.string.title_edit_speed));
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(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);
}
}
}

View File

@@ -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 @org.jetbrains.annotations.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
@org.jetbrains.annotations.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;
}
private String getTitleForStringType(StringType type) {
switch (type) {
case CONDITION_IMMUNITY:
return getString(R.string.title_edit_condition_immunity);
case DAMAGE_IMMUNITY:
return getString(R.string.title_edit_damage_immunity);
case DAMAGE_RESISTANCE:
return getString(R.string.title_edit_damage_resistance);
case DAMAGE_VULNERABILITY:
return getString(R.string.title_edit_damage_vulnerability);
case SENSE:
return getString(R.string.title_edit_sense);
default:
return "";
}
}
@Override
public void onStart() {
super.onStart();
mHolder.description.requestFocus();
}
private static class ViewHolder {
EditText description;
ViewHolder(View root) {
description = root.findViewById(R.id.description);
}
}
}

View File

@@ -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<String> mValue;
public EditStringViewModel() {
super();
mValue = new ChangeTrackedLiveData<>("", this::makeDirty);
}
public LiveData<String> 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);
}
}

View File

@@ -0,0 +1,153 @@
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;
<<<<<<<< HEAD:Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditConditionImmunitiesFragment.java
import androidx.fragment.app.Fragment;
========
import androidx.lifecycle.LiveData;
>>>>>>>> f924bdd (Replaces condition immunities, damage immunities, damage resistances, damage vulnerabilities, and senses with a unified list of strings editor.):Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsFragment.java
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;
<<<<<<<< HEAD:Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditConditionImmunitiesFragment.java
========
import com.majinnaibu.monstercards.data.enums.StringType;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
>>>>>>>> f924bdd (Replaces condition immunities, damage immunities, damage resistances, damage vulnerabilities, and senses with a unified list of strings editor.):Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsFragment.java
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger;
import org.jetbrains.annotations.NotNull;
<<<<<<<< HEAD:Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditConditionImmunitiesFragment.java
/**
* A fragment representing a list of Items.
*/
public class EditConditionImmunitiesFragment extends Fragment {
========
import java.util.List;
public class EditStringsFragment extends MCFragment {
>>>>>>>> f924bdd (Replaces condition immunities, damage immunities, damage resistances, damage vulnerabilities, and senses with a unified list of strings editor.):Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsFragment.java
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
private StringType mStringType;
<<<<<<<< HEAD:Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditConditionImmunitiesFragment.java
private void navigateToEditConditionImmunity(String condition) {
NavDirections action = EditConditionImmunitiesFragmentDirections.actionEditConditionImmunitiesFragmentToEditConditionImmunity(condition);
View view = getView();
assert view != null;
Navigation.findNavController(view).navigate(action);
========
@Override
public void onCreate(@Nullable @org.jetbrains.annotations.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);
>>>>>>>> f924bdd (Replaces condition immunities, damage immunities, damage resistances, damage vulnerabilities, and senses with a unified list of strings editor.):Android/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditStringsFragment.java
}
@Nullable
@org.jetbrains.annotations.Nullable
@Override
public View onCreateView(@NonNull @NotNull LayoutInflater inflater, @Nullable @org.jetbrains.annotations.Nullable ViewGroup container, @Nullable @org.jetbrains.annotations.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;
}
private String getTitleForStringType(StringType type) {
switch (type) {
case CONDITION_IMMUNITY:
return getString(R.string.title_edit_condition_immunities);
case DAMAGE_IMMUNITY:
return getString(R.string.title_edit_damage_immunities);
case DAMAGE_RESISTANCE:
return getString(R.string.title_edit_damage_resistances);
case DAMAGE_VULNERABILITY:
return getString(R.string.title_edit_damage_vulnerabilities);
case SENSE:
return getString(R.string.title_edit_senses);
default:
return "";
}
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
Context context = requireContext();
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
LiveData<List<String>> 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(View root) {
list = root.findViewById(R.id.list);
addItem = root.findViewById(R.id.add_item);
}
}
}

View File

@@ -0,0 +1,61 @@
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.SimpleListItemBinding;
import com.majinnaibu.monstercards.utils.ItemCallback;
import java.util.List;
public class EditStringsRecyclerViewAdapter extends RecyclerView.Adapter<EditStringsRecyclerViewAdapter.ViewHolder> {
private final List<String> mValues;
private final ItemCallback<String> mOnClick;
public EditStringsRecyclerViewAdapter(List<String> items, ItemCallback<String> onClick) {
mValues = items;
mOnClick = onClick;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(SimpleListItemBinding.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.onItem(holder.mItem);
}
});
}
@Override
public int getItemCount() {
return mValues.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView mContentView;
public String mItem;
public ViewHolder(@NonNull SimpleListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}

View File

@@ -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 @org.jetbrains.annotations.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
@org.jetbrains.annotations.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_edit_ability);
case ACTION:
return getString(R.string.title_edit_action);
case LAIR_ACTION:
return getString(R.string.title_edit_lair_action);
case LEGENDARY_ACTION:
return getString(R.string.title_edit_legendary_action);
case REACTIONS:
return getString(R.string.title_edit_reaction);
case REGIONAL_ACTION:
return getString(R.string.title_edit_regional_action);
default:
return "";
}
}
private static class ViewHolder {
EditText description;
EditText name;
ViewHolder(View root) {
description = root.findViewById(R.id.description);
name = root.findViewById(R.id.name);
}
}
}

View File

@@ -0,0 +1,61 @@
package com.majinnaibu.monstercards.ui.editmonster;
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<String> mName;
private final ChangeTrackedLiveData<String> mDescription;
private final MutableLiveData<Trait> mAbility;
public EditTraitViewModel() {
super();
mName = new ChangeTrackedLiveData<>("", this::makeDirty);
mDescription = new ChangeTrackedLiveData<>("", this::makeDirty);
mAbility = new MutableLiveData<>(makeAbility());
}
public LiveData<String> getName() {
return mName;
}
public void setName(String name) {
mName.setValue(name);
mAbility.setValue(makeAbility());
}
public String getNameAsString() {
return mName.getValue();
}
public LiveData<String> 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(Trait trait) {
makeClean();
mName.resetValue(trait.name);
mDescription.resetValue(trait.description);
}
private Trait makeAbility() {
return new Trait(mName.getValue(), mDescription.getValue());
}
}

View File

@@ -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.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);
}
@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_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<List<Trait>> 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);
}
}
}

View File

@@ -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.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.databinding.SimpleListItemBinding;
import com.majinnaibu.monstercards.models.Trait;
import com.majinnaibu.monstercards.utils.ItemCallback;
public class EditTraitsRecyclerViewAdapter extends ListAdapter<Trait, EditTraitsRecyclerViewAdapter.ViewHolder> {
private static final DiffUtil.ItemCallback<Trait> DIFF_CALLBACK = new DiffUtil.ItemCallback<Trait>() {
@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<Trait> mOnClick;
protected EditTraitsRecyclerViewAdapter(ItemCallback<Trait> onClick) {
super(DIFF_CALLBACK);
mOnClick = onClick;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(SimpleListItemBinding.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.onItem(holder.mItem);
}
});
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView mContentView;
public Trait mItem;
public ViewHolder(@NonNull SimpleListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}

View File

@@ -0,0 +1,124 @@
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.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
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.databinding.FragmentLibraryBinding;
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.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class LibraryFragment extends MCFragment {
private LibraryViewModel mViewModel;
private ViewHolder mHolder;
private LibraryRecyclerViewAdapter mAdapter;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(this).get(LibraryViewModel.class);
FragmentLibraryBinding binding = FragmentLibraryBinding.inflate(inflater, container, false);
mHolder = new ViewHolder(binding);
// TODO: set the title with setTitle(...)
setupAddMonsterButton(mHolder.addButton);
setupMonsterList(mHolder.list);
return binding.getRoot();
}
private void setupMonsterList(@NonNull RecyclerView recyclerView) {
Context context = requireContext();
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
LiveData<List<Monster>> monsterData = mViewModel.getMonsters();
mAdapter = new LibraryRecyclerViewAdapter(this::navigateToMonsterDetail);
if (monsterData != null) {
monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
}
recyclerView.setAdapter(mAdapter);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(
requireContext(),
(position, direction) -> mViewModel.removeMonster(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))
.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(Monster monster) {
if (monster != null) {
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monster.id.toString());
Navigation.findNavController(requireView()).navigate(action);
} else {
Logger.logError("Can't navigate to MonsterDetail without a monster.");
}
}
private static class ViewHolder {
final FloatingActionButton addButton;
final RecyclerView list;
public ViewHolder(FragmentLibraryBinding binding) {
addButton = binding.fab;
list = binding.monsterList;
}
}
}

View File

@@ -0,0 +1,53 @@
package com.majinnaibu.monstercards.ui.library;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import com.majinnaibu.monstercards.databinding.SimpleListItemBinding;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.SimpleListItemViewHolder;
import com.majinnaibu.monstercards.utils.ItemCallback;
public class LibraryRecyclerViewAdapter extends ListAdapter<Monster, SimpleListItemViewHolder<Monster>> {
private static final DiffUtil.ItemCallback<Monster> DIFF_CALLBACK = new DiffUtil.ItemCallback<Monster>() {
@Override
public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Monster.areItemsTheSame(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Monster.areContentsTheSame(oldItem, newItem);
}
};
private final ItemCallback<Monster> mOnClick;
public LibraryRecyclerViewAdapter(ItemCallback<Monster> onClick) {
super(DIFF_CALLBACK);
mOnClick = onClick;
}
@Override
@NonNull
public SimpleListItemViewHolder<Monster> onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
SimpleListItemBinding binding = SimpleListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new SimpleListItemViewHolder<>(binding);
}
@Override
public void onBindViewHolder(final @NonNull SimpleListItemViewHolder<Monster> holder, int position) {
Monster monster = getItem(position);
holder.item = monster;
holder.contentView.setText(monster.name);
holder.itemView.setTag(monster);
holder.itemView.setOnClickListener(v -> {
if (mOnClick != null) {
mOnClick.onItem(holder.item);
}
});
}
}

View File

@@ -0,0 +1,74 @@
package com.majinnaibu.monstercards.ui.library;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.majinnaibu.monstercards.AppDatabase;
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.annotations.NonNull;
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subscribers.DisposableSubscriber;
public class LibraryViewModel extends AndroidViewModel {
private final AppDatabase mDB;
private final MutableLiveData<List<Monster>> mMonsters;
public LibraryViewModel(Application application) {
super(application);
mDB = AppDatabase.getInstance(application);
mMonsters = new MutableLiveData<>(new ArrayList<>());
mDB.monsterDAO()
.getAll()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new DisposableSubscriber<List<Monster>>() {
@Override
public void onNext(List<Monster> monsters) {
mMonsters.setValue(monsters);
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
});
}
public LiveData<List<Monster>> getMonsters() {
return mMonsters;
}
public void removeMonster(int position) {
Monster monster = mMonsters.getValue().get(position);
mDB.monsterDAO()
.delete(monster)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
}
@Override
public void onError(@NonNull Throwable e) {
}
});
}
}

View File

@@ -0,0 +1,259 @@
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.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.UUID;
import io.reactivex.rxjava3.observers.DisposableSingleObserver;
public class MonsterDetailFragment extends MCFragment {
private MonsterDetailViewModel monsterDetailViewModel;
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);
monsterDetailViewModel = new ViewModelProvider(this).get(MonsterDetailViewModel.class);
View root = inflater.inflate(R.layout.fragment_monster, container, false);
repository.getMonster(monsterId).toObservable()
.firstOrError()
.subscribe(new DisposableSingleObserver<Monster>() {
@Override
public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) {
monsterDetailViewModel.setMonster(monster);
dispose();
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError(e);
dispose();
}
});
final TextView monsterName = root.findViewById(R.id.name);
monsterDetailViewModel.getName().observe(getViewLifecycleOwner(), monsterName::setText);
final TextView monsterMeta = root.findViewById(R.id.meta);
monsterDetailViewModel.getMeta().observe(getViewLifecycleOwner(), monsterMeta::setText);
final TextView monsterArmorClass = root.findViewById(R.id.armor_class);
monsterDetailViewModel.getArmorClass().observe(getViewLifecycleOwner(), armorText -> monsterArmorClass.setText(Html.fromHtml("<b>Armor Class</b> " + armorText)));
final TextView monsterHitPoints = root.findViewById(R.id.hit_points);
monsterDetailViewModel.getHitPoints().observe(getViewLifecycleOwner(), hitPoints -> monsterHitPoints.setText(Html.fromHtml("<b>Hit Points</b> " + hitPoints)));
final TextView monsterSpeed = root.findViewById(R.id.speed);
monsterDetailViewModel.getSpeed().observe(getViewLifecycleOwner(), speed -> monsterSpeed.setText(Html.fromHtml("<b>Speed</b> " + speed)));
final TextView monsterStrength = root.findViewById(R.id.strength);
monsterDetailViewModel.getStrength().observe(getViewLifecycleOwner(), monsterStrength::setText);
final TextView monsterDexterity = root.findViewById(R.id.dexterity);
monsterDetailViewModel.getDexterity().observe(getViewLifecycleOwner(), monsterDexterity::setText);
final TextView monsterConstitution = root.findViewById(R.id.constitution);
monsterDetailViewModel.getConstitution().observe(getViewLifecycleOwner(), monsterConstitution::setText);
final TextView monsterIntelligence = root.findViewById(R.id.intelligence);
monsterDetailViewModel.getIntelligence().observe(getViewLifecycleOwner(), monsterIntelligence::setText);
final TextView monsterWisdom = root.findViewById(R.id.wisdom);
monsterDetailViewModel.getWisdom().observe(getViewLifecycleOwner(), monsterWisdom::setText);
final TextView monsterCharisma = root.findViewById(R.id.charisma);
monsterDetailViewModel.getCharisma().observe(getViewLifecycleOwner(), monsterCharisma::setText);
final TextView monsterSavingThrows = root.findViewById(R.id.saving_throws);
monsterDetailViewModel.getSavingThrows().observe(getViewLifecycleOwner(), savingThrows -> {
if (StringHelper.isNullOrEmpty(savingThrows)) {
monsterSavingThrows.setVisibility(View.GONE);
} else {
monsterSavingThrows.setVisibility(View.VISIBLE);
}
monsterSavingThrows.setText(Html.fromHtml("<b>Saving Throws</b> " + savingThrows));
});
final TextView monsterSkills = root.findViewById(R.id.skills);
monsterDetailViewModel.getSkills().observe(getViewLifecycleOwner(), skills -> {
if (StringHelper.isNullOrEmpty(skills)) {
monsterSkills.setVisibility(View.GONE);
} else {
monsterSkills.setVisibility(View.VISIBLE);
}
monsterSkills.setText(Html.fromHtml("<b>Skills</b> " + skills));
});
final TextView monsterDamageVulnerabilities = root.findViewById(R.id.damage_vulnerabilities);
monsterDetailViewModel.getDamageVulnerabilities().observe(getViewLifecycleOwner(), damageType -> {
if (StringHelper.isNullOrEmpty(damageType)) {
monsterDamageVulnerabilities.setVisibility(View.GONE);
} else {
monsterDamageVulnerabilities.setVisibility(View.VISIBLE);
}
monsterDamageVulnerabilities.setText(Html.fromHtml("<b>Damage Vulnerabilities</b> " + damageType));
});
final TextView monsterDamageResistances = root.findViewById(R.id.damage_resistances);
monsterDetailViewModel.getDamageResistances().observe(getViewLifecycleOwner(), damageType -> {
if (StringHelper.isNullOrEmpty(damageType)) {
monsterDamageResistances.setVisibility(View.GONE);
} else {
monsterDamageResistances.setVisibility(View.VISIBLE);
}
monsterDamageResistances.setText(Html.fromHtml("<b>Damage Resistances</b> " + damageType));
});
final TextView monsterDamageImmunities = root.findViewById(R.id.damage_immunities);
monsterDetailViewModel.getDamageImmunities().observe(getViewLifecycleOwner(), damageType -> {
if (StringHelper.isNullOrEmpty(damageType)) {
monsterDamageImmunities.setVisibility(View.GONE);
} else {
monsterDamageImmunities.setVisibility(View.VISIBLE);
}
monsterDamageImmunities.setText(Html.fromHtml("<b>Damage Immunities</b> " + damageType));
});
final TextView monsterConditionImmunities = root.findViewById(R.id.condition_immunities);
monsterDetailViewModel.getConditionImmunities().observe(getViewLifecycleOwner(), conditionImmunities -> {
if (StringHelper.isNullOrEmpty(conditionImmunities)) {
monsterConditionImmunities.setVisibility(View.GONE);
} else {
monsterConditionImmunities.setVisibility(View.VISIBLE);
}
monsterConditionImmunities.setText(Html.fromHtml("<b>Condition Immunities</b> " + conditionImmunities));
});
final TextView monsterSenses = root.findViewById(R.id.senses);
monsterDetailViewModel.getSenses().observe(getViewLifecycleOwner(), senses -> {
if (StringHelper.isNullOrEmpty(senses)) {
monsterSenses.setVisibility(View.GONE);
} else {
monsterSenses.setVisibility(View.VISIBLE);
}
monsterSenses.setText(Html.fromHtml("<b>Senses</b> " + senses));
});
final TextView monsterLanguages = root.findViewById(R.id.languages);
monsterDetailViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> {
if (StringHelper.isNullOrEmpty(languages)) {
monsterLanguages.setVisibility(View.GONE);
} else {
monsterLanguages.setVisibility(View.VISIBLE);
}
monsterLanguages.setText(Html.fromHtml("<b>Languages</b> " + languages));
});
final TextView monsterChallenge = root.findViewById(R.id.challenge);
monsterDetailViewModel.getChallenge().observe(getViewLifecycleOwner(), challengeRating -> monsterChallenge.setText(Html.fromHtml("<b>Challenge</b> " + challengeRating)));
final LinearLayout monsterAbilities = root.findViewById(R.id.abilities);
monsterDetailViewModel.getAbilities().observe(getViewLifecycleOwner(), abilities -> {
Context context = getContext();
DisplayMetrics displayMetrics = null;
if (context != null) {
Resources resources = context.getResources();
if (resources != null) {
displayMetrics = resources.getDisplayMetrics();
}
}
monsterAbilities.removeAllViews();
if (abilities != null) {
for (String ability : abilities) {
TextView tvAbility = new TextView(context);
// 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
Spanned spannedText = Html.fromHtml(CommonMarkHelper.toHtml(ability));
tvAbility.setText(spannedText);
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);
tvAbility.setLayoutParams(layoutParams);
monsterAbilities.addView(tvAbility);
}
}
});
final LinearLayout monsterActions = root.findViewById(R.id.actions);
monsterDetailViewModel.getActions().observe(getViewLifecycleOwner(), actions -> {
Context context = getContext();
DisplayMetrics displayMetrics = null;
if (context != null) {
Resources resources = context.getResources();
if (resources != null) {
displayMetrics = resources.getDisplayMetrics();
}
}
monsterActions.removeAllViews();
if (actions != null) {
for (String action : actions) {
TextView tvAction = new TextView(getContext());
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);
monsterActions.addView(tvAction);
}
}
});
// TODO: add lair actions, legendary actions, reactions, and regional actions
return root;
}
@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 = monsterDetailViewModel.getId().getValue();
if (monsterId != null) {
NavDirections action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString());
View view = getView();
assert view != null;
Navigation.findNavController(view).navigate(action);
} else {
Logger.logWTF("monsterId cannot be null.");
}
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,213 @@
package com.majinnaibu.monstercards.ui.monster;
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<List<String>> mAbilities;
private final MutableLiveData<List<String>> mActions;
private final MutableLiveData<String> mArmorClass;
private final MutableLiveData<String> mChallenge;
private final MutableLiveData<String> mCharisma;
private final MutableLiveData<String> mConditionImmunities;
private final MutableLiveData<String> mConstitution;
private final MutableLiveData<String> mDamageResistances;
private final MutableLiveData<String> mDamageImmunities;
private final MutableLiveData<String> mDamageVulnerabilities;
private final MutableLiveData<String> mDexterity;
private final MutableLiveData<String> mHitPoints;
private final MutableLiveData<String> mIntelligence;
private final MutableLiveData<List<String>> mLairActions;
private final MutableLiveData<String> mLanguages;
private final MutableLiveData<List<String>> mLegendaryActions;
private final MutableLiveData<String> mMeta;
private final MutableLiveData<String> mName;
private final MutableLiveData<List<String>> mReactions;
private final MutableLiveData<List<String>> mRegionalEffects;
private final MutableLiveData<String> mSavingThrows;
private final MutableLiveData<String> mSenses;
private final MutableLiveData<String> mSkills;
private final MutableLiveData<String> mSpeed;
private final MutableLiveData<String> mStrength;
private final MutableLiveData<String> mWisdom;
private final MutableLiveData<UUID> 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<List<String>> getAbilities() {
return mAbilities;
}
public LiveData<List<String>> getActions() {
return mActions;
}
public LiveData<List<String>> getReactions() {
return mReactions;
}
public LiveData<List<String>> getLegendaryActions() {
return mLegendaryActions;
}
public LiveData<List<String>> getLairActions() {
return mLairActions;
}
public LiveData<List<String>> getRegionalEffects() {
return mRegionalEffects;
}
public LiveData<String> getArmorClass() {
return mArmorClass;
}
public LiveData<String> getChallenge() {
return mChallenge;
}
public LiveData<String> getCharisma() {
return mCharisma;
}
public LiveData<String> getConditionImmunities() {
return mConditionImmunities;
}
public LiveData<String> getConstitution() {
return mConstitution;
}
public LiveData<String> getDamageResistances() {
return mDamageResistances;
}
public LiveData<String> getDamageImmunities() {
return mDamageImmunities;
}
public LiveData<String> getDamageVulnerabilities() {
return mDamageVulnerabilities;
}
public LiveData<String> getDexterity() {
return mDexterity;
}
public LiveData<String> getHitPoints() {
return mHitPoints;
}
public LiveData<String> getIntelligence() {
return mIntelligence;
}
public LiveData<String> getLanguages() {
return mLanguages;
}
public LiveData<String> getMeta() {
return mMeta;
}
public LiveData<String> getName() {
return mName;
}
public LiveData<String> getSavingThrows() {
return mSavingThrows;
}
public LiveData<String> getSenses() {
return mSenses;
}
public LiveData<String> getSkills() {
return mSkills;
}
public LiveData<String> getSpeed() {
return mSpeed;
}
public LiveData<String> getStrength() {
return mStrength;
}
public LiveData<String> getWisdom() {
return mWisdom;
}
public LiveData<UUID> getId() {
return mMonsterId;
}
public void setMonster(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());
}
}

View File

@@ -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(TextView view, String text, int titleId) {
String title = getString(titleId);
String fullText = String.format("<b>%s</b> %s", title, text);
view.setText(Html.fromHtml(fullText));
}
private void setupOptionalTextView(TextView root, String text, int titleId) {
String title = getString(titleId);
if (StringHelper.isNullOrEmpty(text)) {
root.setVisibility(View.GONE);
} else {
root.setVisibility(View.VISIBLE);
}
Spanned formatted;
if (StringHelper.isNullOrEmpty(title)) {
formatted = Html.fromHtml(text);
} else {
formatted = Html.fromHtml(String.format("<b>%s</b> %s", title, text));
}
root.setText(formatted);
}
private void setupTraitList(@NonNull LinearLayout root, @NonNull List<String> traits) {
setupTraitList(root, traits, null, null);
}
private void setupTraitList(@NonNull LinearLayout root, @NonNull List<String> traits, View label, View divider) {
int visibility = traits.size() > 0 ? View.VISIBLE : View.GONE;
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(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(View root) {
this.root = root;
name = root.findViewById(R.id.name);
meta = root.findViewById(R.id.meta);
armorClass = root.findViewById(R.id.armorClass);
hitPoints = root.findViewById(R.id.hitPoints);
speed = root.findViewById(R.id.speed);
strength = root.findViewById(R.id.strength);
dexterity = root.findViewById(R.id.dexterity);
constitution = root.findViewById(R.id.constitution);
intelligence = root.findViewById(R.id.intelligence);
wisdom = root.findViewById(R.id.wisdom);
charisma = root.findViewById(R.id.charisma);
savingThrows = root.findViewById(R.id.savingThrows);
skills = root.findViewById(R.id.skills);
damageVulnerabilities = root.findViewById(R.id.damageVulnerabilities);
damageResistances = root.findViewById(R.id.damageResistances);
damageImmunities = root.findViewById(R.id.damageImmunities);
conditionImmunities = root.findViewById(R.id.conditionImmunities);
senses = root.findViewById(R.id.senses);
languages = root.findViewById(R.id.languages);
challenge = root.findViewById(R.id.challenge);
abilities = root.findViewById(R.id.abilities);
actions = root.findViewById(R.id.actions);
actions_divider = root.findViewById(R.id.actions_divider);
actions_label = root.findViewById(R.id.actions_label);
reactions = root.findViewById(R.id.reactions);
reactions_divider = root.findViewById(R.id.reactions_divider);
reactions_label = root.findViewById(R.id.reactions_label);
legendaryActions = root.findViewById(R.id.legendaryActions);
legendaryActions_divider = root.findViewById(R.id.legendaryActions_divider);
legendaryActions_label = root.findViewById(R.id.legendaryActions_label);
lairActions = root.findViewById(R.id.lairActions);
lairActions_divider = root.findViewById(R.id.lairActions_divider);
lairActions_label = root.findViewById(R.id.lairActions_label);
regionalEffects = root.findViewById(R.id.regionalEffects);
regionalEffects_divider = root.findViewById(R.id.regionalEffects_divider);
regionalEffects_label = root.findViewById(R.id.regionalEffects_label);
}
}
}

View File

@@ -0,0 +1,216 @@
package com.majinnaibu.monstercards.ui.monster;
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<List<String>> mAbilities;
private final MutableLiveData<List<String>> mActions;
private final MutableLiveData<String> mArmorClass;
private final MutableLiveData<String> mChallenge;
private final MutableLiveData<String> mCharisma;
private final MutableLiveData<String> mConditionImmunities;
private final MutableLiveData<String> mConstitution;
private final MutableLiveData<String> mDamageResistances;
private final MutableLiveData<String> mDamageImmunities;
private final MutableLiveData<String> mDamageVulnerabilities;
private final MutableLiveData<String> mDexterity;
private final MutableLiveData<String> mHitPoints;
private final MutableLiveData<String> mIntelligence;
private final MutableLiveData<List<String>> mLairActions;
private final MutableLiveData<String> mLanguages;
private final MutableLiveData<List<String>> mLegendaryActions;
private final MutableLiveData<String> mMeta;
private final MutableLiveData<String> mName;
private final MutableLiveData<List<String>> mReactions;
private final MutableLiveData<List<String>> mRegionalEffects;
private final MutableLiveData<String> mSavingThrows;
private final MutableLiveData<String> mSenses;
private final MutableLiveData<String> mSkills;
private final MutableLiveData<String> mSpeed;
private final MutableLiveData<String> mStrength;
private final MutableLiveData<String> mWisdom;
private final MutableLiveData<UUID> 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<List<String>> getAbilities() {
return mAbilities;
}
public LiveData<List<String>> getActions() {
return mActions;
}
public LiveData<List<String>> getReactions() {
return mReactions;
}
public LiveData<List<String>> getLegendaryActions() {
return mLegendaryActions;
}
public LiveData<List<String>> getLairActions() {
return mLairActions;
}
public LiveData<List<String>> getRegionalEffects() {
return mRegionalEffects;
}
public LiveData<String> getArmorClass() {
return mArmorClass;
}
public LiveData<String> getChallenge() {
return mChallenge;
}
public LiveData<String> getCharisma() {
return mCharisma;
}
public LiveData<String> getConditionImmunities() {
return mConditionImmunities;
}
public LiveData<String> getConstitution() {
return mConstitution;
}
public LiveData<String> getDamageResistances() {
return mDamageResistances;
}
public LiveData<String> getDamageImmunities() {
return mDamageImmunities;
}
public LiveData<String> getDamageVulnerabilities() {
return mDamageVulnerabilities;
}
public LiveData<String> getDexterity() {
return mDexterity;
}
public LiveData<String> getHitPoints() {
return mHitPoints;
}
public LiveData<String> getIntelligence() {
return mIntelligence;
}
public LiveData<String> getLanguages() {
return mLanguages;
}
public LiveData<String> getMeta() {
return mMeta;
}
public LiveData<String> getName() {
return mName;
}
public LiveData<String> getSavingThrows() {
return mSavingThrows;
}
public LiveData<String> getSenses() {
return mSenses;
}
public LiveData<String> getSkills() {
return mSkills;
}
public LiveData<String> getSpeed() {
return mSpeed;
}
public LiveData<String> getStrength() {
return mStrength;
}
public LiveData<String> getWisdom() {
return mWisdom;
}
public LiveData<UUID> getId() {
return mMonsterId;
}
public Monster getMonster() {
return mMonster;
}
public void setMonster(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());
}
}

View File

@@ -0,0 +1,95 @@
package com.majinnaibu.monstercards.ui.search;
import android.content.Context;
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.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.databinding.FragmentSearchBinding;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.List;
public class SearchFragment extends MCFragment {
private SearchViewModel mViewModel;
private ViewHolder mHolder;
private SearchResultsRecyclerViewAdapter mAdapter;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(this).get(SearchViewModel.class);
FragmentSearchBinding binding = FragmentSearchBinding.inflate(inflater, container, false);
mHolder = new ViewHolder(binding);
// TODO: set the title with setTitle(...)
setupMonsterList(binding.monsterList);
setupFilterBox(binding.searchQuery);
return binding.getRoot();
}
private void setupFilterBox(@NonNull TextView textBox) {
textBox.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) {
mViewModel.setFilterText(textBox.getText().toString());
}
});
}
private void setupMonsterList(@NonNull RecyclerView recyclerView) {
Context context = requireContext();
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
LiveData<List<Monster>> monsterData = mViewModel.getMatchedMonsters();
mAdapter = new SearchResultsRecyclerViewAdapter(this::navigateToMonsterDetail);
if (monsterData != null) {
monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
}
recyclerView.setAdapter(mAdapter);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
}
public void navigateToMonsterDetail(Monster monster) {
if (monster == null) {
NavDirections action = SearchFragmentDirections.actionNavigationSearchToNavigationMonster(monster.id.toString());
Navigation.findNavController(requireView()).navigate(action);
} else {
Logger.logError("Can't navigate to MonsterDetail without a monster.");
}
}
private static class ViewHolder {
final RecyclerView monsterList;
final EditText filterQuery;
public ViewHolder(FragmentSearchBinding binding) {
monsterList = binding.monsterList;
filterQuery = binding.searchQuery;
}
}
}

View File

@@ -0,0 +1,52 @@
package com.majinnaibu.monstercards.ui.search;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import com.majinnaibu.monstercards.databinding.SimpleListItemBinding;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.SimpleListItemViewHolder;
import com.majinnaibu.monstercards.utils.ItemCallback;
public class SearchResultsRecyclerViewAdapter extends ListAdapter<Monster, SimpleListItemViewHolder<Monster>> {
private static final DiffUtil.ItemCallback<Monster> DIFF_CALLBACK = new DiffUtil.ItemCallback<Monster>() {
@Override
public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Monster.areItemsTheSame(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Monster.areContentsTheSame(oldItem, newItem);
}
};
private final ItemCallback<Monster> mOnClick;
public SearchResultsRecyclerViewAdapter(ItemCallback<Monster> onClick) {
super(DIFF_CALLBACK);
mOnClick = onClick;
}
@NonNull
@Override
public SimpleListItemViewHolder<Monster> onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
SimpleListItemBinding binding = SimpleListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new SimpleListItemViewHolder<>(binding);
}
@Override
public void onBindViewHolder(@NonNull final SimpleListItemViewHolder<Monster> holder, int position) {
Monster monster = getItem(position);
holder.item = monster;
holder.contentView.setText(monster.name);
holder.itemView.setOnClickListener(view -> {
if (mOnClick != null) {
mOnClick.onItem(holder.item);
}
});
}
}

View File

@@ -0,0 +1,118 @@
package com.majinnaibu.monstercards.ui.search;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import com.majinnaibu.monstercards.AppDatabase;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subscribers.DisposableSubscriber;
public class SearchViewModel extends AndroidViewModel {
private final MutableLiveData<List<Monster>> mAllMonsters;
private final MediatorLiveData<List<Monster>> mFilteredMonsters;
private final MutableLiveData<String> mFilterText;
private final AppDatabase mDB;
public SearchViewModel(Application application) {
super(application);
mDB = AppDatabase.getInstance(application);
mAllMonsters = new MutableLiveData<>(new ArrayList<>());
mFilterText = new MutableLiveData<>("");
mFilteredMonsters = new MediatorLiveData<>();
mFilteredMonsters.addSource(
mAllMonsters,
allMonsters -> mFilteredMonsters.setValue(
filterMonsters(allMonsters, mFilterText.getValue())));
mFilteredMonsters.addSource(
mFilterText,
filterText -> mFilteredMonsters.setValue(
filterMonsters(mAllMonsters.getValue(), filterText)));
mDB.monsterDAO()
.getAll()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new DisposableSubscriber<List<Monster>>() {
@Override
public void onNext(List<Monster> monsters) {
mAllMonsters.setValue(monsters);
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
});
}
private boolean monsterMatchesFilter(Monster monster, String filterText) {
if (StringHelper.isNullOrEmpty(filterText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.name, filterText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.size, filterText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.type, filterText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.subtype, filterText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.alignment, filterText)) {
return true;
}
return false;
}
private List<Monster> filterMonsters(List<Monster> allMonsters, String filterText) {
ArrayList<Monster> filteredMonsters = new ArrayList<>();
filterText = filterText.toLowerCase(Locale.ROOT);
if (allMonsters != null) {
for (Monster monster : allMonsters) {
// TODO: do the filtering like the iOS app does.
Logger.logUnimplementedFeature("do the filtering like the iOS app does");
// TODO: consider splitting search text into words and if each word appears in any of these fields return true e.g, "large demon" would match large in size and demon in type.
// TODO: add tags and search by tags
// TODO: add a display of what fields matched on each item in the results
// TODO: make the criteria configurable from this screen
// TODO: find a way to add challenge rating as a search criteria
if (monsterMatchesFilter(monster, filterText)) {
filteredMonsters.add(monster);
}
}
}
return filteredMonsters;
}
public LiveData<List<Monster>> getMatchedMonsters() {
return mFilteredMonsters;
}
public void setFilterText(String filterText) {
mFilterText.setValue(filterText);
}
}

View File

@@ -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<Boolean> 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);
}
}

Some files were not shown because too many files have changed in this diff Show More