Compare commits

..

230 Commits

Author SHA1 Message Date
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
414 changed files with 2563 additions and 43672 deletions

7
.gitignore vendored
View File

@@ -1,10 +1,3 @@
# OS / System
.DS_Store
._*
thumbs.db
# Build & Tooling
.artifacts/
.worktrees/
.build/

18
.idea/.gitignore generated vendored
View File

@@ -1,18 +0,0 @@
# User-specific and local IDE state files
/workspace.xml
/shelf/
/caches/
/deviceManager.xml
/deploymentTargetSelector.xml
/appInsightsSettings.xml
/planningMode.xml
/studiobot.xml
/markdown.xml
/assetWizardSettings.xml
/usage.statistics.xml
/tasks.xml
# IntelliJ module files generated by Gradle
*.iml
*.iws
/modules.xml

1
.idea/.name generated
View File

@@ -1 +0,0 @@
MonsterCards

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
.idea/compiler.xml generated
View File

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

18
.idea/gradle.xml generated
View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$/Android" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/Android" />
<option value="$PROJECT_DIR$/Android/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

60
.idea/misc.xml generated
View File

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
<option name="myOrdered" value="true" />
<option name="myNullables">
<value>
<list size="17">
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="1" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
<item index="2" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="13" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="14" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
<item index="15" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="16" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="16">
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="1" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
<item index="2" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="13" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="14" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
<item index="15" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@@ -1,68 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false" focusToolWindowBeforeRun="true">
<module name="MonsterCards.app" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="RESTORE_FRESH_INSTALL_ONLY" value="false" />
<option name="CLEAR_LOGCAT" value="true" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="LeakCanary" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

6
.idea/vcs.xml generated
View File

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

28
Android/.gitignore vendored
View File

@@ -1,15 +1,17 @@
# Build outputs
build/
app/build/
.externalNativeBuild/
.cxx/
captures/
local.properties
*.iml
# Android / Gradle caches
.gradle/
.artifacts/
# System
.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

18
Android/.idea/.gitignore generated vendored
View File

@@ -1,18 +0,0 @@
# User-specific and local IDE state files
/workspace.xml
/shelf/
/caches/
/deviceManager.xml
/deploymentTargetSelector.xml
/appInsightsSettings.xml
/planningMode.xml
/studiobot.xml
/markdown.xml
/assetWizardSettings.xml
/usage.statistics.xml
/tasks.xml
# IntelliJ module files generated by Gradle
*.iml
*.iws
/modules.xml

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Android vitals">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="com.planbgamestudios.downtosize" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="SEVEN_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

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

View File

@@ -1,17 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<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>

View File

@@ -21,10 +21,5 @@
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

53
Android/.idea/misc.xml generated
View File

@@ -1,57 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
<option name="myOrdered" value="true" />
<option name="myNullables">
<value>
<list size="17">
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="1" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
<item index="2" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="13" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="14" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
<item index="15" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="16" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="16">
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="1" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
<item index="2" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="13" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="14" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
<item index="15" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -3,14 +3,6 @@
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
<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" />

View File

@@ -1,68 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false" focusToolWindowBeforeRun="true">
<module name="MonsterCards.app" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="RESTORE_FRESH_INSTALL_ONLY" value="false" />
<option name="CLEAR_LOGCAT" value="true" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="LeakCanary" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -9,7 +9,6 @@
<option value="noActionIfCompileErrors" />
<option value="organizeImports" />
<option value="reformat" />
<option value="rearrange" />
</set>
</option>
<option name="configurationPath" value="" />

2
Android/.idea/vcs.xml generated
View File

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

View File

@@ -11,16 +11,22 @@ if (propertiesFile.exists()) {
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 {
namespace "com.majinnaibu.monstercards"
compileSdk 37
compileSdkVersion 30
buildToolsVersion '30.0.2'
defaultConfig {
applicationId "com.majinnaibu.monstercards"
minSdkVersion 24
//noinspection OldTargetApi
targetSdk 36
minSdkVersion 22
targetSdkVersion 30
versionCode 1
versionName "1.0"
buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\""
@@ -51,18 +57,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
buildConfig true
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
@@ -71,35 +70,40 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
// Google
implementation 'androidx.appcompat:appcompat:1.8.0'
implementation 'com.google.android.material:material:1.14.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.2'
implementation "androidx.navigation:navigation-fragment:2.10.1"
implementation "androidx.navigation:navigation-ui:2.10.1"
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.4.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.11.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.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.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
// Room DB
implementation "io.reactivex.rxjava3:rxjava:3.1.12"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
implementation "androidx.room:room-runtime:2.8.5"
annotationProcessor "androidx.room:room-compiler:2.8.5"
implementation "androidx.room:room-rxjava3:2.8.5"
//testImplementation "androidx.room:room-testing:2.8.5"
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:5.0.6"
debugImplementation "com.microsoft.appcenter:appcenter-crashes:5.0.6"
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.17.0'
implementation "com.google.code.gson:gson:2.14.0"
implementation 'com.atlassian.commonmark:commonmark:0.15.2'
implementation "com.google.code.gson:gson:$gson_version"
}

View File

@@ -1,554 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "ca0ad042eed557daa5ff0ada5de6596c",
"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",
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"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",
"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",
"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"
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"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",
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"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"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"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": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "collection_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collection_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_monsters_collection_id",
"unique": false,
"columnNames": [
"collection_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
},
{
"name": "index_collection_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "collections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collection_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"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, 'ca0ad042eed557daa5ff0ada5de6596c')"
]
}
}

View File

@@ -1,609 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "6d497d0f0530c0f46bed1d57492e940e",
"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",
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"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",
"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",
"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"
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"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",
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"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"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"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": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "collection_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collection_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_monsters_collection_id",
"unique": false,
"columnNames": [
"collection_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
},
{
"name": "index_collection_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "collections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collection_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "dashboard_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_dashboard_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"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, '6d497d0f0530c0f46bed1d57492e940e')"
]
}
}

View File

@@ -1,663 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "0ab331e246de46304f847d7ed2d10124",
"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 '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` 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",
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"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",
"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",
"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"
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"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",
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "sourceUrl",
"columnName": "source_url",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "bonusActions",
"columnName": "bonus_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "mythicActions",
"columnName": "mythic_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActionsDescription",
"columnName": "legendary_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsDescription",
"columnName": "lair_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsEndNote",
"columnName": "lair_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsDescription",
"columnName": "regional_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsEndNote",
"columnName": "regional_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "mythicActionsDescription",
"columnName": "mythic_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"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"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"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": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "collection_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collection_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_monsters_collection_id",
"unique": false,
"columnNames": [
"collection_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
},
{
"name": "index_collection_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "collections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collection_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "dashboard_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_dashboard_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"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, '0ab331e246de46304f847d7ed2d10124')"
]
}
}

View File

@@ -1,768 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "38f6bbd6f1e298b20e17b3a08fbc7f70",
"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 '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL 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",
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"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",
"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",
"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"
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"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",
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "sourceUrl",
"columnName": "source_url",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "bonusActions",
"columnName": "bonus_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "mythicActions",
"columnName": "mythic_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActionsDescription",
"columnName": "legendary_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsDescription",
"columnName": "lair_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsEndNote",
"columnName": "lair_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsDescription",
"columnName": "regional_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsEndNote",
"columnName": "regional_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "mythicActionsDescription",
"columnName": "mythic_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "playerName",
"columnName": "player_name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "background",
"columnName": "background",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "personalityTraits",
"columnName": "personality_traits",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "ideals",
"columnName": "ideals",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "bonds",
"columnName": "bonds",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "flaws",
"columnName": "flaws",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "age",
"columnName": "age",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "weight",
"columnName": "weight",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "eyes",
"columnName": "eyes",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "skin",
"columnName": "skin",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "hair",
"columnName": "hair",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "appearance",
"columnName": "appearance",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "backstory",
"columnName": "backstory",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alliesAndOrganizations",
"columnName": "allies_and_organizations",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"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"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"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": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "collection_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collection_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_monsters_collection_id",
"unique": false,
"columnNames": [
"collection_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
},
{
"name": "index_collection_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "collections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collection_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "dashboard_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_dashboard_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"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, '38f6bbd6f1e298b20e17b3a08fbc7f70')"
]
}
}

View File

@@ -1,768 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "38f6bbd6f1e298b20e17b3a08fbc7f70",
"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 '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL 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",
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"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",
"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",
"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"
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"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",
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "sourceUrl",
"columnName": "source_url",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "bonusActions",
"columnName": "bonus_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "mythicActions",
"columnName": "mythic_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActionsDescription",
"columnName": "legendary_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsDescription",
"columnName": "lair_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsEndNote",
"columnName": "lair_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsDescription",
"columnName": "regional_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsEndNote",
"columnName": "regional_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "mythicActionsDescription",
"columnName": "mythic_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "playerName",
"columnName": "player_name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "background",
"columnName": "background",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "personalityTraits",
"columnName": "personality_traits",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "ideals",
"columnName": "ideals",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "bonds",
"columnName": "bonds",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "flaws",
"columnName": "flaws",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "age",
"columnName": "age",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "weight",
"columnName": "weight",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "eyes",
"columnName": "eyes",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "skin",
"columnName": "skin",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "hair",
"columnName": "hair",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "appearance",
"columnName": "appearance",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "backstory",
"columnName": "backstory",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alliesAndOrganizations",
"columnName": "allies_and_organizations",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"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"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"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": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "collection_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collection_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_monsters_collection_id",
"unique": false,
"columnNames": [
"collection_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
},
{
"name": "index_collection_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "collections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collection_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "dashboard_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_dashboard_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"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, '38f6bbd6f1e298b20e17b3a08fbc7f70')"
]
}
}

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

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.majinnaibu.monstercards">
<uses-permission android:name="android.permission.INTERNET" />
@@ -10,90 +11,46 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:intentMatchingFlags="enforceIntentFilter"
android:theme="@style/AppTheme">
<activity
android:name=".SplashActivity"
android:theme="@style/Theme.App.Starting"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<!-- Intent filter for viewing/opening content:// URIs with binary/octet-stream MIME type (.monster) -->
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Intent filter for file:// URIs ending in .monster, .monster.txt, .card, or .card.txt -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" android:host="*" />
<data android:pathPattern=".*\\.monster" />
<data android:pathPattern=".*\\.monster\\.txt" />
<data android:pathPattern=".*\\.card" />
<data android:pathPattern=".*\\.card\\.txt" />
<data android:pathPattern=".*\\.binder" />
<data android:pathPattern=".*\\.binder\\.txt" />
</intent-filter>
<!-- Intent filter for explicitly sharing files/text to Monster Cards -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
<!-- Intent filter for viewing dndbeyond.com character URLs directly in Monster Cards -->
<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" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="www.dndbeyond.com" />
<data android:host="dndbeyond.com" />
<data android:pathPrefix="/characters/" />
<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>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.majinnaibu.monstercards.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -1,37 +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.CollectionDAO;
import com.majinnaibu.monstercards.data.DashboardDAO;
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.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.MonsterFTS;
@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class, DashboardMonster.class}, version = 8)
@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 {
public abstract MonsterDAO monsterDAO();
public abstract CollectionDAO collectionDAO();
public abstract DashboardDAO dashboardDAO();
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

@@ -1,66 +1,32 @@
package com.majinnaibu.monstercards;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
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.appbar.MaterialToolbar;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.snackbar.Snackbar;
import com.google.gson.Gson;
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.importers.BinderImporter;
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.importers.GitRepoImporterService;
import com.majinnaibu.monstercards.importers.Open5eApiWrapper;
import com.majinnaibu.monstercards.init.AppCenterInitializer;
import com.majinnaibu.monstercards.models.BinderExport;
import com.majinnaibu.monstercards.models.ImportSource;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.init.FlipperInitializer;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import com.majinnaibu.monstercards.utils.ToastHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import java.util.concurrent.Callable;
public class MainActivity extends AppCompatActivity {
private Disposable mImportDisposable;
private ImportSource mCurrentImportSource;
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
@@ -74,33 +40,10 @@ public class MainActivity extends AppCompatActivity {
@SuppressWarnings("ConstantConditions")
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
super.onCreate(savedInstanceState);
AppCenterInitializer.init(getApplication());
setContentView(R.layout.activity_main);
MaterialToolbar toolbar = findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
}
View appBarLayout = findViewById(R.id.app_bar_layout);
BottomNavigationView navView = findViewById(R.id.nav_view);
View container = findViewById(R.id.container);
if (container != null) {
ViewCompat.setOnApplyWindowInsetsListener(container, (v, windowInsets) -> {
Insets systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
if (appBarLayout != null) {
appBarLayout.setPadding(0, systemBars.top, 0, 0);
}
if (navView != null) {
navView.setPadding(0, 0, 0, systemBars.bottom);
}
return windowInsets;
});
}
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
@@ -111,6 +54,9 @@ public class MainActivity extends AppCompatActivity {
.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());
@@ -120,338 +66,38 @@ public class MainActivity extends AppCompatActivity {
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent == null) {
return;
}
String action = intent.getAction();
Bundle extras = intent.getExtras();
if ("android.intent.action.SEND".equals(action) && extras != null) {
CharSequence sharedText = extras.getCharSequence(Intent.EXTRA_TEXT);
if (sharedText != null) {
String textStr = sharedText.toString().trim();
DnDBeyondImporter dndImporter = new DnDBeyondImporter();
if (dndImporter.canImport(textStr)) {
importMonsterFromInputAndNavigate(textStr);
return;
}
}
} else if ("android.intent.action.VIEW".equals(action)) {
Uri dataUri = intent.getData();
if (dataUri != null) {
String dataStr = dataUri.toString().trim();
DnDBeyondImporter dndImporter = new DnDBeyondImporter();
if (dndImporter.canImport(dataStr)) {
importMonsterFromInputAndNavigate(dataStr);
return;
}
}
}
Uri intentUri = getUriFromIntent(intent);
String fileName = intentUri != null ? getFileNameFromUri(intentUri) : null;
String json = readMonsterJSONFromIntent(intent);
if (!StringHelper.isNullOrEmpty(json)) {
importMonsterFromInputAndNavigate(json, fileName);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
NavDirections action = MobileNavigationDirections.actionGlobalMonsterImportFragment(json);
navController.navigate(action);
}
}
@Nullable
private Uri getUriFromIntent(@NonNull Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
if ("android.intent.action.SEND".equals(action)) {
if (extras != null) {
return extras.getParcelable(Intent.EXTRA_STREAM);
}
} else if ("android.intent.action.VIEW".equals(action) || "android.intent.action.EDIT".equals(action)) {
return intent.getData();
}
return null;
}
public void importMonsterFromInputAndNavigate(@NonNull String input) {
importMonsterFromInputAndNavigate(input, null);
}
public void importMonsterFromInputAndNavigate(@NonNull String input, @Nullable String fileName) {
try {
BinderImporter binderImporter = new BinderImporter();
if (binderImporter.canImport(input)) {
ToastHelper.showShort(this, R.string.toast_importing_url);
Single.fromCallable(() -> binderImporter.parse(input))
.flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
ToastHelper.showLong(this, R.string.snackbar_import_binder_success);
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
NavController navController = navHostFragment.getNavController();
navController.navigate(R.id.navigation_library);
}, throwable -> {
Logger.logError("Failed to import binder from input: " + fileName, throwable);
ToastHelper.showLong(this, R.string.failed_to_import_url);
});
return;
}
ToastHelper.showShort(this, R.string.toast_importing_url);
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input, fileName))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monster -> {
String serializedJson = new Gson().toJson(monster);
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
NavController navController = navHostFragment.getNavController();
NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson);
navController.navigate(navAction);
}, throwable -> {
Logger.logError("Failed to import monster from input: " + fileName, throwable);
ToastHelper.showLong(this, R.string.failed_to_import_url);
});
} catch (Exception e) {
Logger.logError("Failed to process import input: " + fileName, e);
ToastHelper.showLong(this, R.string.failed_to_import_url);
}
}
public void startImportFromSource(ImportSource source) {
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
new AlertDialog.Builder(this)
.setTitle(R.string.dialog_import_already_running_title)
.setMessage(getString(R.string.dialog_import_already_running_message, mCurrentImportSource != null ? mCurrentImportSource.projectName : ""))
.setPositiveButton(R.string.action_cancel_current, (dialog, which) -> {
mImportDisposable.dispose();
executeImportFromSource(source);
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
return;
}
executeImportFromSource(source);
}
private void executeImportFromSource(ImportSource source) {
mCurrentImportSource = source;
View rootView = findViewById(android.R.id.content);
Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, getString(R.string.snackbar_importing_source, source.projectName));
snackbar.setAction(R.string.action_cancel, v -> {
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
mImportDisposable.dispose();
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_cancelled, source.projectName));
}
});
snackbar.show();
Callable<Integer> importTask;
if (source.importType == ImportSource.ImportType.OPEN5E_API) {
importTask = () -> {
int totalImported = 0;
String nextUrl = null;
do {
if (mImportDisposable != null && mImportDisposable.isDisposed()) {
throw new InterruptedException("Open5e import was cancelled");
}
Open5eApiWrapper.Open5ePageResult page = Open5eApiWrapper.fetchPage(nextUrl, () -> mImportDisposable != null && mImportDisposable.isDisposed());
for (Monster monster : page.monsters) {
if (mImportDisposable != null && mImportDisposable.isDisposed()) {
throw new InterruptedException("Open5e import was cancelled");
}
try {
((MonsterCardsApplication) getApplication()).getMonsterRepository()
.saveMonster(monster)
.blockingAwait();
totalImported++;
} catch (Exception e) {
Logger.logError("Failed to save monster to database", e);
runOnUiThread(() -> ToastHelper.showShort(MainActivity.this, "An error occurred while saving a monster."));
}
}
nextUrl = page.nextUrl;
} while (nextUrl != null && !nextUrl.isEmpty());
return totalImported;
};
} else {
importTask = () -> {
try {
return GitRepoImporterService.importFromGitRepository(getApplicationContext(), source, () -> mImportDisposable != null && mImportDisposable.isDisposed());
} catch (InterruptedException e) {
Logger.logError("Import task was interrupted", e);
return 0; // Return gracefully on interruption
}
};
}
mImportDisposable = Single.fromCallable(importTask)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(resultCount -> {
snackbar.dismiss();
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_complete, resultCount, source.projectName));
}, throwable -> {
snackbar.dismiss();
if (throwable instanceof InterruptedException) {
Logger.logError("Import task was cancelled: " + source.projectName, throwable);
// The user cancelled via UI, message handled in the snackbar action.
} else {
Logger.logError("Failed to import from source: " + source.projectName, throwable);
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_failed, source.projectName));
}
});
}
public void importMultipleFilesFromUris(@NonNull List<Uri> uris) {
if (uris.isEmpty()) return;
if (uris.size() == 1) {
Uri singleUri = uris.get(0);
String fileName = getFileNameFromUri(singleUri);
String content = readContentsOfUri(singleUri);
if (content != null && !content.trim().isEmpty()) {
importMonsterFromInputAndNavigate(content, fileName);
} else {
ToastHelper.showLong(this, R.string.failed_to_import_url);
}
return;
}
ToastHelper.showShort(this, R.string.toast_importing_url);
Single.fromCallable(() -> {
int monstersImported = 0;
int bindersImported = 0;
for (Uri uri : uris) {
try {
String fileName = getFileNameFromUri(uri);
String content = readContentsOfUri(uri);
if (content == null || content.trim().isEmpty()) {
continue;
}
BinderImporter binderImporter = new BinderImporter();
if (binderImporter.canImport(content)) {
BinderExport binder = binderImporter.parse(content);
((MonsterCardsApplication) getApplication()).getMonsterRepository()
.importBinder(binder)
.blockingAwait();
bindersImported++;
} else {
Monster monster = MonsterImportHelper.fromJSON(content, fileName);
((MonsterCardsApplication) getApplication()).getMonsterRepository()
.saveMonster(monster)
.blockingAwait();
monstersImported++;
}
} catch (Exception e) {
Logger.logError("Failed to import file URI: " + uri, e);
}
}
return new BatchImportResult(monstersImported, bindersImported);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result.bindersImported > 0 || result.monstersImported > 0) {
String message = buildImportSummaryMessage(result.monstersImported, result.bindersImported);
ToastHelper.showLong(this, message);
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
NavController navController = navHostFragment.getNavController();
navController.navigate(R.id.navigation_library);
} else {
ToastHelper.showLong(this, R.string.failed_to_import_url);
}
}, throwable -> {
Logger.logError("Failed to execute batch file import", throwable);
ToastHelper.showLong(this, R.string.failed_to_import_url);
});
}
private String buildImportSummaryMessage(int monstersCount, int bindersCount) {
StringBuilder sb = new StringBuilder("Successfully imported ");
if (bindersCount > 0 && monstersCount > 0) {
sb.append(bindersCount).append(bindersCount == 1 ? " collection" : " collections")
.append(" and ")
.append(monstersCount).append(monstersCount == 1 ? " monster" : " monsters");
} else if (bindersCount > 0) {
sb.append(bindersCount).append(bindersCount == 1 ? " collection" : " collections");
} else {
sb.append(monstersCount).append(monstersCount == 1 ? " monster" : " monsters");
}
return sb.toString();
}
private static class BatchImportResult {
final int monstersImported;
final int bindersImported;
BatchImportResult(int monstersImported, int bindersImported) {
this.monstersImported = monstersImported;
this.bindersImported = bindersImported;
}
}
@Nullable
private String readMonsterJSONFromIntent(@NonNull Intent intent) {
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)) {
if (extras != null) {
uri = extras.getParcelable(Intent.EXTRA_STREAM);
}
} else if ("android.intent.action.VIEW".equals(action) || "android.intent.action.EDIT".equals(action)) {
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", action));
}
if (uri == null || !isMonsterFile(uri)) {
if (uri != null) {
Logger.logError("Ignored file because extension is not supported (.monster, .card, .binder): " + uri);
Logger.logError(String.format("unexpected launch configuration action: %s, type: %s, uri: %s", action, type, uri));
}
if (uri == null) {
return null;
}
String json = readContentsOfUri(uri);
json = readContentsOfUri(uri);
if (StringHelper.isNullOrEmpty(json)) {
return null;
}
return json;
}
private boolean isMonsterFile(@NonNull Uri uri) {
String fileName = getFileNameFromUri(uri);
if (fileName == null) {
return false;
}
String lowerName = fileName.toLowerCase(Locale.ROOT);
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt")
|| lowerName.endsWith(".card") || lowerName.endsWith(".card.txt")
|| lowerName.endsWith(".binder") || lowerName.endsWith(".binder.txt");
}
@Nullable
private String getFileNameFromUri(@NonNull Uri uri) {
String displayName = null;
if ("content".equals(uri.getScheme())) {
try (Cursor cursor = getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (nameIndex != -1) {
displayName = cursor.getString(nameIndex);
}
}
} catch (Exception e) {
Logger.logError("Error querying display name from content URI", e);
}
}
if (displayName == null) {
displayName = uri.getPath();
}
return displayName;
}
@Nullable
private String readContentsOfUri(Uri uri) {
StringBuilder builder = new StringBuilder();
try (InputStream inputStream =
@@ -464,17 +110,8 @@ public class MainActivity extends AppCompatActivity {
}
} catch (IOException e) {
Logger.logError("error reading file", e);
ToastHelper.showLong(this, "An error occurred while reading the file.");
return null;
}
return builder.toString();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
mImportDisposable.dispose();
}
}
}

View File

@@ -3,104 +3,12 @@ package com.majinnaibu.monstercards;
import android.app.Application;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
import androidx.room.Room;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import com.majinnaibu.monstercards.data.MonsterRepository;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import com.majinnaibu.monstercards.utils.Logger;
import java.io.InterruptedIOException;
import com.majinnaibu.monstercards.init.FlipperInitializer;
public class MonsterCardsApplication extends Application {
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// rename table monster to monsters
database.execSQL("ALTER TABLE monster RENAME TO monsters");
// create the fts view
database.execSQL("CREATE VIRTUAL TABLE IF NOT EXISTS `monsters_fts` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)");
// build the initial full text search index
database.execSQL("INSERT INTO monsters_fts(monsters_fts) VALUES('rebuild')");
}
};
private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// Add the senses column
database.execSQL("ALTER TABLE monsters ADD COLUMN 'senses' TEXT DEFAULT '[]'");
database.execSQL("CREATE TABLE new_monsters (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', PRIMARY KEY(`id`))");
database.execSQL("INSERT INTO new_monsters(id, name, size, type, subtype, alignment, strength_score, strength_saving_throw_advantage, strength_saving_throw_proficiency, dexterity_score, dexterity_saving_throw_advantage, dexterity_saving_throw_proficiency, constitution_score, constitution_saving_throw_advantage, constitution_saving_throw_proficiency, intelligence_score, intelligence_saving_throw_advantage, intelligence_saving_throw_proficiency, wisdom_score, wisdom_saving_throw_advantage, wisdom_saving_throw_proficiency, charisma_score, charisma_saving_throw_advantage, charisma_saving_throw_proficiency, armor_type, shield_bonus, natural_armor_bonus, other_armor_description, hit_dice, has_custom_hit_points, custom_hit_points_description, walk_speed, burrow_speed, climb_speed, fly_speed, can_hover, swim_speed, has_custom_speed, custom_speed_description, challenge_rating, custom_challenge_rating_description, custom_proficiency_bonus, telepathy_range, understands_but_description, senses, skills, damage_immunities, damage_resistances, damage_vulnerabilities, condition_immunities, languages, abilities, actions, reactions, lair_actions, legendary_actions, regional_actions) SELECT id, name, size, type, subtype, alignment, strength_score, strength_saving_throw_advantage, strength_saving_throw_proficiency, dexterity_score, dexterity_saving_throw_advantage, dexterity_saving_throw_proficiency, constitution_score, constitution_saving_throw_advantage, constitution_saving_throw_proficiency, intelligence_score, intelligence_saving_throw_advantage, intelligence_saving_throw_proficiency, wisdom_score, wisdom_saving_throw_advantage, wisdom_saving_throw_proficiency, charisma_score, charisma_saving_throw_advantage, charisma_saving_throw_proficiency, armor_type, shield_bonus, natural_armor_bonus, other_armor_description, hit_dice, has_custom_hit_points, custom_hit_points_description, walk_speed, burrow_speed, climb_speed, fly_speed, can_hover, swim_speed, has_custom_speed, custom_speed_description, challenge_rating, custom_challenge_rating_description, custom_proficiency_bonus, telepathy_range, understands_but_description, senses, skills, damage_immunities, damage_resistances, damage_vulnerabilities, condition_immunities, languages, abilities, actions, reactions, lair_actions, legendary_actions, regional_actions FROM monsters");
database.execSQL("DROP TABLE monsters");
database.execSQL("ALTER TABLE new_monsters RENAME TO monsters");
}
};
private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS `collections` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))");
database.execSQL("CREATE TABLE IF NOT EXISTS `collection_monsters` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)");
database.execSQL("CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `collection_monsters` (`collection_id`)");
database.execSQL("CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `collection_monsters` (`monster_id`)");
}
};
private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS `dashboard_monsters` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)");
database.execSQL("CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `dashboard_monsters` (`monster_id`)");
database.execSQL("INSERT INTO dashboard_monsters(monster_id, ordinal) SELECT id, 0 FROM monsters");
}
};
private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE monsters ADD COLUMN `source_url` TEXT DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `bonus_actions` TEXT DEFAULT '[]'");
database.execSQL("ALTER TABLE monsters ADD COLUMN `mythic_actions` TEXT DEFAULT '[]'");
database.execSQL("ALTER TABLE monsters ADD COLUMN `legendary_actions_description` TEXT DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `lair_actions_description` TEXT DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `lair_actions_end_note` TEXT DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `regional_actions_description` TEXT DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `regional_actions_end_note` TEXT DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `mythic_actions_description` TEXT DEFAULT ''");
}
};
private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE monsters ADD COLUMN `player_name` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `background` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `personality_traits` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `ideals` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `bonds` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `flaws` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `age` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `height` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `weight` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `eyes` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `skin` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `hair` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `appearance` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `backstory` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE monsters ADD COLUMN `allies_and_organizations` TEXT NOT NULL DEFAULT ''");
}
};
private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS `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 '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))");
database.execSQL("INSERT INTO `new_monsters` 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, source_url, bonus_actions, mythic_actions, legendary_actions_description, lair_actions_description, lair_actions_end_note, regional_actions_description, regional_actions_end_note, mythic_actions_description, COALESCE(player_name, ''), COALESCE(background, ''), COALESCE(personality_traits, ''), COALESCE(ideals, ''), COALESCE(bonds, ''), COALESCE(flaws, ''), COALESCE(age, ''), COALESCE(height, ''), COALESCE(weight, ''), COALESCE(eyes, ''), COALESCE(skin, ''), COALESCE(hair, ''), COALESCE(appearance, ''), COALESCE(backstory, ''), COALESCE(allies_and_organizations, '') FROM `monsters`");
database.execSQL("DROP TABLE `monsters`");
database.execSQL("ALTER TABLE `new_monsters` RENAME TO `monsters`");
}
};
private MonsterRepository m_monsterLibraryRepository;
@@ -116,34 +24,11 @@ public class MonsterCardsApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
RxJavaPlugins.setErrorHandler(e -> {
if (e instanceof UndeliverableException) {
e = e.getCause();
}
if (e instanceof InterruptedException || e instanceof InterruptedIOException) {
// Expected when a blocking task is cancelled; swallow silently.
return;
}
Logger.logError("Undeliverable exception received by RxJava global handler", e);
});
// Required initialization logic here!
// .fallbackToDestructiveMigration()
AppDatabase m_db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "monsters")
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4)
.addMigrations(MIGRATION_4_5)
.addMigrations(MIGRATION_5_6)
.addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.fallbackToDestructiveMigrationOnDowngrade()
// .fallbackToDestructiveMigration()
.build();
m_monsterLibraryRepository = new MonsterRepository(m_db);
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.

View File

@@ -1,36 +0,0 @@
package com.majinnaibu.monstercards;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.appcompat.app.AppCompatActivity;
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
Intent intent = new Intent(SplashActivity.this, MainActivity.class);
if (getIntent() != null) {
if (getIntent().getExtras() != null) {
intent.putExtras(getIntent().getExtras());
}
if (getIntent().getAction() != null) {
intent.setAction(getIntent().getAction());
}
if (getIntent().getData() != null) {
intent.setData(getIntent().getData());
}
}
startActivity(intent);
finish();
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}, 1200);
}
}

View File

@@ -1,64 +0,0 @@
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 androidx.room.Update;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.CollectionWithCount;
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 CollectionDAO {
@Query("SELECT * FROM collections")
Flowable<List<Collection>> getAll();
@Query("SELECT collections.*, COUNT(collection_monsters.id) as monsterCount FROM collections LEFT JOIN collection_monsters ON collections.id = collection_monsters.collection_id GROUP BY collections.id")
Flowable<List<CollectionWithCount>> getCollectionsWithCount();
@Query("SELECT * FROM collections WHERE id = :id LIMIT 1")
Flowable<Collection> getById(String id);
@Insert(onConflict = OnConflictStrategy.REPLACE)
Completable save(Collection... collections);
@Delete
Completable delete(Collection collection);
@Query("SELECT monsters.* FROM monsters INNER JOIN collection_monsters ON monsters.id = collection_monsters.monster_id WHERE collection_monsters.collection_id = :collectionId ORDER BY collection_monsters.ordinal ASC, collection_monsters.id ASC")
Flowable<List<Monster>> getMonstersForCollection(String collectionId);
@Query("SELECT * FROM collection_monsters WHERE collection_id = :collectionId ORDER BY ordinal ASC, id ASC")
Flowable<List<CollectionMonster>> getCollectionMonstersForCollection(String collectionId);
@Insert
Completable addMonsterToCollection(CollectionMonster... collectionMonsters);
@Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId AND monster_id = :monsterId")
Completable removeMonsterFromCollection(String collectionId, String monsterId);
@Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId")
Completable removeAllMonstersFromCollection(String collectionId);
@Query("DELETE FROM collection_monsters WHERE id = :id")
Completable removeCollectionMonsterById(long id);
@Query("DELETE FROM collection_monsters")
Completable deleteAllCollectionMonsters();
@Query("DELETE FROM collections")
Completable deleteAllCollections();
@Update
Completable updateCollectionMonsters(List<CollectionMonster> collectionMonsters);
}

View File

@@ -1,48 +0,0 @@
package com.majinnaibu.monstercards.data;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update;
import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
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 DashboardDAO {
@Query("SELECT monsters.* FROM monsters INNER JOIN dashboard_monsters ON monsters.id = dashboard_monsters.monster_id ORDER BY dashboard_monsters.ordinal ASC, dashboard_monsters.id ASC")
Flowable<List<Monster>> getDashboardMonsters();
@Transaction
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
Flowable<List<DashboardMonsterWithMonster>> getDashboardMonstersWithMonster();
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
Flowable<List<DashboardMonster>> getDashboardMonsterEntries();
@Insert
Completable addMonsterToDashboard(DashboardMonster... dashboardMonsters);
@Query("DELETE FROM dashboard_monsters WHERE id = :id")
Completable removeDashboardMonsterById(long id);
@Query("DELETE FROM dashboard_monsters WHERE id = (SELECT id FROM dashboard_monsters WHERE monster_id = :monsterId LIMIT 1)")
Completable removeMonsterFromDashboard(String monsterId);
@Query("DELETE FROM dashboard_monsters WHERE monster_id = :monsterId")
Completable removeAllMonstersFromDashboard(String monsterId);
@Query("DELETE FROM dashboard_monsters")
Completable clearDashboard();
@Update
Completable updateDashboardMonsters(List<DashboardMonster> dashboardMonsters);
}

View File

@@ -1,7 +1,5 @@
package com.majinnaibu.monstercards.data;
import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ArmorType;
@@ -14,7 +12,6 @@ import com.majinnaibu.monstercards.models.Trait;
@SuppressWarnings("unused")
public final class DevContent {
@NonNull
public static Monster createSampleMonster() {
Monster monster = new Monster();
// Name

View File

@@ -1,48 +0,0 @@
package com.majinnaibu.monstercards.data;
import com.majinnaibu.monstercards.models.ImportSource;
import java.util.ArrayList;
import java.util.List;
public class ImportConfig {
public static final List<ImportSource> SOURCES = new ArrayList<>();
static {
SOURCES.add(new ImportSource(
"open5e",
"Open5e.com System Reference Document",
"Open5e",
"https://open5e.com",
ImportSource.ImportType.OPEN5E_API,
null,
null,
null,
null
));
SOURCES.add(new ImportSource(
"pf2e_foundry",
"Pathfinder 2e Foundry VTT",
"foundryvtt",
"https://github.com/foundryvtt/pf2e",
ImportSource.ImportType.GIT_ARCHIVE,
"https://github.com/foundryvtt/pf2e/archive/refs/heads/v14-dev.zip",
"com.majinnaibu.monstercards.importers.Pf2eImporter",
".json",
"pf2e-14-dev/packs/pf2e"
));
SOURCES.add(new ImportSource(
"sf2e_foundry",
"Starfinder 2e Foundry VTT",
"foundryvtt",
"https://github.com/foundryvtt/pf2e",
ImportSource.ImportType.GIT_ARCHIVE,
"https://github.com/foundryvtt/pf2e/archive/refs/heads/v14-dev.zip",
"com.majinnaibu.monstercards.importers.Pf2eImporter",
".json",
"pf2e-14-dev/packs/sf2e"
));
}
}

View File

@@ -36,7 +36,4 @@ public interface MonsterDAO {
@Delete
Completable delete(Monster monster);
@Query("DELETE FROM monsters")
Completable deleteAllMonsters();
}

View File

@@ -3,40 +3,26 @@ package com.majinnaibu.monstercards.data;
import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.AppDatabase;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.BinderExport;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.CollectionWithCount;
import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.SearchResultItem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
@SuppressWarnings("ResultOfMethodCallIgnored")
public class MonsterRepository {
private final AppDatabase m_db;
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())
@@ -45,58 +31,12 @@ public class MonsterRepository {
public Flowable<List<Monster>> searchMonsters(String searchText) {
return m_db.monsterDAO()
.getAll()
.map(monsters -> {
ArrayList<Monster> filteredMonsters = new ArrayList<>();
for (Monster monster : monsters) {
if (Helpers.monsterMatchesSearch(monster, searchText)) {
filteredMonsters.add(monster);
}
}
return (List<Monster>) filteredMonsters;
})
.search(searchText)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<SearchResultItem>> searchAll(String searchText) {
Flowable<List<Monster>> monstersFlowable = m_db.monsterDAO()
.getAll()
.map(monsters -> {
ArrayList<Monster> filteredMonsters = new ArrayList<>();
for (Monster monster : monsters) {
if (Helpers.monsterMatchesSearch(monster, searchText)) {
filteredMonsters.add(monster);
}
}
return (List<Monster>) filteredMonsters;
});
Flowable<List<Collection>> collectionsFlowable = m_db.collectionDAO()
.getAll()
.map(collections -> {
ArrayList<Collection> filteredCollections = new ArrayList<>();
for (Collection collection : collections) {
if (StringHelper.isNullOrEmpty(searchText) || StringHelper.containsCaseInsensitive(collection.name, searchText)) {
filteredCollections.add(collection);
}
}
return (List<Collection>) filteredCollections;
});
return Flowable.combineLatest(monstersFlowable, collectionsFlowable, (monsters, collections) -> {
ArrayList<SearchResultItem> results = new ArrayList<>();
for (Monster monster : monsters) {
results.add(new SearchResultItem(monster));
}
for (Collection collection : collections) {
results.add(new SearchResultItem(collection));
}
return (List<SearchResultItem>) results;
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
public Flowable<Monster> getMonster(@NonNull UUID monsterId) {
public Flowable<Monster> getMonster(UUID monsterId) {
return m_db.monsterDAO()
.loadAllByIds(new String[]{monsterId.toString()})
.map(
@@ -128,424 +68,4 @@ public class MonsterRepository {
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Flowable<List<Collection>> getCollections() {
return m_db.collectionDAO()
.getAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<CollectionWithCount>> getCollectionsWithCount() {
return m_db.collectionDAO()
.getCollectionsWithCount()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<Collection> getCollection(@NonNull UUID collectionId) {
return m_db.collectionDAO()
.getById(collectionId.toString())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Completable saveCollection(Collection collection) {
Completable result = m_db.collectionDAO().save(collection);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable deleteCollection(Collection collection) {
Completable result = m_db.collectionDAO().delete(collection);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Flowable<List<Monster>> getMonstersForCollection(@NonNull UUID collectionId) {
return m_db.collectionDAO()
.getMonstersForCollection(collectionId.toString())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<CollectionMonster>> getCollectionMonstersForCollection(@NonNull UUID collectionId) {
return m_db.collectionDAO()
.getCollectionMonstersForCollection(collectionId.toString())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Completable addMonsterToCollection(@NonNull UUID collectionId, @NonNull UUID monsterId) {
return addMonsterToCollection(collectionId, monsterId, 0);
}
public Completable addMonsterToCollection(@NonNull UUID collectionId, @NonNull UUID monsterId, int ordinal) {
CollectionMonster collectionMonster = new CollectionMonster(collectionId, monsterId, ordinal);
Completable result = m_db.collectionDAO().addMonsterToCollection(collectionMonster);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable removeMonsterFromCollection(@NonNull UUID collectionId, @NonNull UUID monsterId) {
Completable result = m_db.collectionDAO().removeMonsterFromCollection(collectionId.toString(), monsterId.toString());
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable removeCollectionMonsterById(long junctionId) {
Completable result = m_db.collectionDAO().removeCollectionMonsterById(junctionId);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable updateCollectionMonsters(List<CollectionMonster> collectionMonsters) {
Completable result = m_db.collectionDAO().updateCollectionMonsters(collectionMonsters);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Flowable<List<Monster>> getDashboardMonsters() {
return m_db.dashboardDAO()
.getDashboardMonsters()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster>> getDashboardMonstersWithMonster() {
return m_db.dashboardDAO()
.getDashboardMonstersWithMonster()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<DashboardMonster>> getDashboardMonsterEntries() {
return m_db.dashboardDAO()
.getDashboardMonsterEntries()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Completable addMonsterToDashboard(@NonNull UUID monsterId) {
DashboardMonster entry = new DashboardMonster(monsterId, 0);
Completable result = m_db.dashboardDAO().addMonsterToDashboard(entry);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable addCollectionToDashboard(@NonNull UUID collectionId) {
return m_db.collectionDAO().getMonstersForCollection(collectionId.toString())
.firstOrError()
.flatMapCompletable(monsters -> {
if (monsters.isEmpty()) {
return Completable.complete();
}
DashboardMonster[] entries = new DashboardMonster[monsters.size()];
for (int i = 0; i < monsters.size(); i++) {
entries[i] = new DashboardMonster(monsters.get(i).id, i);
}
return m_db.dashboardDAO().addMonsterToDashboard(entries);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Completable removeMonsterFromDashboard(@NonNull UUID monsterId) {
Completable result = m_db.dashboardDAO().removeMonsterFromDashboard(monsterId.toString());
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable removeDashboardMonsterById(long id) {
Completable result = m_db.dashboardDAO().removeDashboardMonsterById(id);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable clearDashboard() {
Completable result = m_db.dashboardDAO().clearDashboard();
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable clearAllData() {
return Completable.fromAction(() -> {
m_db.dashboardDAO().clearDashboard().blockingAwait();
m_db.collectionDAO().deleteAllCollectionMonsters().blockingAwait();
m_db.collectionDAO().deleteAllCollections().blockingAwait();
m_db.monsterDAO().deleteAllMonsters().blockingAwait();
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
public Completable removeAllCollections() {
return Completable.fromAction(() -> {
m_db.collectionDAO().deleteAllCollectionMonsters().blockingAwait();
m_db.collectionDAO().deleteAllCollections().blockingAwait();
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
public Completable removeAllMonstersFromCollection(@NonNull UUID collectionId) {
Completable result = m_db.collectionDAO().removeAllMonstersFromCollection(collectionId.toString());
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Single<String> exportCollections() {
return m_db.collectionDAO().getAll().first(new ArrayList<>())
.map(collections -> {
List<BinderExport.CollectionExport> colExports = new ArrayList<>();
for (Collection col : collections) {
List<Monster> colMonsters = m_db.collectionDAO().getMonstersForCollection(col.id.toString())
.first(new ArrayList<>()).blockingGet();
colExports.add(new BinderExport.CollectionExport(
col.id.toString(),
col.name,
col.description,
colMonsters
));
}
return new com.majinnaibu.monstercards.exporters.BinderExporter().exportFullBackup(colExports, null);
})
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
public Single<String> exportEverything() {
return Single.zip(
m_db.collectionDAO().getAll().first(new ArrayList<>()),
m_db.monsterDAO().getAll().first(new ArrayList<>()),
m_db.dashboardDAO().getDashboardMonsters().first(new ArrayList<>()),
(collections, allMonsters, dashboardMonsters) -> {
List<BinderExport.CollectionExport> colExports = new ArrayList<>();
Set<UUID> monstersInCollections = new HashSet<>();
for (Collection col : collections) {
List<Monster> colMonsters = m_db.collectionDAO().getMonstersForCollection(col.id.toString())
.first(new ArrayList<>()).blockingGet();
for (Monster m : colMonsters) {
monstersInCollections.add(m.id);
}
colExports.add(new BinderExport.CollectionExport(
col.id.toString(),
col.name,
col.description,
colMonsters
));
}
List<Monster> uncategorized = new ArrayList<>();
for (Monster m : allMonsters) {
if (!monstersInCollections.contains(m.id)) {
uncategorized.add(m);
}
}
if (!uncategorized.isEmpty() || colExports.isEmpty()) {
colExports.add(new BinderExport.CollectionExport(
UUID.randomUUID().toString(),
colExports.isEmpty() ? "All Monsters" : "Uncategorized",
"",
colExports.isEmpty() ? allMonsters : uncategorized
));
}
return new com.majinnaibu.monstercards.exporters.BinderExporter().exportFullBackup(colExports, dashboardMonsters);
}
).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
public Completable updateDashboardMonsters(List<DashboardMonster> items) {
Completable result = m_db.dashboardDAO().updateDashboardMonsters(items);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable importBinder(@NonNull BinderExport binder) {
return Completable.fromAction(() -> {
// 1. Process Collections & Cards in Binder
if (binder.collections != null && !binder.collections.isEmpty()) {
List<Collection> existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet();
for (BinderExport.CollectionExport colExport : binder.collections) {
if (colExport == null) {
continue;
}
String colName = colExport.name != null ? colExport.name.trim() : "";
String colDesc = colExport.description != null ? colExport.description.trim() : "";
UUID colExportId = null;
if (colExport.id != null && !colExport.id.trim().isEmpty()) {
try {
colExportId = UUID.fromString(colExport.id.trim());
} catch (Exception ignored) {
}
}
Collection targetCollection = null;
if (colExportId != null) {
for (Collection existing : existingCols) {
if (existing.id.equals(colExportId)) {
targetCollection = existing;
break;
}
}
}
if (targetCollection == null && !colName.isEmpty()) {
for (Collection existing : existingCols) {
if (existing.name != null && existing.name.equalsIgnoreCase(colName)) {
targetCollection = existing;
break;
}
}
}
UUID targetCollectionId;
if (targetCollection != null) {
targetCollectionId = targetCollection.id;
if (!colDesc.isEmpty() && (targetCollection.description == null || targetCollection.description.isEmpty())) {
targetCollection.description = colDesc;
m_db.collectionDAO().save(targetCollection).blockingAwait();
}
} else {
Collection newCol = new Collection();
newCol.id = colExportId != null ? colExportId : UUID.randomUUID();
newCol.name = !colName.isEmpty() ? colName : "Imported Collection";
newCol.description = colDesc;
m_db.collectionDAO().save(newCol).blockingAwait();
targetCollectionId = newCol.id;
existingCols.add(newCol);
}
if (colExport.cards != null && !colExport.cards.isEmpty()) {
List<Monster> monstersToSave = new ArrayList<>();
Map<UUID, Integer> importedCardCounts = new HashMap<>();
for (Monster card : colExport.cards) {
if (card != null) {
if (card.id == null) {
card.id = UUID.randomUUID();
}
monstersToSave.add(card);
importedCardCounts.put(card.id, getCount(importedCardCounts, card.id) + 1);
}
}
if (!monstersToSave.isEmpty()) {
m_db.monsterDAO().save(monstersToSave.toArray(new Monster[0])).blockingAwait();
List<Monster> existingColMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString())
.first(new ArrayList<>()).blockingGet();
Map<UUID, Integer> existingCardCounts = new HashMap<>();
for (Monster m : existingColMonsters) {
existingCardCounts.put(m.id, getCount(existingCardCounts, m.id) + 1);
}
int ordinal = existingColMonsters.size();
for (Map.Entry<UUID, Integer> entry : importedCardCounts.entrySet()) {
UUID monsterId = entry.getKey();
int importedCount = entry.getValue();
int existingCount = getCount(existingCardCounts, monsterId);
if (importedCount > existingCount) {
int extraCopiesNeeded = importedCount - existingCount;
for (int k = 0; k < extraCopiesNeeded; k++) {
m_db.collectionDAO().addMonsterToCollection(new CollectionMonster(targetCollectionId, monsterId, ordinal++)).blockingAwait();
}
}
}
}
}
}
}
// 2. Process Dashboard entries in Binder if present
if (binder.dashboard != null && !binder.dashboard.isEmpty()) {
List<Monster> dashboardMonstersToSave = new ArrayList<>();
Map<UUID, Integer> importedDashboardCounts = new HashMap<>();
for (Monster card : binder.dashboard) {
if (card != null) {
if (card.id == null) {
card.id = UUID.randomUUID();
}
dashboardMonstersToSave.add(card);
importedDashboardCounts.put(card.id, getCount(importedDashboardCounts, card.id) + 1);
}
}
if (!dashboardMonstersToSave.isEmpty()) {
m_db.monsterDAO().save(dashboardMonstersToSave.toArray(new Monster[0])).blockingAwait();
List<DashboardMonsterWithMonster> existingDashboard = m_db.dashboardDAO().getDashboardMonstersWithMonster()
.first(new ArrayList<>()).blockingGet();
Map<UUID, Integer> existingDashboardCounts = new HashMap<>();
int currentMaxOrdinal = 0;
for (DashboardMonsterWithMonster entry : existingDashboard) {
if (entry.monster != null) {
existingDashboardCounts.put(entry.monster.id, getCount(existingDashboardCounts, entry.monster.id) + 1);
}
if (entry.dashboardEntry != null && entry.dashboardEntry.ordinal > currentMaxOrdinal) {
currentMaxOrdinal = entry.dashboardEntry.ordinal;
}
}
int ordinal = currentMaxOrdinal + 1;
List<DashboardMonster> extraEntriesToAdd = new ArrayList<>();
for (Map.Entry<UUID, Integer> entry : importedDashboardCounts.entrySet()) {
UUID monsterId = entry.getKey();
int importedCount = entry.getValue();
int existingCount = getCount(existingDashboardCounts, monsterId);
if (importedCount > existingCount) {
int extraNeeded = importedCount - existingCount;
for (int k = 0; k < extraNeeded; k++) {
extraEntriesToAdd.add(new DashboardMonster(monsterId, ordinal++));
}
}
}
if (!extraEntriesToAdd.isEmpty()) {
m_db.dashboardDAO().addMonsterToDashboard(extraEntriesToAdd.toArray(new DashboardMonster[0])).blockingAwait();
}
}
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
private static int getCount(Map<UUID, Integer> map, UUID key) {
if (map == null || key == null) return 0;
Integer val = map.get(key);
return val != null ? val : 0;
}
private static class Helpers {
static boolean monsterMatchesSearch(Monster monster, String searchText) {
if (StringHelper.isNullOrEmpty(searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.name, searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.size, searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.type, searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.subtype, searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.alignment, searchText)) {
return true;
}
return false;
}
}
}

View File

@@ -1,6 +1,5 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.annotation.NonNull;
import androidx.room.TypeConverter;
import com.majinnaibu.monstercards.data.enums.ArmorType;
@@ -8,7 +7,7 @@ import com.majinnaibu.monstercards.data.enums.ArmorType;
public class ArmorTypeConverter {
@TypeConverter
public static String fromArmorType(@NonNull ArmorType armorType) {
public static String fromArmorType(ArmorType armorType) {
return armorType.stringValue;
}

View File

@@ -1,6 +1,5 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.annotation.NonNull;
import androidx.room.TypeConverter;
import com.majinnaibu.monstercards.data.enums.ChallengeRating;
@@ -8,7 +7,7 @@ import com.majinnaibu.monstercards.data.enums.ChallengeRating;
public class ChallengeRatingConverter {
@TypeConverter
public static String fromChallengeRating(@NonNull ChallengeRating challengeRating) {
public static String fromChallengeRating(ChallengeRating challengeRating) {
return challengeRating.stringValue;
}

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

@@ -1,15 +1,13 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.annotation.NonNull;
import androidx.room.TypeConverter;
import java.util.UUID;
public class UUIDConverter {
@NonNull
@TypeConverter
public static String fromUUID(@NonNull UUID uuid) {
public static String fromUUID(UUID uuid) {
return uuid.toString();
}

View File

@@ -5,7 +5,7 @@ public enum AbilityScore {
STRENGTH("strength", "Strength", "STR"),
DEXTERITY("dexterity", "Dexterity", "DEX"),
CONSTITUTION("constitution", "Constitution", "CON"),
INTELLIGENCE("intelligence", "Intelligence", "INT"),
INTELLIGENCE("intellligence", "Intelligence", "INT"),
WISDOM("wisdom", "Wisdom", "WIS"),
CHARISMA("charisma", "Charisma", "CHA"),
;
@@ -21,23 +21,11 @@ public enum AbilityScore {
}
public static AbilityScore valueOfString(String string) {
if (string == null) {
return AbilityScore.STRENGTH;
}
String lower = string.trim().toLowerCase(java.util.Locale.ROOT);
for (AbilityScore abilityScore : values()) {
if (abilityScore.stringValue.equalsIgnoreCase(lower)
|| abilityScore.shortDisplayName.equalsIgnoreCase(lower)
|| abilityScore.name().equalsIgnoreCase(lower)) {
if (abilityScore.stringValue.equals(string)) {
return abilityScore;
}
}
if (lower.startsWith("str")) return STRENGTH;
if (lower.startsWith("dex")) return DEXTERITY;
if (lower.startsWith("con")) return CONSTITUTION;
if (lower.startsWith("int")) return INTELLIGENCE;
if (lower.startsWith("wis")) return WISDOM;
if (lower.startsWith("cha")) return CHARISMA;
return AbilityScore.STRENGTH;
}
}

View File

@@ -3,7 +3,7 @@ package com.majinnaibu.monstercards.data.enums;
public enum ProficiencyType {
NONE("none", "None", ""),
PROFICIENT("proficient", "Proficient", "P"),
EXPERTISE("expertise", "Expertise", "Ex"),
EXPERTISE("experties", "Expertise", "Ex"),
;
public final String displayName;

View File

@@ -1,94 +0,0 @@
package com.majinnaibu.monstercards.exporters;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.majinnaibu.monstercards.models.BinderExport;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.Monster;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class BinderExporter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@NonNull
public String exportBinder(String collectionName, List<Monster> monsters) {
return exportBinder(new Collection(collectionName != null ? collectionName : "", ""), monsters, null);
}
@NonNull
public String exportBinder(@Nullable Collection collection, List<Monster> monsters) {
return exportBinder(collection, monsters, null);
}
@NonNull
public String exportBinder(@Nullable Collection collection, @Nullable List<Monster> monsters, @Nullable List<Monster> dashboardMonsters) {
BinderExport export = new BinderExport();
export.schema = "https://majinnaibu.com/schemas/binder.schema.json";
export.schemaVersion = 1;
if (monsters != null) {
for (Monster monster : monsters) {
monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
monster.schemaVersion = 1;
}
}
if (dashboardMonsters != null && !dashboardMonsters.isEmpty()) {
for (Monster monster : dashboardMonsters) {
monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
monster.schemaVersion = 1;
}
export.dashboard = dashboardMonsters;
}
if (collection != null) {
String colId = collection.id.toString();
String colName = collection.name;
String colDesc = collection.description;
BinderExport.CollectionExport col = new BinderExport.CollectionExport(
colId,
colName,
colDesc,
monsters
);
export.collections = Collections.singletonList(col);
}
return GSON.toJson(export);
}
@NonNull
public String exportFullBackup(@Nullable List<BinderExport.CollectionExport> collections, @Nullable List<Monster> dashboardMonsters) {
BinderExport export = new BinderExport();
export.schema = "https://majinnaibu.com/schemas/binder.schema.json";
export.schemaVersion = 1;
if (collections != null) {
for (BinderExport.CollectionExport col : collections) {
if (col.cards != null) {
for (Monster m : col.cards) {
m.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
m.schemaVersion = 1;
}
}
}
export.collections = collections;
}
if (dashboardMonsters != null && !dashboardMonsters.isEmpty()) {
for (Monster m : dashboardMonsters) {
m.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
m.schemaVersion = 1;
}
export.dashboard = dashboardMonsters;
}
return GSON.toJson(export);
}
}

View File

@@ -1,18 +0,0 @@
package com.majinnaibu.monstercards.exporters;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.majinnaibu.monstercards.models.Monster;
public class MonsterCardExporter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@NonNull
public String exportCard(@NonNull Monster monster) {
monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
monster.schemaVersion = 1;
return GSON.toJson(monster);
}
}

View File

@@ -1,287 +0,0 @@
package com.majinnaibu.monstercards.exporters;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.ArmorType;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.models.Trait;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public class Open5eExporter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@NonNull
public String exportMonster(@NonNull Monster monster) {
JsonObject root = new JsonObject();
root.addProperty("$schema", "../../../schema/entity.json");
root.addProperty("uuid", monster.id != null ? monster.id.toString() : UUID.randomUUID().toString());
root.addProperty("ruleset_id", "open5e");
root.addProperty("entity_type", "character");
root.addProperty("display_name", monster.name != null && !monster.name.isEmpty() ? monster.name : "Unnamed Entity");
root.addProperty("description", buildDescription(monster));
root.addProperty("version", "1.0.0");
root.addProperty("template_id", "stat_block");
JsonObject props = new JsonObject();
props.addProperty("name", monster.name != null ? monster.name : "");
props.addProperty("size", monster.size != null ? monster.size : "");
props.addProperty("type", monster.type != null ? monster.type : "");
props.addProperty("subtype", monster.subtype != null ? monster.subtype : "");
props.addProperty("alignment", monster.alignment != null ? monster.alignment : "");
props.addProperty("armor_class", monster.getArmorClassValue());
props.addProperty("armor_description", monster.getArmorClass());
if (monster.armorType != null && !monster.armorType.equals(ArmorType.NONE)) {
JsonObject armorObj = new JsonObject();
armorObj.addProperty("id", monster.armorType.stringValue);
armorObj.addProperty("name", monster.armorType.displayName);
armorObj.addProperty("armor_category", getArmorCategory(monster.armorType));
armorObj.addProperty("ac_base", monster.armorType.baseArmorClass);
Integer dexCap = getDexCap(monster.armorType);
if (dexCap != null) {
armorObj.addProperty("dex_cap", dexCap);
} else {
armorObj.add("dex_cap", JsonNull.INSTANCE);
}
armorObj.addProperty("stealth_disadvantage", isStealthDisadvantage(monster.armorType));
props.add("equipped_armor", armorObj);
} else {
props.add("equipped_armor", JsonNull.INSTANCE);
}
if (monster.shieldBonus > 0) {
JsonObject shieldObj = new JsonObject();
shieldObj.addProperty("id", "shield");
shieldObj.addProperty("name", "Shield");
shieldObj.addProperty("ac_bonus", monster.shieldBonus);
props.add("equipped_shield", shieldObj);
} else {
props.add("equipped_shield", JsonNull.INSTANCE);
}
JsonObject hpObj = new JsonObject();
hpObj.addProperty("max", monster.getHitPointsValue());
hpObj.addProperty("current", monster.getHitPointsValue());
hpObj.addProperty("formula", monster.hasCustomHP && !monster.customHPDescription.isEmpty() ? monster.customHPDescription : monster.getHitPoints());
hpObj.addProperty("hit_dice", monster.hitDice + "d8");
props.add("hit_points", hpObj);
props.addProperty("speed", monster.walkSpeed);
props.addProperty("speed_desc", monster.getSpeedText());
props.addProperty("challenge_rating", monster.getChallengeRatingDescription());
props.addProperty("cr", monster.challengeRating != null ? monster.challengeRating.stringValue : "1");
JsonObject abilitiesObj = new JsonObject();
abilitiesObj.addProperty("strength", monster.strengthScore);
abilitiesObj.addProperty("dexterity", monster.dexterityScore);
abilitiesObj.addProperty("constitution", monster.constitutionScore);
abilitiesObj.addProperty("intelligence", monster.intelligenceScore);
abilitiesObj.addProperty("wisdom", monster.wisdomScore);
abilitiesObj.addProperty("charisma", monster.charismaScore);
props.add("abilities", abilitiesObj);
if (monster.playerName != null && !monster.playerName.isEmpty()) props.addProperty("player_name", monster.playerName);
if (monster.background != null && !monster.background.isEmpty()) props.addProperty("background", monster.background);
if (monster.personalityTraits != null && !monster.personalityTraits.isEmpty()) props.addProperty("personality_traits", monster.personalityTraits);
if (monster.ideals != null && !monster.ideals.isEmpty()) props.addProperty("ideals", monster.ideals);
if (monster.bonds != null && !monster.bonds.isEmpty()) props.addProperty("bonds", monster.bonds);
if (monster.flaws != null && !monster.flaws.isEmpty()) props.addProperty("flaws", monster.flaws);
if (hasDetails(monster)) {
JsonObject detailsObj = new JsonObject();
if (monster.age != null && !monster.age.isEmpty()) detailsObj.addProperty("age", monster.age);
if (monster.height != null && !monster.height.isEmpty()) detailsObj.addProperty("height", monster.height);
if (monster.weight != null && !monster.weight.isEmpty()) detailsObj.addProperty("weight", monster.weight);
if (monster.eyes != null && !monster.eyes.isEmpty()) detailsObj.addProperty("eyes", monster.eyes);
if (monster.skin != null && !monster.skin.isEmpty()) detailsObj.addProperty("skin", monster.skin);
if (monster.hair != null && !monster.hair.isEmpty()) detailsObj.addProperty("hair", monster.hair);
if (monster.appearance != null && !monster.appearance.isEmpty()) detailsObj.addProperty("appearance", monster.appearance);
if (monster.backstory != null && !monster.backstory.isEmpty()) detailsObj.addProperty("backstory", monster.backstory);
if (monster.alliesAndOrganizations != null && !monster.alliesAndOrganizations.isEmpty()) detailsObj.addProperty("allies_and_organizations", monster.alliesAndOrganizations);
props.add("character_details", detailsObj);
}
JsonArray savesArr = buildSavingThrowsArray(monster);
if (savesArr.size() > 0) props.add("saving_throws", savesArr);
JsonArray skillsArr = buildSkillsArray(monster);
if (skillsArr.size() > 0) props.add("skills", skillsArr);
if (monster.damageImmunities != null && !monster.damageImmunities.isEmpty()) props.add("damage_immunities", toJsonArray(monster.damageImmunities));
if (monster.damageResistances != null && !monster.damageResistances.isEmpty()) props.add("damage_resistances", toJsonArray(monster.damageResistances));
if (monster.damageVulnerabilities != null && !monster.damageVulnerabilities.isEmpty()) props.add("damage_vulnerabilities", toJsonArray(monster.damageVulnerabilities));
if (monster.conditionImmunities != null && !monster.conditionImmunities.isEmpty()) props.add("condition_immunities", toJsonArray(monster.conditionImmunities));
props.addProperty("senses", monster.getSensesDescription());
if (monster.languages != null && !monster.languages.isEmpty()) {
JsonArray langArr = new JsonArray();
for (Language l : monster.languages) {
if (l != null && l.getName() != null && !l.getName().isEmpty()) {
langArr.add(l.getName());
}
}
props.add("languages", langArr);
}
if (monster.abilities != null && !monster.abilities.isEmpty()) props.add("traits", traitsToJsonArray(monster.abilities));
if (monster.actions != null && !monster.actions.isEmpty()) props.add("actions", traitsToJsonArray(monster.actions));
if (monster.bonusActions != null && !monster.bonusActions.isEmpty()) props.add("bonus_actions", traitsToJsonArray(monster.bonusActions));
if (monster.reactions != null && !monster.reactions.isEmpty()) props.add("reactions", traitsToJsonArray(monster.reactions));
if (monster.legendaryActions != null && !monster.legendaryActions.isEmpty()) props.add("legendary_actions", traitsToJsonArray(monster.legendaryActions));
if (monster.legendaryActionsDescription != null && !monster.legendaryActionsDescription.isEmpty()) props.addProperty("legendary_desc", monster.legendaryActionsDescription);
if (monster.lairActions != null && !monster.lairActions.isEmpty()) props.add("lair_actions", traitsToJsonArray(monster.lairActions));
if (monster.lairActionsDescription != null && !monster.lairActionsDescription.isEmpty()) props.addProperty("lair_desc", monster.lairActionsDescription);
if (monster.regionalActions != null && !monster.regionalActions.isEmpty()) props.add("regional_effects", traitsToJsonArray(monster.regionalActions));
if (monster.regionalActionsDescription != null && !monster.regionalActionsDescription.isEmpty()) props.addProperty("regional_desc", monster.regionalActionsDescription);
if (monster.mythicActions != null && !monster.mythicActions.isEmpty()) props.add("mythic_actions", traitsToJsonArray(monster.mythicActions));
if (monster.mythicActionsDescription != null && !monster.mythicActionsDescription.isEmpty()) props.addProperty("mythic_desc", monster.mythicActionsDescription);
if (monster.sourceUrl != null && !monster.sourceUrl.isEmpty()) props.addProperty("source_url", monster.sourceUrl);
root.add("properties", props);
return GSON.toJson(root);
}
private String getArmorCategory(ArmorType armorType) {
switch (armorType) {
case PADDED:
case LEATHER:
case STUDDED_LEATHER:
return "light";
case HIDE:
case CHAIN_SHIRT:
case SCALE_MAIL:
case BREASTPLATE:
case HALF_PLATE:
return "medium";
case RING_MAIL:
case CHAIN_MAIL:
case SPLINT_MAIL:
case PLATE_MAIL:
return "heavy";
default:
return "natural";
}
}
private Integer getDexCap(ArmorType armorType) {
switch (armorType) {
case HIDE:
case CHAIN_SHIRT:
case SCALE_MAIL:
case BREASTPLATE:
case HALF_PLATE:
return 2;
case RING_MAIL:
case CHAIN_MAIL:
case SPLINT_MAIL:
case PLATE_MAIL:
return 0;
default:
return null;
}
}
private boolean isStealthDisadvantage(ArmorType armorType) {
switch (armorType) {
case PADDED:
case SCALE_MAIL:
case HALF_PLATE:
case RING_MAIL:
case CHAIN_MAIL:
case SPLINT_MAIL:
case PLATE_MAIL:
return true;
default:
return false;
}
}
private boolean hasDetails(Monster monster) {
return (monster.age != null && !monster.age.isEmpty()) ||
(monster.height != null && !monster.height.isEmpty()) ||
(monster.weight != null && !monster.weight.isEmpty()) ||
(monster.eyes != null && !monster.eyes.isEmpty()) ||
(monster.skin != null && !monster.skin.isEmpty()) ||
(monster.hair != null && !monster.hair.isEmpty()) ||
(monster.appearance != null && !monster.appearance.isEmpty()) ||
(monster.backstory != null && !monster.backstory.isEmpty()) ||
(monster.alliesAndOrganizations != null && !monster.alliesAndOrganizations.isEmpty());
}
private String buildDescription(Monster monster) {
StringBuilder sb = new StringBuilder();
if (monster.size != null && !monster.size.isEmpty()) sb.append(monster.size).append(" ");
if (monster.type != null && !monster.type.isEmpty()) sb.append(monster.type);
if (monster.subtype != null && !monster.subtype.isEmpty()) sb.append(" (").append(monster.subtype).append(")");
if (monster.alignment != null && !monster.alignment.isEmpty()) sb.append(", ").append(monster.alignment);
return sb.toString().trim();
}
private JsonArray buildSavingThrowsArray(Monster monster) {
JsonArray arr = new JsonArray();
checkSave(arr, "Strength", monster.getSavingThrowProficiencyType(AbilityScore.STRENGTH), monster.getAbilityModifier(AbilityScore.STRENGTH) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.STRENGTH)));
checkSave(arr, "Dexterity", monster.getSavingThrowProficiencyType(AbilityScore.DEXTERITY), monster.getAbilityModifier(AbilityScore.DEXTERITY) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.DEXTERITY)));
checkSave(arr, "Constitution", monster.getSavingThrowProficiencyType(AbilityScore.CONSTITUTION), monster.getAbilityModifier(AbilityScore.CONSTITUTION) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.CONSTITUTION)));
checkSave(arr, "Intelligence", monster.getSavingThrowProficiencyType(AbilityScore.INTELLIGENCE), monster.getAbilityModifier(AbilityScore.INTELLIGENCE) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.INTELLIGENCE)));
checkSave(arr, "Wisdom", monster.getSavingThrowProficiencyType(AbilityScore.WISDOM), monster.getAbilityModifier(AbilityScore.WISDOM) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.WISDOM)));
checkSave(arr, "Charisma", monster.getSavingThrowProficiencyType(AbilityScore.CHARISMA), monster.getAbilityModifier(AbilityScore.CHARISMA) + monster.getProficiencyBonus(monster.getSavingThrowProficiencyType(AbilityScore.CHARISMA)));
return arr;
}
private void checkSave(JsonArray arr, String statName, ProficiencyType prof, int bonus) {
if (prof != null && !prof.equals(ProficiencyType.NONE)) {
arr.add(String.format(Locale.ROOT, "%s %+d", statName, bonus));
}
}
private JsonArray buildSkillsArray(Monster monster) {
JsonArray arr = new JsonArray();
if (monster.skills != null) {
for (Skill s : monster.skills) {
if (s != null && s.proficiencyType != null && !s.proficiencyType.equals(ProficiencyType.NONE)) {
int bonus = s.getSkillBonus(monster);
arr.add(String.format(Locale.ROOT, "%s %+d", s.name.toLowerCase(Locale.ROOT), bonus));
}
}
}
return arr;
}
private JsonArray toJsonArray(Collection<String> items) {
JsonArray arr = new JsonArray();
for (String s : items) {
if (s != null && !s.isEmpty()) arr.add(s);
}
return arr;
}
private JsonArray traitsToJsonArray(List<Trait> traits) {
JsonArray arr = new JsonArray();
for (Trait t : traits) {
if (t != null && t.name != null && !t.name.isEmpty()) {
JsonObject obj = new JsonObject();
obj.addProperty("name", t.name);
obj.addProperty("desc", t.description != null ? t.description : "");
arr.add(obj);
}
}
return arr;
}
}

View File

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

View File

@@ -4,9 +4,20 @@ 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);

View File

@@ -1,86 +1,293 @@
package com.majinnaibu.monstercards.helpers;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
import com.majinnaibu.monstercards.importers.EntityImporter;
import com.majinnaibu.monstercards.importers.MonsterJsonImporter;
import com.majinnaibu.monstercards.importers.Open5eImporter;
import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter;
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.utils.Logger;
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.Locale;
import java.util.Set;
public class MonsterImportHelper {
private static final List<EntityImporter<Monster>> IMPORTERS = new ArrayList<>();
static {
IMPORTERS.add(new TetraCubeMonsterImporter());
IMPORTERS.add(new MonsterJsonImporter());
IMPORTERS.add(new Open5eImporter());
IMPORTERS.add(new DnDBeyondImporter());
}
@NonNull
public static Monster fromJSON(String json) {
return fromJSON(json, null);
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;
}
@NonNull
public static Monster fromJSON(String json, @Nullable String fileName) {
if (json == null || json.trim().isEmpty()) {
throw new IllegalArgumentException("JSON payload is null or empty");
public static class Helpers {
public static String getString(JsonObject dict, String name) {
return getString(dict, name, "");
}
// 1. Check for explicit $schema reference matching monster-card or tetracube schema
try {
JsonElement el = JsonParser.parseString(json);
if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject();
if (obj.has("$schema")) {
String schemaVal = obj.get("$schema").getAsString();
if (schemaVal.contains("monster-card.schema.json")) {
return new MonsterJsonImporter().parse(json);
} else if (schemaVal.contains("tetracube-monster.schema.json")) {
return new TetraCubeMonsterImporter().parse(json);
}
}
}
} catch (Exception e) {
Logger.logError("Error checking $schema reference tag", e);
public static String getString(@NotNull JsonObject dict, String name, String defaultValue) {
if (dict.has(name)) {
return dict.get(name).getAsString();
}
// 2. If filename ends in .monster, try TetraCube importer first
String lowerFileName = fileName != null ? fileName.toLowerCase(Locale.ROOT) : "";
boolean isMonsterFile = lowerFileName.endsWith(".monster") || lowerFileName.endsWith(".monster.txt");
if (isMonsterFile) {
TetraCubeMonsterImporter tetraImporter = new TetraCubeMonsterImporter();
if (tetraImporter.canImport(json)) {
try {
return tetraImporter.parse(json);
} catch (Exception e) {
Logger.logError("TetraCube importer failed for .monster file: " + fileName, e);
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;
}
}
}
}
}
}
// 3. Fallback to current importer detection chain
for (EntityImporter<Monster> importer : IMPORTERS) {
if (importer.canImport(json)) {
try {
return importer.parse(json);
} catch (Exception e) {
Logger.logError("Importer " + importer.getClass().getSimpleName() + " failed", e);
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);
}
}
}
throw new IllegalArgumentException("No importer found capable of parsing the given JSON payload.");
}
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

@@ -1,11 +1,6 @@
package com.majinnaibu.monstercards.helpers;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collection;
@SuppressWarnings({"RedundantIfStatement"})
@SuppressWarnings({"BooleanMethodIsAlwaysInverted", "RedundantIfStatement"})
public final class StringHelper {
public static boolean isNullOrEmpty(CharSequence value) {
if (value == null) {
@@ -18,62 +13,4 @@ public final class StringHelper {
return false;
}
@NonNull
public static String join(String delimiter, @NonNull Collection<String> strings) {
int length = strings.size();
if (length < 1) {
return "";
} else {
StringBuilder sb = new StringBuilder();
boolean isFirst = true;
for (String element : strings) {
if (!isFirst) {
sb.append(delimiter);
}
sb.append(element);
isFirst = false;
}
return sb.toString();
}
}
public static String oxfordJoin(String delimiter, String lastDelimiter, String onlyDelimiter, @NonNull Collection<String> strings) {
int length = strings.size();
if (length < 1) {
return "";
} else if (length == 2) {
return join(onlyDelimiter, strings);
} else {
StringBuilder sb = new StringBuilder();
int index = 0;
int lastIndex = length - 1;
for (String element : strings) {
if (index > 0 && index < lastIndex) {
sb.append(delimiter);
} else if (index > 0 && index >= lastIndex) {
sb.append(lastDelimiter);
}
sb.append(element);
index++;
}
return sb.toString();
}
}
@Nullable
public static Integer parseInt(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException _ex) {
return null;
}
}
public static boolean containsCaseInsensitive(@NonNull String text, @NonNull String search) {
// TODO: find a locale independent way to do this
return text.toLowerCase().contains(search.toLowerCase());
}
}

View File

@@ -1,43 +0,0 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.majinnaibu.monstercards.models.BinderExport;
public class BinderImporter implements EntityImporter<BinderExport> {
private static final Gson GSON = new Gson();
@Override
public boolean canImport(@NonNull String input) {
try {
JsonElement el = JsonParser.parseString(input);
if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject();
if (obj.has("$schema")) {
String schemaVal = obj.get("$schema").getAsString();
if (schemaVal.contains("binder.schema.json")) {
return true;
}
}
return obj.has("collections") && obj.get("collections").isJsonArray();
}
} catch (Exception ignored) {
}
return false;
}
@NonNull
@Override
public BinderExport parse(@NonNull String input) throws Exception {
BinderExport binder = GSON.fromJson(input, BinderExport.class);
if (binder == null || binder.collections == null) {
throw new IllegalArgumentException("Failed to deserialize Binder JSON");
}
return binder;
}
}

View File

@@ -1,418 +0,0 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.models.Trait;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
public class DnDBeyondImporter implements EntityImporter<Monster> {
private static final Pattern DNDBEYOND_URL_PATTERN = Pattern.compile("https?://(?:www\\.)?dndbeyond\\.com/characters/(\\d+)(?:/([a-zA-Z0-9]+))?", Pattern.CASE_INSENSITIVE);
private static final Pattern CHARACTER_ID_PATTERN = Pattern.compile("^\\d+$");
@Override
public boolean canImport(@NonNull String input) {
String trimmed = input.trim();
if (trimmed.isEmpty()) {
return false;
}
if (DNDBEYOND_URL_PATTERN.matcher(trimmed).find() || CHARACTER_ID_PATTERN.matcher(trimmed).matches()) {
return true;
}
try {
JsonElement element = JsonParser.parseString(trimmed);
if (element.isJsonObject()) {
JsonObject root = element.getAsJsonObject();
if (root.has("data") && root.getAsJsonObject("data").has("baseHitPoints")) {
return true;
}
}
} catch (Exception ignored) {
}
return false;
}
@NonNull
@Override
public Monster parse(@NonNull String input) throws Exception {
String trimmed = input.trim();
JsonObject characterData;
String sourceUrl = "";
if (trimmed.startsWith("{")) {
JsonObject root = JsonParser.parseString(trimmed).getAsJsonObject();
characterData = root.has("data") ? root.getAsJsonObject("data") : root;
} else {
String characterId = extractCharacterId(trimmed);
if (characterId == null) {
throw new IllegalArgumentException("Could not extract D&D Beyond character ID from input: " + input);
}
sourceUrl = trimmed.startsWith("http") ? trimmed : "https://www.dndbeyond.com/characters/" + characterId;
String jsonPayload = fetchCharacterJson(characterId);
JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();
if (!root.has("data") || root.get("data").isJsonNull()) {
throw new IllegalArgumentException("D&D Beyond response did not contain character data.");
}
characterData = root.getAsJsonObject("data");
}
return buildMonsterFromCharacterData(characterData, sourceUrl);
}
@Nullable
public static String extractCharacterId(@NonNull String input) {
String trimmed = input.trim();
Matcher urlMatcher = DNDBEYOND_URL_PATTERN.matcher(trimmed);
if (urlMatcher.find()) {
return urlMatcher.group(1);
}
if (CHARACTER_ID_PATTERN.matcher(trimmed).matches()) {
return trimmed;
}
return null;
}
@NonNull
private String fetchCharacterJson(@NonNull String characterId) throws Exception {
String serviceUrl = "https://character-service.dndbeyond.com/character/v5/character/" + characterId + "?includeCustomItems=true";
URL url = new URL(serviceUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Android; Mobile)");
conn.setRequestProperty("Accept", "application/json");
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
int code = conn.getResponseCode();
if (code != 200) {
throw new IllegalStateException("D&D Beyond service returned HTTP " + code);
}
StringBuilder sb = new StringBuilder();
try (InputStream in = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
return sb.toString();
}
@NonNull
private Monster buildMonsterFromCharacterData(@NonNull JsonObject data, @NonNull String sourceUrl) {
Monster monster = new Monster();
monster.sourceUrl = sourceUrl;
String rawName = getString(data, "name");
if (rawName.startsWith("\"") && rawName.endsWith("\"") && rawName.length() > 1) {
rawName = rawName.substring(1, rawName.length() - 1);
}
monster.name = rawName.isEmpty() ? "D&D Beyond Character" : rawName;
monster.size = "Medium";
monster.type = "Humanoid";
parseClassesAndLevel(data, monster);
parseStats(data, monster);
parseHitPoints(data, monster);
parseSpeedAndSenses(data, monster);
parseModifiers(data, monster);
parseTraitsAndFeatures(data, monster);
parseActionsAndAttacks(data, monster);
return monster;
}
private void parseClassesAndLevel(@NonNull JsonObject data, @NonNull Monster monster) {
StringBuilder classSummary = new StringBuilder();
int totalLevel = 0;
if (data.has("classes") && data.get("classes").isJsonArray()) {
JsonArray classes = data.getAsJsonArray("classes");
for (int i = 0; i < classes.size(); i++) {
JsonObject cls = classes.get(i).getAsJsonObject();
int level = getInt(cls, "level", 1);
totalLevel += level;
String className = "";
if (cls.has("definition") && cls.get("definition").isJsonObject()) {
className = getString(cls.getAsJsonObject("definition"), "name");
}
String subclassName = "";
if (cls.has("subclassDefinition") && cls.get("subclassDefinition").isJsonObject()) {
subclassName = getString(cls.getAsJsonObject("subclassDefinition"), "name");
}
if (classSummary.length() > 0) {
classSummary.append(" / ");
}
classSummary.append(className);
if (!subclassName.isEmpty()) {
classSummary.append(" (").append(subclassName).append(")");
}
classSummary.append(" ").append(level);
}
}
monster.hitDice = totalLevel > 0 ? totalLevel : 1;
String raceName = "";
if (data.has("race") && data.get("race").isJsonObject()) {
JsonObject race = data.getAsJsonObject("race");
raceName = getString(race, "fullName");
if (raceName.isEmpty()) {
raceName = getString(race, "baseRaceName");
}
}
StringBuilder subtypeSb = new StringBuilder();
if (totalLevel > 0) {
subtypeSb.append("Level ").append(totalLevel);
}
if (!raceName.isEmpty()) {
if (subtypeSb.length() > 0) subtypeSb.append(" ");
subtypeSb.append(raceName);
}
if (classSummary.length() > 0) {
if (subtypeSb.length() > 0) subtypeSb.append(" ");
subtypeSb.append(classSummary);
}
monster.subtype = subtypeSb.toString();
}
private void parseStats(@NonNull JsonObject data, @NonNull Monster monster) {
if (data.has("stats") && data.get("stats").isJsonArray()) {
JsonArray stats = data.getAsJsonArray("stats");
for (int i = 0; i < stats.size(); i++) {
JsonObject stat = stats.get(i).getAsJsonObject();
int id = getInt(stat, "id");
int val = getInt(stat, "value", 10);
switch (id) {
case 1: monster.strengthScore = val; break;
case 2: monster.dexterityScore = val; break;
case 3: monster.constitutionScore = val; break;
case 4: monster.intelligenceScore = val; break;
case 5: monster.wisdomScore = val; break;
case 6: monster.charismaScore = val; break;
}
}
}
}
private void parseHitPoints(@NonNull JsonObject data, @NonNull Monster monster) {
int baseHp = getInt(data, "baseHitPoints");
int overrideHp = getInt(data, "overrideHitPoints");
int maxHp = overrideHp > 0 ? overrideHp : baseHp;
if (maxHp > 0) {
monster.hasCustomHP = true;
monster.customHPDescription = String.format(Locale.ROOT, "%d (%dd%d)", maxHp, monster.hitDice, 8);
}
}
private void parseSpeedAndSenses(@NonNull JsonObject data, @NonNull Monster monster) {
if (data.has("race") && data.get("race").isJsonObject()) {
JsonObject race = data.getAsJsonObject("race");
if (race.has("weightSpeeds") && race.get("weightSpeeds").isJsonObject()) {
JsonObject weightSpeeds = race.getAsJsonObject("weightSpeeds");
if (weightSpeeds.has("normal") && weightSpeeds.get("normal").isJsonObject()) {
JsonObject normal = weightSpeeds.getAsJsonObject("normal");
monster.walkSpeed = getInt(normal, "walk", 30);
monster.flySpeed = getInt(normal, "fly", 0);
monster.burrowSpeed = getInt(normal, "burrow", 0);
monster.swimSpeed = getInt(normal, "swim", 0);
monster.climbSpeed = getInt(normal, "climb", 0);
}
}
}
if (monster.walkSpeed == 0) {
monster.walkSpeed = 30;
}
}
private void parseModifiers(@NonNull JsonObject data, @NonNull Monster monster) {
if (!data.has("modifiers") || !data.get("modifiers").isJsonObject()) {
return;
}
JsonObject modifiersObj = data.getAsJsonObject("modifiers");
String[] categories = new String[]{"race", "class", "background", "item", "feat"};
for (String cat : categories) {
if (modifiersObj.has(cat) && modifiersObj.get(cat).isJsonArray()) {
JsonArray mods = modifiersObj.getAsJsonArray(cat);
for (int i = 0; i < mods.size(); i++) {
JsonObject mod = mods.get(i).getAsJsonObject();
String type = getString(mod, "type");
String subType = getString(mod, "subType");
int fixedVal = getInt(mod, "fixedValue");
if ("set-base".equals(type) && "darkvision".equals(subType)) {
int dist = fixedVal > 0 ? fixedVal : 60;
monster.senses.add("darkvision " + dist + " ft.");
} else if ("language".equals(type)) {
String langName = capitalize(subType.replace("-", " "));
monster.languages.add(new Language(langName, true));
} else if ("proficiency".equals(type)) {
if (subType.endsWith("-saving-throws")) {
String stat = subType.replace("-saving-throws", "");
applySavingThrowProficiency(monster, stat);
} else {
applySkillProficiency(monster, subType, ProficiencyType.PROFICIENT);
}
} else if ("expertise".equals(type)) {
applySkillProficiency(monster, subType, ProficiencyType.EXPERTISE);
} else if ("immunity".equals(type)) {
monster.damageImmunities.add(capitalize(subType.replace("-", " ")));
} else if ("resistance".equals(type)) {
monster.damageResistances.add(capitalize(subType.replace("-", " ")));
} else if ("vulnerability".equals(type)) {
monster.damageVulnerabilities.add(capitalize(subType.replace("-", " ")));
}
}
}
}
}
private void applySavingThrowProficiency(@NonNull Monster monster, String stat) {
if ("strength".equals(stat) || "str".equals(stat)) monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if ("dexterity".equals(stat) || "dex".equals(stat)) monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
else if ("constitution".equals(stat) || "con".equals(stat)) monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if ("intelligence".equals(stat) || "int".equals(stat)) monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if ("wisdom".equals(stat) || "wis".equals(stat)) monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if ("charisma".equals(stat) || "cha".equals(stat)) monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
}
private void applySkillProficiency(@NonNull Monster monster, String subType, ProficiencyType profType) {
String skillName = capitalize(subType.replace("-", " "));
AbilityScore abilityScore = AbilityScore.STRENGTH;
if ("Acrobatics".equalsIgnoreCase(skillName) || "Sleight Of Hand".equalsIgnoreCase(skillName) || "Stealth".equalsIgnoreCase(skillName)) {
abilityScore = AbilityScore.DEXTERITY;
} else if ("Arcana".equalsIgnoreCase(skillName) || "History".equalsIgnoreCase(skillName) || "Investigation".equalsIgnoreCase(skillName) || "Nature".equalsIgnoreCase(skillName) || "Religion".equalsIgnoreCase(skillName)) {
abilityScore = AbilityScore.INTELLIGENCE;
} else if ("Animal Handling".equalsIgnoreCase(skillName) || "Insight".equalsIgnoreCase(skillName) || "Medicine".equalsIgnoreCase(skillName) || "Perception".equalsIgnoreCase(skillName) || "Survival".equalsIgnoreCase(skillName)) {
abilityScore = AbilityScore.WISDOM;
} else if ("Deception".equalsIgnoreCase(skillName) || "Intimidation".equalsIgnoreCase(skillName) || "Performance".equalsIgnoreCase(skillName) || "Persuasion".equalsIgnoreCase(skillName)) {
abilityScore = AbilityScore.CHARISMA;
} else if ("Athletics".equalsIgnoreCase(skillName)) {
abilityScore = AbilityScore.STRENGTH;
}
monster.skills.add(new Skill(skillName, abilityScore, AdvantageType.NONE, profType));
}
private void parseTraitsAndFeatures(@NonNull JsonObject data, @NonNull Monster monster) {
if (data.has("race") && data.get("race").isJsonObject()) {
JsonObject race = data.getAsJsonObject("race");
if (race.has("racialTraits") && race.get("racialTraits").isJsonArray()) {
JsonArray traits = race.getAsJsonArray("racialTraits");
for (int i = 0; i < traits.size(); i++) {
JsonObject tr = traits.get(i).getAsJsonObject();
if (tr.has("definition") && tr.get("definition").isJsonObject()) {
JsonObject def = tr.getAsJsonObject("definition");
String name = getString(def, "name");
String desc = stripHtml(getString(def, "snippet"));
if (desc.isEmpty()) {
desc = stripHtml(getString(def, "description"));
}
if (!name.isEmpty() && !desc.isEmpty()) {
monster.abilities.add(new Trait(name, desc));
}
}
}
}
}
}
private void parseActionsAndAttacks(@NonNull JsonObject data, @NonNull Monster monster) {
if (data.has("actions") && data.get("actions").isJsonObject()) {
JsonObject actionsObj = data.getAsJsonObject("actions");
String[] categories = new String[]{"class", "race", "background", "item", "feat"};
for (String cat : categories) {
if (actionsObj.has(cat) && actionsObj.get(cat).isJsonArray()) {
JsonArray acts = actionsObj.getAsJsonArray(cat);
for (int i = 0; i < acts.size(); i++) {
JsonObject act = acts.get(i).getAsJsonObject();
String name = getString(act, "name");
String desc = stripHtml(getString(act, "snippet"));
if (desc.isEmpty()) {
desc = stripHtml(getString(act, "description"));
}
int actType = 1;
if (act.has("activation") && act.get("activation").isJsonObject()) {
actType = getInt(act.getAsJsonObject("activation"), "activationType", 1);
}
if (!name.isEmpty()) {
Trait trait = new Trait(name, desc);
if (actType == 3) {
monster.bonusActions.add(trait);
} else if (actType == 4) {
monster.reactions.add(trait);
} else {
monster.actions.add(trait);
}
}
}
}
}
}
}
private static String getString(@NonNull JsonObject obj, String key) {
return getString(obj, key, "");
}
private static String getString(@NonNull JsonObject obj, String key, String defaultVal) {
if (obj.has(key) && !obj.get(key).isJsonNull()) {
return obj.get(key).getAsString();
}
return defaultVal;
}
private static int getInt(@NonNull JsonObject obj, String key) {
return getInt(obj, key, 0);
}
private static int getInt(@NonNull JsonObject obj, String key, int defaultVal) {
if (obj.has(key) && !obj.get(key).isJsonNull()) {
try {
return obj.get(key).getAsInt();
} catch (Exception ignored) {
}
}
return defaultVal;
}
private static String stripHtml(String html) {
if (html == null) return "";
return html.replaceAll("<[^>]*>", "").replaceAll("&nbsp;", " ").trim();
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
String[] words = str.split("\\s+");
StringBuilder sb = new StringBuilder();
for (String w : words) {
if (!w.isEmpty()) {
if (sb.length() > 0) sb.append(" ");
sb.append(Character.toUpperCase(w.charAt(0))).append(w.substring(1).toLowerCase(Locale.ROOT));
}
}
return sb.toString();
}
}

View File

@@ -1,16 +0,0 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
public interface EntityImporter<T> {
/**
* Determines whether the given input string or payload can be handled by this importer.
*/
boolean canImport(@NonNull String input);
/**
* Parses the raw input string into internal domain memory objects (e.g., Monster).
*/
@NonNull
T parse(@NonNull String input) throws Exception;
}

View File

@@ -1,230 +0,0 @@
package com.majinnaibu.monstercards.importers;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.MonsterCardsApplication;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.ImportSource;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.utils.Logger;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.function.BooleanSupplier;
public class GitRepoImporterService {
public static int importFromGitRepository(Context context, ImportSource source, BooleanSupplier isCancelled) throws Exception {
int totalImported = 0;
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
if (!downloadsDir.exists()) {
downloadsDir.mkdirs();
}
File tempZip = null;
File[] existingZips = downloadsDir.listFiles((dir, name) -> name.startsWith(source.id + "_download") && name.endsWith(".zip"));
if (existingZips != null && existingZips.length > 0) {
tempZip = existingZips[0];
} else {
tempZip = new File(downloadsDir, source.id + "_download_" + System.currentTimeMillis() + ".zip");
downloadZip(source.downloadUrl, tempZip, isCancelled);
}
File extractDir = new File(context.getCacheDir(), "repo_extracted_" + System.currentTimeMillis());
try {
if (isCancelled.getAsBoolean()) return 0;
if (!extractDir.exists()) {
extractDir.mkdirs();
}
int extractedFiles = unzipAndFilter(tempZip, extractDir, source.fileExtension, source.subfolder, isCancelled);
Logger.logWTF("Extracted " + extractedFiles + " files to " + extractDir.getAbsolutePath());
if (isCancelled.getAsBoolean()) return 0;
// Instantiate Importer dynamically
Class<?> clazz = Class.forName(source.importerClassName);
@SuppressWarnings("unchecked")
EntityImporter<Monster> importer = (EntityImporter<Monster>) clazz.newInstance();
MonsterRepository repository = ((MonsterCardsApplication) context.getApplicationContext()).getMonsterRepository();
// Process files
totalImported = processDirectory(extractDir, importer, repository, isCancelled);
} finally {
// We no longer delete tempZip since the user wants to keep the downloaded zip
// deleteFileOrDir(tempZip);
// DO NOT DELETE EXTRACT DIR FOR NOW
// deleteFileOrDir(extractDir);
Logger.logWTF("Skipping deletion of extract directory: " + extractDir.getAbsolutePath());
}
return totalImported;
}
private static void downloadZip(String urlStr, File dest, BooleanSupplier isCancelled) throws IOException, InterruptedException {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(15000);
conn.setReadTimeout(30000);
int code = conn.getResponseCode();
// GitHub might redirect archive links, so handle redirect
if (code == HttpURLConnection.HTTP_MOVED_TEMP || code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_SEE_OTHER) {
String newUrl = conn.getHeaderField("Location");
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setConnectTimeout(15000);
conn.setReadTimeout(30000);
code = conn.getResponseCode();
}
if (code != 200) {
throw new IllegalStateException("Git repo download failed with HTTP " + code);
}
try (InputStream in = new BufferedInputStream(conn.getInputStream());
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int count;
while ((count = in.read(buffer)) != -1) {
if (isCancelled.getAsBoolean()) {
throw new InterruptedException("Git download was cancelled.");
}
out.write(buffer, 0, count);
}
}
}
private static int unzipAndFilter(File zipFile, File extractDir, String filterExtension, String subfolder, BooleanSupplier isCancelled) throws InterruptedException {
int extractedCount = 0;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (isCancelled.getAsBoolean()) {
try {
zis.closeEntry();
} catch (Exception e) {
Logger.logError("Failed to close zip entry upon cancellation", e);
}
throw new InterruptedException("Git extraction was cancelled.");
}
if (!entry.isDirectory()) {
String name = entry.getName();
boolean inSubfolder = subfolder == null || subfolder.isEmpty() || name.contains("/" + subfolder + "/") || name.startsWith(subfolder + "/");
if (inSubfolder && (filterExtension == null || name.endsWith(filterExtension))) {
File outFile = new File(extractDir, new File(name).getName());
if (outFile.exists()) {
outFile = new File(extractDir, System.currentTimeMillis() + "_" + new File(name).getName());
}
try (FileOutputStream fos = new FileOutputStream(outFile)) {
byte[] buffer = new byte[8192];
int count;
while ((count = zis.read(buffer)) != -1) {
if (isCancelled.getAsBoolean()) {
throw new InterruptedException("Git extraction was cancelled during file read.");
}
fos.write(buffer, 0, count);
}
} catch (InterruptedException e) {
throw e;
} catch (Exception e) {
Logger.logError("Failed to write extracted file to disk: " + name, e);
}
}
}
try {
zis.closeEntry();
} catch (Exception e) {
Logger.logError("Failed to close zip entry", e);
}
}
} catch (InterruptedException e) {
throw e;
} catch (Exception e) {
Logger.logError("Failed during unzip stream processing", e);
}
return extractedCount;
}
private static int processDirectory(File dir, EntityImporter<Monster> importer, MonsterRepository repository, BooleanSupplier isCancelled) throws InterruptedException {
int importedCount = 0;
File[] files = dir.listFiles();
if (files == null) {
Logger.logWTF("processDirectory: files array is null for " + dir.getAbsolutePath());
return 0;
}
Logger.logWTF("Attempting to process " + files.length + " JSON files.");
for (File file : files) {
if (isCancelled.getAsBoolean()) {
throw new InterruptedException("Git import processing was cancelled.");
}
if (file.isFile()) {
String content = readFileContent(file);
if (content != null) {
if (importer.canImport(content)) {
try {
Monster monster = importer.parse(content);
repository.saveMonster(monster).blockingAwait();
importedCount++;
} catch (Exception e) {
Logger.logError("Failed to parse/save monster from file: " + file.getName(), e);
}
} else {
Logger.logWTF("Importer rejected file: " + file.getName());
}
} else {
Logger.logError("Failed to read content from file: " + file.getName());
}
}
}
return importedCount;
}
private static String readFileContent(File file) {
StringBuilder sb = new StringBuilder();
try (InputStream in = new FileInputStream(file);
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
} catch (IOException e) {
Logger.logError("Error reading extracted file", e);
return null;
}
return sb.toString();
}
private static void deleteFileOrDir(File fileOrDir) {
if (fileOrDir.isDirectory()) {
File[] children = fileOrDir.listFiles();
if (children != null) {
for (File child : children) {
deleteFileOrDir(child);
}
}
}
fileOrDir.delete();
}
}

View File

@@ -1,43 +0,0 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.majinnaibu.monstercards.models.Monster;
public class MonsterJsonImporter implements EntityImporter<Monster> {
private static final Gson GSON = new Gson();
@Override
public boolean canImport(@NonNull String input) {
try {
JsonElement el = JsonParser.parseString(input);
if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject();
if (obj.has("$schema")) {
String schemaVal = obj.get("$schema").getAsString();
if (schemaVal.contains("monster-card.schema.json")) {
return true;
}
}
return obj.has("name") && (obj.has("strengthScore") || obj.has("strengthSavingThrowProficiency"));
}
} catch (Exception ignored) {
}
return false;
}
@NonNull
@Override
public Monster parse(@NonNull String input) throws Exception {
Monster monster = GSON.fromJson(input, Monster.class);
if (monster == null) {
throw new IllegalArgumentException("Failed to deserialize Monster JSON");
}
return monster;
}
}

View File

@@ -1,92 +0,0 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.majinnaibu.monstercards.models.Monster;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;
import com.majinnaibu.monstercards.utils.Logger;
public class Open5eApiWrapper {
private static final String BASE_URL = "https://api.open5e.com/v2/creatures/";
public static class Open5ePageResult {
public List<Monster> monsters = new ArrayList<>();
@Nullable public String nextUrl;
}
@NonNull
public static Open5ePageResult fetchPage(@Nullable String urlStr, BooleanSupplier isCancelled) throws Exception {
if (urlStr == null || urlStr.isEmpty()) {
urlStr = BASE_URL;
}
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Android; Mobile)");
conn.setRequestProperty("Accept", "application/json");
conn.setConnectTimeout(15000);
conn.setReadTimeout(15000);
int code = conn.getResponseCode();
if (code != 200) {
throw new IllegalStateException("Open5e API returned HTTP " + code);
}
StringBuilder sb = new StringBuilder();
try (InputStream in = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
if (isCancelled.getAsBoolean()) {
throw new InterruptedException("Open5e network read was cancelled.");
}
sb.append(line);
}
} catch (InterruptedIOException e) {
throw new InterruptedException("Open5e network read was cancelled.");
}
String jsonPayload = sb.toString();
JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();
Open5ePageResult result = new Open5ePageResult();
if (root.has("next") && !root.get("next").isJsonNull()) {
result.nextUrl = root.get("next").getAsString();
}
if (root.has("results") && root.get("results").isJsonArray()) {
JsonArray results = root.getAsJsonArray("results");
Open5eImporter importer = new Open5eImporter();
for (int i = 0; i < results.size(); i++) {
JsonElement el = results.get(i);
if (el.isJsonObject()) {
try {
Monster monster = importer.parse(el.toString());
result.monsters.add(monster);
} catch (Exception e) {
Logger.logError("Failed to parse Open5e monster", e);
}
}
}
}
return result;
}
}

View File

@@ -1,450 +0,0 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.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;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Open5eImporter implements EntityImporter<Monster> {
private static final Pattern HIT_DICE_PATTERN = Pattern.compile("(\\d+)d(\\d+)");
@Override
public boolean canImport(@NonNull String input) {
String trimmed = input.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
return false;
}
try {
JsonElement element = JsonParser.parseString(trimmed);
if (!element.isJsonObject()) {
return false;
}
JsonObject root = element.getAsJsonObject();
if (root.has("ruleset_id") && root.get("ruleset_id").getAsString().equals("open5e")) {
return true;
}
if (root.has("properties") && root.getAsJsonObject("properties").has("abilities")) {
return true;
}
} catch (Exception ignored) {
}
return false;
}
@NonNull
@Override
public Monster parse(@NonNull String input) throws Exception {
JsonObject root = JsonParser.parseString(input.trim()).getAsJsonObject();
Monster monster = new Monster();
if (root.has("uuid") && !root.get("uuid").isJsonNull()) {
try {
monster.id = UUID.fromString(root.get("uuid").getAsString());
} catch (Exception ignored) {
}
}
if (root.has("display_name") && !root.get("display_name").isJsonNull()) {
monster.name = root.get("display_name").getAsString();
}
JsonObject props = root.has("properties") ? root.getAsJsonObject("properties") : root;
if (monster.name == null || monster.name.isEmpty()) {
monster.name = getString(props, "name", "Unnamed Entity");
}
monster.size = getString(props, "size");
monster.type = getString(props, "type");
monster.subtype = getString(props, "subtype");
monster.alignment = getString(props, "alignment");
parseArmorClass(props, monster);
parseHitPoints(props, monster);
if (props.has("speed") && props.get("speed").isJsonObject()) {
JsonObject speedObj = props.getAsJsonObject("speed");
monster.walkSpeed = getInt(speedObj, "walk", 30);
monster.flySpeed = getInt(speedObj, "fly", 0);
monster.swimSpeed = getInt(speedObj, "swim", 0);
monster.burrowSpeed = getInt(speedObj, "burrow", 0);
monster.climbSpeed = getInt(speedObj, "climb", 0);
} else {
monster.walkSpeed = getInt(props, "speed", 30);
}
if (props.has("speed_desc")) {
String speedDesc = getString(props, "speed_desc");
if (!speedDesc.isEmpty()) {
monster.hasCustomSpeed = true;
monster.customSpeedDescription = speedDesc;
}
}
parseChallengeRating(props, monster);
parseAbilities(props, monster);
parseIdentityFields(props, monster);
parseSavingThrows(props, monster);
parseSkills(props, monster);
parseSenses(props, monster);
parseLanguages(props, monster);
parseDamageAndConditions(props, monster);
parseTraitsAndActions(props, monster);
if (props.has("source_url")) {
monster.sourceUrl = getString(props, "source_url");
}
return monster;
}
private void parseArmorClass(JsonObject props, Monster monster) {
if (props.has("equipped_armor") && props.get("equipped_armor").isJsonObject()) {
JsonObject armorObj = props.getAsJsonObject("equipped_armor");
String armorId = getString(armorObj, "id");
ArmorType armorType = ArmorType.valueOfString(armorId);
if (armorType != ArmorType.NONE) {
monster.armorType = armorType;
}
}
if (props.has("equipped_shield") && props.get("equipped_shield").isJsonObject()) {
JsonObject shieldObj = props.getAsJsonObject("equipped_shield");
monster.shieldBonus = getInt(shieldObj, "ac_bonus", 2);
}
if (monster.armorType == ArmorType.NONE && props.has("armor_description")) {
String armorDesc = getString(props, "armor_description");
if (!armorDesc.isEmpty()) {
monster.otherArmorDescription = armorDesc;
}
}
}
private void parseHitPoints(JsonObject props, Monster monster) {
if (props.has("hit_points") && props.get("hit_points").isJsonObject()) {
JsonObject hpObj = props.getAsJsonObject("hit_points");
int maxHp = getInt(hpObj, "max", getInt(hpObj, "current", 10));
if (hpObj.has("hit_dice") && !hpObj.get("hit_dice").isJsonNull()) {
String hitDiceStr = hpObj.get("hit_dice").getAsString();
Matcher m = HIT_DICE_PATTERN.matcher(hitDiceStr);
if (m.find()) {
monster.hitDice = Integer.parseInt(m.group(1));
}
}
if (hpObj.has("formula") && !hpObj.get("formula").isJsonNull()) {
monster.hasCustomHP = true;
monster.customHPDescription = String.format(Locale.ROOT, "%d (%s)", maxHp, hpObj.get("formula").getAsString());
}
}
}
private void parseChallengeRating(JsonObject props, Monster monster) {
String crStr = "";
if (props.has("cr") && !props.get("cr").isJsonNull()) {
crStr = props.get("cr").getAsString();
} else if (props.has("challenge_rating") && !props.get("challenge_rating").isJsonNull()) {
crStr = props.get("challenge_rating").getAsString();
}
if (!crStr.isEmpty()) {
crStr = crStr.replace(".0", "");
if (crStr.equals("0.125")) crStr = "1/8";
else if (crStr.equals("0.25")) crStr = "1/4";
else if (crStr.equals("0.5")) crStr = "1/2";
monster.challengeRating = ChallengeRating.valueOfString(crStr);
}
}
private void parseAbilities(JsonObject props, Monster monster) {
if (!props.has("abilities") || !props.get("abilities").isJsonObject()) {
return;
}
JsonObject abs = props.getAsJsonObject("abilities");
monster.strengthScore = extractAbilityScore(abs, "strength");
monster.dexterityScore = extractAbilityScore(abs, "dexterity");
monster.constitutionScore = extractAbilityScore(abs, "constitution");
monster.intelligenceScore = extractAbilityScore(abs, "intelligence");
monster.wisdomScore = extractAbilityScore(abs, "wisdom");
monster.charismaScore = extractAbilityScore(abs, "charisma");
}
private int extractAbilityScore(JsonObject abs, String key) {
if (!abs.has(key) || abs.get(key).isJsonNull()) {
return 10;
}
JsonElement el = abs.get(key);
if (el.isJsonPrimitive()) {
return el.getAsInt();
} else if (el.isJsonObject() && el.getAsJsonObject().has("value")) {
return el.getAsJsonObject().get("value").getAsInt();
}
return 10;
}
private void parseIdentityFields(JsonObject props, Monster monster) {
monster.playerName = getString(props, "player_name");
monster.background = getString(props, "background");
monster.personalityTraits = getString(props, "personality_traits");
monster.ideals = getString(props, "ideals");
monster.bonds = getString(props, "bonds");
monster.flaws = getString(props, "flaws");
if (props.has("character_details") && props.get("character_details").isJsonObject()) {
JsonObject details = props.getAsJsonObject("character_details");
monster.age = getString(details, "age");
monster.height = getString(details, "height");
monster.weight = getString(details, "weight");
monster.eyes = getString(details, "eyes");
monster.skin = getString(details, "skin");
monster.hair = getString(details, "hair");
monster.appearance = getString(details, "appearance");
monster.backstory = getString(details, "backstory");
monster.alliesAndOrganizations = getString(details, "allies_and_organizations");
}
}
private void parseSavingThrows(JsonObject props, Monster monster) {
if (!props.has("saving_throws")) return;
JsonElement savesEl = props.get("saving_throws");
if (savesEl.isJsonObject()) {
JsonObject savesObj = savesEl.getAsJsonObject();
for (String key : savesObj.keySet()) {
applySaveProficiency(monster, key.toLowerCase(Locale.ROOT));
}
} else if (savesEl.isJsonArray()) {
JsonArray arr = savesEl.getAsJsonArray();
for (int i = 0; i < arr.size(); i++) {
String saveStr = arr.get(i).getAsString().toLowerCase(Locale.ROOT);
applySaveProficiency(monster, saveStr);
}
}
}
private void applySaveProficiency(Monster monster, String saveStr) {
if (saveStr.contains("str")) monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("dex")) monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("con")) monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("int")) monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("wis")) monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
else if (saveStr.contains("cha")) monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
}
private void parseSkills(JsonObject props, Monster monster) {
if (props.has("skill_bonuses") && props.get("skill_bonuses").isJsonObject()) {
JsonObject skillsObj = props.getAsJsonObject("skill_bonuses");
for (String key : skillsObj.keySet()) {
AbilityScore stat = getAbilityForSkill(key);
monster.skills.add(new Skill(capitalize(key.replace("_", " ")), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
}
} else if (props.has("skills")) {
JsonElement skillsEl = props.get("skills");
if (skillsEl.isJsonArray()) {
JsonArray arr = skillsEl.getAsJsonArray();
for (int i = 0; i < arr.size(); i++) {
String skillStr = arr.get(i).getAsString();
String skillName = skillStr.replaceAll("[+-]\\d+", "").trim();
if (!skillName.isEmpty()) {
AbilityScore stat = getAbilityForSkill(skillName);
monster.skills.add(new Skill(capitalize(skillName), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
}
}
}
}
}
private AbilityScore getAbilityForSkill(String skillName) {
String lower = skillName.toLowerCase(Locale.ROOT);
if (lower.contains("acro") || lower.contains("sleight") || lower.contains("stealth")) return AbilityScore.DEXTERITY;
if (lower.contains("arca") || lower.contains("hist") || lower.contains("inve") || lower.contains("natu") || lower.contains("reli")) return AbilityScore.INTELLIGENCE;
if (lower.contains("anim") || lower.contains("insi") || lower.contains("medi") || lower.contains("perc") || lower.contains("surv")) return AbilityScore.WISDOM;
if (lower.contains("dece") || lower.contains("inti") || lower.contains("perf") || lower.contains("pers")) return AbilityScore.CHARISMA;
return AbilityScore.STRENGTH;
}
private void parseSenses(JsonObject props, Monster monster) {
if (props.has("senses")) {
String sensesStr = getString(props, "senses");
if (!sensesStr.isEmpty()) {
String[] parts = sensesStr.split(",");
for (String p : parts) {
if (!p.trim().isEmpty()) {
monster.senses.add(p.trim());
}
}
}
}
}
private void parseLanguages(JsonObject props, Monster monster) {
if (props.has("languages")) {
JsonElement langEl = props.get("languages");
if (langEl.isJsonObject()) {
if (langEl.getAsJsonObject().has("data") && langEl.getAsJsonObject().get("data").isJsonArray()) {
JsonArray arr = langEl.getAsJsonObject().getAsJsonArray("data");
for (int i = 0; i < arr.size(); i++) {
if (arr.get(i).isJsonObject()) {
String langName = getString(arr.get(i).getAsJsonObject(), "name");
if (!langName.isEmpty()) {
monster.languages.add(new Language(langName, true));
}
}
}
} else if (langEl.getAsJsonObject().has("as_string")) {
String langStr = langEl.getAsJsonObject().get("as_string").getAsString();
for (String l : langStr.split(",")) {
if (!l.trim().isEmpty()) {
monster.languages.add(new Language(l.trim(), true));
}
}
}
} else if (langEl.isJsonArray()) {
JsonArray arr = langEl.getAsJsonArray();
for (int i = 0; i < arr.size(); i++) {
String langName = arr.get(i).getAsString();
if (!langName.isEmpty()) {
monster.languages.add(new Language(langName, true));
}
}
} else if (langEl.isJsonPrimitive()) {
String langStr = langEl.getAsString();
for (String l : langStr.split(",")) {
if (!l.trim().isEmpty()) {
monster.languages.add(new Language(l.trim(), true));
}
}
}
}
}
private void parseDamageAndConditions(JsonObject props, Monster monster) {
if (props.has("resistances_and_immunities") && props.get("resistances_and_immunities").isJsonObject()) {
JsonObject resObj = props.getAsJsonObject("resistances_and_immunities");
parseStringSet(resObj, "damage_immunities", monster.damageImmunities);
parseStringSet(resObj, "damage_resistances", monster.damageResistances);
parseStringSet(resObj, "damage_vulnerabilities", monster.damageVulnerabilities);
parseStringSet(resObj, "condition_immunities", monster.conditionImmunities);
} else {
parseStringSet(props, "damage_immunities", monster.damageImmunities);
parseStringSet(props, "damage_resistances", monster.damageResistances);
parseStringSet(props, "damage_vulnerabilities", monster.damageVulnerabilities);
parseStringSet(props, "condition_immunities", monster.conditionImmunities);
}
}
private void parseStringSet(JsonObject props, String key, java.util.Set<String> set) {
if (!props.has(key)) return;
JsonElement el = props.get(key);
if (el.isJsonArray()) {
JsonArray arr = el.getAsJsonArray();
for (int i = 0; i < arr.size(); i++) {
JsonElement item = arr.get(i);
if (item.isJsonObject() && item.getAsJsonObject().has("name")) {
String val = item.getAsJsonObject().get("name").getAsString();
if (!val.isEmpty()) set.add(capitalize(val));
} else if (item.isJsonPrimitive()) {
String val = item.getAsString();
if (!val.isEmpty()) set.add(capitalize(val));
}
}
} else if (el.isJsonPrimitive()) {
for (String val : el.getAsString().split(",")) {
if (!val.trim().isEmpty()) set.add(capitalize(val.trim()));
}
}
}
private void parseTraitsAndActions(JsonObject props, Monster monster) {
parseTraitList(props, "traits", monster.abilities);
parseTraitList(props, "actions", monster.actions);
parseTraitList(props, "bonus_actions", monster.bonusActions);
parseTraitList(props, "reactions", monster.reactions);
parseTraitList(props, "legendary_actions", monster.legendaryActions);
parseTraitList(props, "lair_actions", monster.lairActions);
parseTraitList(props, "regional_effects", monster.regionalActions);
parseTraitList(props, "mythic_actions", monster.mythicActions);
if (props.has("legendary_desc")) monster.legendaryActionsDescription = getString(props, "legendary_desc");
if (props.has("lair_desc")) monster.lairActionsDescription = getString(props, "lair_desc");
if (props.has("lair_end_note")) monster.lairActionsEndNote = getString(props, "lair_end_note");
if (props.has("regional_desc")) monster.regionalActionsDescription = getString(props, "regional_desc");
if (props.has("regional_end_note")) monster.regionalActionsEndNote = getString(props, "regional_end_note");
if (props.has("mythic_desc")) monster.mythicActionsDescription = getString(props, "mythic_desc");
}
private void parseTraitList(JsonObject props, String key, List<Trait> list) {
if (!props.has(key) || !props.get(key).isJsonArray()) return;
JsonArray arr = props.getAsJsonArray(key);
for (int i = 0; i < arr.size(); i++) {
JsonElement el = arr.get(i);
if (el.isJsonObject()) {
JsonObject obj = el.getAsJsonObject();
String name = getString(obj, "name");
String desc = getString(obj, "desc");
if (desc.isEmpty()) desc = getString(obj, "description");
if (!name.isEmpty()) {
list.add(new Trait(name, desc));
}
}
}
}
private static String getString(JsonObject obj, String key) {
return getString(obj, key, "");
}
private static String getString(JsonObject obj, String key, String defaultVal) {
if (obj.has(key) && !obj.get(key).isJsonNull()) {
JsonElement el = obj.get(key);
if (el.isJsonPrimitive()) {
return el.getAsString();
} else if (el.isJsonObject() && el.getAsJsonObject().has("name")) {
return el.getAsJsonObject().get("name").getAsString();
}
}
return defaultVal;
}
private static int getInt(JsonObject obj, String key, int defaultVal) {
if (obj.has(key) && !obj.get(key).isJsonNull()) {
try {
return obj.get(key).getAsInt();
} catch (Exception ignored) {
}
}
return defaultVal;
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
String[] words = str.split("\\s+");
StringBuilder sb = new StringBuilder();
for (String w : words) {
if (!w.isEmpty()) {
if (sb.length() > 0) sb.append(" ");
sb.append(Character.toUpperCase(w.charAt(0))).append(w.substring(1).toLowerCase(Locale.ROOT));
}
}
return sb.toString();
}
}

View File

@@ -1,169 +0,0 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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;
import java.util.Locale;
import java.util.UUID;
public class Pf2eImporter implements EntityImporter<Monster> {
@Override
public boolean canImport(@NonNull String input) {
String trimmed = input.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
return false;
}
try {
JsonObject root = JsonParser.parseString(trimmed).getAsJsonObject();
if (root.has("type") && "npc".equals(root.get("type").getAsString())) {
if (root.has("system") && root.getAsJsonObject("system").has("attributes")) {
return true;
}
}
} catch (Exception ignored) {
}
return false;
}
@NonNull
@Override
public Monster parse(@NonNull String input) throws Exception {
JsonObject root = JsonParser.parseString(input.trim()).getAsJsonObject();
JsonObject system = root.has("system") ? root.getAsJsonObject("system") : new JsonObject();
Monster monster = new Monster();
monster.id = UUID.randomUUID();
// Name
if (root.has("name") && !root.get("name").isJsonNull()) {
monster.name = root.get("name").getAsString();
} else {
monster.name = "Unknown PF2e Monster";
}
// Type / Size / Alignment
if (system.has("traits") && system.get("traits").isJsonObject()) {
JsonObject traits = system.getAsJsonObject("traits");
if (traits.has("size") && traits.get("size").isJsonObject()) {
monster.size = mapPf2eSize(getString(traits.getAsJsonObject("size"), "value"));
}
if (traits.has("value") && traits.get("value").isJsonArray()) {
// PF2e holds type/alignment tags in traits array
StringBuilder typeSb = new StringBuilder();
for (JsonElement el : traits.getAsJsonArray("value")) {
String trait = el.getAsString();
if (typeSb.length() > 0) typeSb.append(", ");
typeSb.append(capitalize(trait));
}
monster.type = typeSb.toString();
}
}
// Ability Scores
if (system.has("abilities") && system.get("abilities").isJsonObject()) {
JsonObject abilities = system.getAsJsonObject("abilities");
monster.strengthScore = 10 + (getInt(abilities.getAsJsonObject("str"), "mod") * 2);
monster.dexterityScore = 10 + (getInt(abilities.getAsJsonObject("dex"), "mod") * 2);
monster.constitutionScore = 10 + (getInt(abilities.getAsJsonObject("con"), "mod") * 2);
monster.intelligenceScore = 10 + (getInt(abilities.getAsJsonObject("int"), "mod") * 2);
monster.wisdomScore = 10 + (getInt(abilities.getAsJsonObject("wis"), "mod") * 2);
monster.charismaScore = 10 + (getInt(abilities.getAsJsonObject("cha"), "mod") * 2);
}
// Attributes (HP, AC, Speed, Level)
if (system.has("attributes") && system.get("attributes").isJsonObject()) {
JsonObject attributes = system.getAsJsonObject("attributes");
if (attributes.has("ac") && attributes.get("ac").isJsonObject()) {
monster.armorType = ArmorType.NONE;
monster.otherArmorDescription = String.valueOf(getInt(attributes.getAsJsonObject("ac"), "value"));
}
if (attributes.has("hp") && attributes.get("hp").isJsonObject()) {
monster.hasCustomHP = true;
monster.customHPDescription = String.valueOf(getInt(attributes.getAsJsonObject("hp"), "max"));
}
if (attributes.has("speed") && attributes.get("speed").isJsonObject()) {
monster.walkSpeed = getInt(attributes.getAsJsonObject("speed"), "value");
}
}
// Level (Mapped to CR)
if (system.has("details") && system.get("details").isJsonObject()) {
JsonObject details = system.getAsJsonObject("details");
if (details.has("level") && details.get("level").isJsonObject()) {
int level = getInt(details.getAsJsonObject("level"), "value");
monster.challengeRating = mapLevelToCr(level);
}
if (details.has("languages") && details.get("languages").isJsonObject()) {
JsonObject langObj = details.getAsJsonObject("languages");
if (langObj.has("value") && langObj.get("value").isJsonArray()) {
for (JsonElement el : langObj.getAsJsonArray("value")) {
monster.languages.add(new Language(capitalize(el.getAsString()), true));
}
}
}
if (details.has("publicNotes") && !details.get("publicNotes").isJsonNull()) {
String notes = details.get("publicNotes").getAsString().replaceAll("<[^>]*>", "").trim();
monster.backstory = notes;
}
}
return monster;
}
private String mapPf2eSize(String pf2eSize) {
if (pf2eSize == null) return "Medium";
switch (pf2eSize.toLowerCase(Locale.ROOT)) {
case "tiny": return "Tiny";
case "sm": return "Small";
case "med": return "Medium";
case "lg": return "Large";
case "huge": return "Huge";
case "grg": return "Gargantuan";
default: return "Medium";
}
}
private ChallengeRating mapLevelToCr(int level) {
if (level <= 0) return ChallengeRating.ZERO;
if (level > 30) return ChallengeRating.THIRTY;
return ChallengeRating.valueOfString(String.valueOf(level));
}
private static String getString(JsonObject obj, String key) {
if (obj != null && obj.has(key) && !obj.get(key).isJsonNull()) {
return obj.get(key).getAsString();
}
return "";
}
private static int getInt(JsonObject obj, String key) {
if (obj != null && obj.has(key) && !obj.get(key).isJsonNull()) {
try {
return obj.get(key).getAsInt();
} catch (Exception ignored) {}
}
return 0;
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toUpperCase(Locale.ROOT) + str.substring(1).toLowerCase(Locale.ROOT);
}
}

View File

@@ -1,364 +0,0 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.models.Trait;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public class TetraCubeMonsterImporter implements EntityImporter<Monster> {
@Override
public boolean canImport(@NonNull String input) {
if (input.trim().isEmpty()) {
return false;
}
try {
JsonElement element = JsonParser.parseString(input);
if (!element.isJsonObject()) {
return false;
}
JsonObject rootDict = element.getAsJsonObject();
if (rootDict.has("$schema")) {
String schemaVal = rootDict.get("$schema").getAsString();
if (schemaVal.contains("tetracube-monster.schema.json")) {
return true;
}
}
return rootDict.has("armorName") || rootDict.has("strPoints")
|| rootDict.has("isLegendary") || rootDict.has("isMythic")
|| rootDict.has("legendariesDescription");
} catch (Exception e) {
return false;
}
}
@NonNull
@Override
public Monster parse(@NonNull String input) throws Exception {
JsonObject rootDict = JsonParser.parseString(input).getAsJsonObject();
Monster monster = new Monster();
monster.name = getString(rootDict, "name");
monster.size = getString(rootDict, "size");
monster.type = getString(rootDict, "type");
monster.subtype = getString(rootDict, "tag");
monster.alignment = getString(rootDict, "alignment");
monster.hitDice = getInt(rootDict, "hitDice");
monster.armorType = ArmorTypeConverter.armorTypeFromStringValue(getString(rootDict, "armorName"));
monster.shieldBonus = getInt(rootDict, "shieldBonus");
monster.naturalArmorBonus = getInt(rootDict, "natArmorBonus");
monster.otherArmorDescription = getString(rootDict, "otherArmorDesc");
monster.walkSpeed = getInt(rootDict, "speed");
monster.burrowSpeed = getInt(rootDict, "burrowSpeed");
monster.climbSpeed = getInt(rootDict, "climbSpeed");
monster.flySpeed = getInt(rootDict, "flySpeed");
monster.canHover = getBool(rootDict, "hover");
monster.swimSpeed = getInt(rootDict, "swimSpeed");
monster.hasCustomHP = getBool(rootDict, "customHP");
monster.hasCustomSpeed = getBool(rootDict, "customSpeed");
monster.customHPDescription = getString(rootDict, "hpText");
monster.customSpeedDescription = getString(rootDict, "speedDesc");
monster.strengthScore = getInt(rootDict, "strPoints");
monster.dexterityScore = getInt(rootDict, "dexPoints");
monster.constitutionScore = getInt(rootDict, "conPoints");
monster.intelligenceScore = getInt(rootDict, "intPoints");
monster.wisdomScore = getInt(rootDict, "wisPoints");
monster.charismaScore = getInt(rootDict, "chaPoints");
addSense(monster, rootDict, "blindsight");
addSense(monster, rootDict, "darkvision");
addSense(monster, rootDict, "tremorsense");
addSense(monster, rootDict, "truesight");
monster.telepathyRange = getInt(rootDict, "telepathy");
monster.challengeRating = ChallengeRatingConverter.challengeRatingFromStringValue(getString(rootDict, "cr"));
monster.customChallengeRatingDescription = getString(rootDict, "customCr");
monster.customProficiencyBonus = getInt(rootDict, "customProf");
monster.abilities = getListOfTraits(rootDict, "abilities");
monster.actions = getListOfTraits(rootDict, "actions");
monster.bonusActions = getListOfTraits(rootDict, "bonusActions");
monster.reactions = getListOfTraits(rootDict, "reactions");
monster.legendaryActions = getListOfTraits(rootDict, "legendaries");
monster.mythicActions = getListOfTraits(rootDict, "mythics");
monster.lairActions = getListOfTraits(rootDict, "lairs");
monster.regionalActions = getListOfTraits(rootDict, "regionals");
monster.legendaryActionsDescription = getString(rootDict, "legendariesDescription");
if (!monster.legendaryActionsDescription.isEmpty()) {
monster.legendaryActions.add(0, new Trait("Legendary Actions", monster.legendaryActionsDescription));
}
monster.mythicActionsDescription = getString(rootDict, "mythicDescription");
if (!monster.mythicActionsDescription.isEmpty() || !monster.mythicActions.isEmpty()) {
if (!monster.mythicActionsDescription.isEmpty()) {
monster.legendaryActions.add(new Trait("Mythic Actions", monster.mythicActionsDescription));
}
for (Trait mythic : monster.mythicActions) {
String traitName = mythic.name;
if (!traitName.toLowerCase(Locale.ROOT).startsWith("mythic action")) {
traitName = "Mythic Action: " + traitName;
}
monster.legendaryActions.add(new Trait(traitName, mythic.description));
}
}
monster.lairActionsDescription = getString(rootDict, "lairDescription");
monster.lairActionsEndNote = getString(rootDict, "lairDescriptionEnd");
String fullLairDesc = combineDescriptions(monster.lairActionsDescription, monster.lairActionsEndNote);
if (!fullLairDesc.isEmpty()) {
monster.lairActions.add(0, new Trait("Lair Actions", fullLairDesc));
}
monster.regionalActionsDescription = getString(rootDict, "regionalDescription");
monster.regionalActionsEndNote = getString(rootDict, "regionalDescriptionEnd");
String fullRegionalDesc = combineDescriptions(monster.regionalActionsDescription, monster.regionalActionsEndNote);
if (!fullRegionalDesc.isEmpty()) {
monster.regionalActions.add(0, new Trait("Regional Effects", fullRegionalDesc));
}
addSavingThrows(monster, rootDict);
monster.skills = getSetOfSkills(rootDict);
monster.damageImmunities = getSetOfDamageTypes(rootDict, "damageTypes", "i");
monster.damageImmunities.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "i"));
monster.damageResistances = getSetOfDamageTypes(rootDict, "damageTypes", "r");
monster.damageResistances.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "r"));
monster.damageVulnerabilities = getSetOfDamageTypes(rootDict, "damageTypes", "v");
monster.damageVulnerabilities.addAll(getSetOfDamageTypes(rootDict, "specialdamage", "v"));
monster.conditionImmunities = getSetOfDamageTypes(rootDict, "conditions");
monster.languages = getSetOfLanguages(rootDict, "languages");
monster.understandsButDescription = getString(rootDict, "understandsBut");
return monster;
}
@NonNull
private static String combineDescriptions(String desc1, String desc2) {
if (!desc1.isEmpty() && !desc2.isEmpty()) {
return desc1 + "\n\n" + desc2;
} else if (!desc1.isEmpty()) {
return desc1;
} else {
return desc2;
}
}
private static String getString(JsonObject dict, String name) {
return getString(dict, name, "");
}
private static String getString(@NonNull JsonObject dict, String name, String defaultValue) {
if (dict.has(name)) {
return dict.get(name).getAsString();
}
return defaultValue;
}
private static int getInt(JsonObject dict, String name) {
return getInt(dict, name, 0);
}
private static int getInt(@NonNull JsonObject dict, String name, int defaultValue) {
if (dict.has(name)) {
JsonElement element = dict.get(name);
if (element.isJsonPrimitive()) {
JsonPrimitive rawValue = element.getAsJsonPrimitive();
if (rawValue.isNumber()) {
return rawValue.getAsInt();
} else {
try {
return rawValue.getAsInt();
} catch (Exception ex) {
return defaultValue;
}
}
}
}
return defaultValue;
}
private static boolean getBool(JsonObject dict, String name) {
return getBool(dict, name, false);
}
private static boolean getBool(@NonNull JsonObject dict, String name, boolean defaultValue) {
if (dict.has(name)) {
JsonElement element = dict.get(name);
if (element.isJsonPrimitive()) {
JsonPrimitive rawValue = element.getAsJsonPrimitive();
if (rawValue.isBoolean()) {
return rawValue.getAsBoolean();
} else {
try {
return rawValue.getAsBoolean();
} catch (Exception ex) {
return defaultValue;
}
}
}
}
return defaultValue;
}
@NonNull
private static String formatDistance(String name, int distance) {
return String.format(Locale.getDefault(), "%s %d ft.", name, distance);
}
private static void addSense(Monster monster, JsonObject root, String name) {
int distance = getInt(root, name);
if (distance > 0) {
String senseStr = formatDistance(name, distance);
if ("blindsight".equals(name) && getBool(root, "blind")) {
senseStr += " (blind beyond this radius)";
}
monster.senses.add(senseStr);
}
}
@NonNull
private static List<Trait> getListOfTraits(@NonNull 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 = getString(jsonObject, "name");
String description = getString(jsonObject, "desc");
Trait trait = new Trait(traitName, description);
traits.add(trait);
}
}
}
}
return traits;
}
private static void addSavingThrows(Monster monster, @NonNull JsonObject root) {
if (root.has("sthrows")) {
JsonElement arrayElement = root.get("sthrows");
if (arrayElement.isJsonArray()) {
JsonArray array = arrayElement.getAsJsonArray();
int size = array.size();
for (int index = 0; index < size; index++) {
JsonElement jsonElement = array.get(index);
if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
String name = getString(jsonObject, "name");
if ("str".equals(name)) {
monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("dex".equals(name)) {
monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("con".equals(name)) {
monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("int".equals(name)) {
monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("wis".equals(name)) {
monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("cha".equals(name)) {
monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
}
}
}
}
}
}
@NonNull
private static Set<Skill> getSetOfSkills(@NonNull 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 = getString(jsonObject, "name");
String stat = getString(jsonObject, "stat");
String note = getString(jsonObject, "note");
Skill skill = new Skill(name, AbilityScore.valueOfString(stat), AdvantageType.NONE, " (ex)".equals(note) ? ProficiencyType.EXPERTISE : ProficiencyType.PROFICIENT);
skills.add(skill);
}
}
}
}
return skills;
}
@NonNull
private static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name) {
return getSetOfDamageTypes(rootDict, name, null);
}
@NonNull
private static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name, String type) {
HashSet<String> damageTypes = new HashSet<>();
if (rootDict.has(name)) {
JsonElement arrayElement = rootDict.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 = getString(jsonObject, "name");
String dtType = getString(jsonObject, "type");
if (type == null || type.equals(dtType)) {
damageTypes.add(dtName);
}
}
}
}
}
return damageTypes;
}
@NonNull
private static Set<Language> getSetOfLanguages(@NonNull 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 = getString(jsonObject, "name");
boolean canSpeak = getBool(jsonObject, "speaks");
Language language = new Language(languageName, canSpeak);
languages.add(language);
}
}
}
}
return languages;
}
}

View File

@@ -1,49 +0,0 @@
package com.majinnaibu.monstercards.models;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class BinderExport {
@SerializedName("$schema")
public String schema = "https://majinnaibu.com/schemas/binder.schema.json";
@SerializedName("schemaVersion")
public int schemaVersion = 1;
@SerializedName("collections")
public List<CollectionExport> collections = new ArrayList<>();
@SerializedName("dashboard")
public List<Monster> dashboard = new ArrayList<>();
public static class CollectionExport {
@SerializedName("id")
public String id;
@SerializedName("name")
public String name;
@SerializedName("description")
public String description;
@SerializedName("cards")
public List<Monster> cards = new ArrayList<>();
public CollectionExport() {
}
public CollectionExport(String id, String name, String description, List<Monster> cards) {
this.id = id != null ? id : UUID.randomUUID().toString();
this.name = name != null ? name : "";
this.description = description != null ? description : "";
this.cards = cards != null ? cards : new ArrayList<>();
}
public CollectionExport(String name, List<Monster> cards) {
this(UUID.randomUUID().toString(), name, "", cards);
}
}
}

View File

@@ -1,55 +0,0 @@
package com.majinnaibu.monstercards.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import java.util.Objects;
import java.util.UUID;
@Entity(tableName = "collections")
public class Collection {
@PrimaryKey
@NonNull
public UUID id;
@NonNull
@ColumnInfo(defaultValue = "")
public String name;
@NonNull
@ColumnInfo(defaultValue = "")
public String description;
public Collection() {
this.id = UUID.randomUUID();
this.name = "";
this.description = "";
}
@Ignore
public Collection(@NonNull String name, @NonNull String description) {
this.id = UUID.randomUUID();
this.name = name;
this.description = description;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Collection that = (Collection) obj;
return Objects.equals(id, that.id) &&
Objects.equals(name, that.name) &&
Objects.equals(description, that.description);
}
@Override
public int hashCode() {
return Objects.hash(id, name, description);
}
}

View File

@@ -1,62 +0,0 @@
package com.majinnaibu.monstercards.models;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import java.util.UUID;
@Entity(
tableName = "collection_monsters",
foreignKeys = {
@ForeignKey(
entity = Collection.class,
parentColumns = "id",
childColumns = "collection_id",
onDelete = ForeignKey.CASCADE
),
@ForeignKey(
entity = Monster.class,
parentColumns = "id",
childColumns = "monster_id",
onDelete = ForeignKey.CASCADE
)
},
indices = {
@Index(value = {"collection_id"}),
@Index(value = {"monster_id"})
}
)
public class CollectionMonster {
@PrimaryKey(autoGenerate = true)
public long id;
@NonNull
@ColumnInfo(name = "collection_id")
public UUID collectionId;
@NonNull
@ColumnInfo(name = "monster_id")
public UUID monsterId;
@ColumnInfo(name = "ordinal", defaultValue = "0")
public int ordinal;
public CollectionMonster() {
this.collectionId = UUID.fromString("00000000-0000-0000-0000-000000000000");
this.monsterId = UUID.fromString("00000000-0000-0000-0000-000000000000");
this.ordinal = 0;
}
@Ignore
public CollectionMonster(@NonNull UUID collectionId, @NonNull UUID monsterId, int ordinal) {
this.collectionId = collectionId;
this.monsterId = monsterId;
this.ordinal = ordinal;
}
}

View File

@@ -1,22 +0,0 @@
package com.majinnaibu.monstercards.models;
import androidx.room.Embedded;
import androidx.room.Ignore;
public class CollectionWithCount {
@Embedded
public Collection collection;
public int monsterCount;
public CollectionWithCount() {
this.collection = new Collection();
this.monsterCount = 0;
}
@Ignore
public CollectionWithCount(Collection collection, int monsterCount) {
this.collection = collection;
this.monsterCount = monsterCount;
}
}

View File

@@ -1,45 +0,0 @@
package com.majinnaibu.monstercards.models;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import java.util.UUID;
@Entity(
tableName = "dashboard_monsters",
foreignKeys = @ForeignKey(
entity = Monster.class,
parentColumns = "id",
childColumns = "monster_id",
onDelete = ForeignKey.CASCADE
),
indices = @Index(value = {"monster_id"})
)
public class DashboardMonster {
@PrimaryKey(autoGenerate = true)
public long id;
@NonNull
@ColumnInfo(name = "monster_id")
public UUID monsterId;
@ColumnInfo(name = "ordinal", defaultValue = "0")
public int ordinal;
public DashboardMonster() {
this.monsterId = UUID.fromString("00000000-0000-0000-0000-000000000000");
this.ordinal = 0;
}
@Ignore
public DashboardMonster(@NonNull UUID monsterId, int ordinal) {
this.monsterId = monsterId;
this.ordinal = ordinal;
}
}

View File

@@ -1,25 +0,0 @@
package com.majinnaibu.monstercards.models;
import androidx.room.Embedded;
import androidx.room.Ignore;
import androidx.room.Relation;
public class DashboardMonsterWithMonster {
@Embedded
public DashboardMonster dashboardEntry;
@Relation(
parentColumn = "monster_id",
entityColumn = "id"
)
public Monster monster;
public DashboardMonsterWithMonster() {
}
@Ignore
public DashboardMonsterWithMonster(DashboardMonster dashboardEntry, Monster monster) {
this.dashboardEntry = dashboardEntry;
this.monster = monster;
}
}

View File

@@ -1,32 +0,0 @@
package com.majinnaibu.monstercards.models;
public class ImportSource {
public enum ImportType {
GIT_ARCHIVE,
OPEN5E_API
}
public String id;
public String projectName;
public String creatorName;
public String creatorPageLink;
public ImportType importType;
// Optional attributes specific to Git archive
public String downloadUrl;
public String importerClassName;
public String fileExtension;
public String subfolder;
public ImportSource(String id, String projectName, String creatorName, String creatorPageLink, ImportType importType, String downloadUrl, String importerClassName, String fileExtension, String subfolder) {
this.id = id;
this.projectName = projectName;
this.creatorName = creatorName;
this.creatorPageLink = creatorPageLink;
this.importType = importType;
this.downloadUrl = downloadUrl;
this.importerClassName = importerClassName;
this.fileExtension = fileExtension;
this.subfolder = subfolder;
}
}

View File

@@ -1,74 +1,37 @@
package com.majinnaibu.monstercards.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Comparator;
import java.util.Objects;
public class Language implements Comparator<Language>, Comparable<Language> {
private String mName;
private boolean mSpeaks;
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) {
if (this.mSpeaks && !o.mSpeaks) {
return -1;
}
if (!this.mSpeaks && o.mSpeaks) {
return 1;
}
return this.mName.compareToIgnoreCase(o.mName);
return this.getName().compareToIgnoreCase(o.getName());
}
@Override
public int compare(@NonNull Language o1, Language o2) {
if (o1.mSpeaks && !o2.mSpeaks) {
return -1;
}
if (!o1.mSpeaks && o2.mSpeaks) {
return 1;
}
return o1.mName.compareToIgnoreCase(o2.mName);
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof Language)) {
return false;
}
Language otherLanguage = (Language) obj;
if (!Objects.equals(this.mName, otherLanguage.mName)) {
return false;
}
if (this.mSpeaks != otherLanguage.mSpeaks) {
return false;
}
return true;
public int compare(Language o1, Language o2) {
return o1.getName().compareToIgnoreCase(o2.getName());
}
}

View File

@@ -6,10 +6,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.google.gson.annotations.SerializedName;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ArmorType;
@@ -18,11 +16,14 @@ import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.utils.Logger;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -31,16 +32,8 @@ import java.util.UUID;
@SuppressWarnings("unused")
public class Monster {
@SerializedName("$schema")
@Ignore
public String schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
@Ignore
public int schemaVersion = 1;
@PrimaryKey
@NonNull
@SerializedName("id")
public UUID id;
@NonNull
@@ -216,93 +209,6 @@ public class Monster {
@ColumnInfo(name = "regional_actions", defaultValue = "[]")
public List<Trait> regionalActions;
@ColumnInfo(name = "source_url", defaultValue = "")
public String sourceUrl;
@ColumnInfo(name = "bonus_actions", defaultValue = "[]")
public List<Trait> bonusActions;
@ColumnInfo(name = "mythic_actions", defaultValue = "[]")
public List<Trait> mythicActions;
@ColumnInfo(name = "legendary_actions_description", defaultValue = "")
public String legendaryActionsDescription;
@ColumnInfo(name = "lair_actions_description", defaultValue = "")
public String lairActionsDescription;
@ColumnInfo(name = "lair_actions_end_note", defaultValue = "")
public String lairActionsEndNote;
@ColumnInfo(name = "regional_actions_description", defaultValue = "")
public String regionalActionsDescription;
@ColumnInfo(name = "regional_actions_end_note", defaultValue = "")
public String regionalActionsEndNote;
@ColumnInfo(name = "mythic_actions_description", defaultValue = "")
public String mythicActionsDescription;
@NonNull
@ColumnInfo(name = "player_name", defaultValue = "")
public String playerName;
@NonNull
@ColumnInfo(name = "background", defaultValue = "")
public String background;
@NonNull
@ColumnInfo(name = "personality_traits", defaultValue = "")
public String personalityTraits;
@NonNull
@ColumnInfo(name = "ideals", defaultValue = "")
public String ideals;
@NonNull
@ColumnInfo(name = "bonds", defaultValue = "")
public String bonds;
@NonNull
@ColumnInfo(name = "flaws", defaultValue = "")
public String flaws;
@NonNull
@ColumnInfo(name = "age", defaultValue = "")
public String age;
@NonNull
@ColumnInfo(name = "height", defaultValue = "")
public String height;
@NonNull
@ColumnInfo(name = "weight", defaultValue = "")
public String weight;
@NonNull
@ColumnInfo(name = "eyes", defaultValue = "")
public String eyes;
@NonNull
@ColumnInfo(name = "skin", defaultValue = "")
public String skin;
@NonNull
@ColumnInfo(name = "hair", defaultValue = "")
public String hair;
@NonNull
@ColumnInfo(name = "appearance", defaultValue = "")
public String appearance;
@NonNull
@ColumnInfo(name = "backstory", defaultValue = "")
public String backstory;
@NonNull
@ColumnInfo(name = "allies_and_organizations", defaultValue = "")
public String alliesAndOrganizations;
public Monster() {
id = UUID.randomUUID();
name = "";
@@ -348,21 +254,6 @@ public class Monster {
wisdomSavingThrowProficiency = ProficiencyType.NONE;
charismaSavingThrowAdvantage = AdvantageType.NONE;
charismaSavingThrowProficiency = ProficiencyType.NONE;
playerName = "";
background = "";
personalityTraits = "";
ideals = "";
bonds = "";
flaws = "";
age = "";
height = "";
weight = "";
eyes = "";
skin = "";
hair = "";
appearance = "";
backstory = "";
alliesAndOrganizations = "";
skills = new HashSet<>();
@@ -378,15 +269,69 @@ public class Monster {
lairActions = new ArrayList<>();
legendaryActions = new ArrayList<>();
regionalActions = new ArrayList<>();
bonusActions = new ArrayList<>();
mythicActions = new ArrayList<>();
sourceUrl = "";
legendaryActionsDescription = "";
lairActionsDescription = "";
lairActionsEndNote = "";
regionalActionsDescription = "";
regionalActionsEndNote = "";
mythicActionsDescription = "";
}
public static boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Objects.equals(oldItem.id, newItem.id);
}
public static boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Objects.equals(oldItem.abilities, newItem.abilities) &&
Objects.equals(oldItem.actions, newItem.actions) &&
Objects.equals(oldItem.alignment, newItem.alignment) &&
Objects.equals(oldItem.armorType, newItem.armorType) &&
Objects.equals(oldItem.burrowSpeed, newItem.burrowSpeed) &&
Objects.equals(oldItem.canHover, newItem.canHover) &&
Objects.equals(oldItem.challengeRating, newItem.challengeRating) &&
Objects.equals(oldItem.charismaSavingThrowAdvantage, newItem.charismaSavingThrowAdvantage) &&
Objects.equals(oldItem.charismaSavingThrowProficiency, newItem.charismaSavingThrowProficiency) &&
Objects.equals(oldItem.charismaScore, newItem.charismaScore) &&
Objects.equals(oldItem.climbSpeed, newItem.climbSpeed) &&
Objects.equals(oldItem.conditionImmunities, newItem.conditionImmunities) &&
Objects.equals(oldItem.constitutionSavingThrowAdvantage, newItem.constitutionSavingThrowAdvantage) &&
Objects.equals(oldItem.constitutionSavingThrowProficiency, newItem.constitutionSavingThrowProficiency) &&
Objects.equals(oldItem.constitutionScore, newItem.constitutionScore) &&
Objects.equals(oldItem.customChallengeRatingDescription, newItem.customChallengeRatingDescription) &&
Objects.equals(oldItem.customHPDescription, newItem.customHPDescription) &&
Objects.equals(oldItem.customProficiencyBonus, newItem.customProficiencyBonus) &&
Objects.equals(oldItem.customSpeedDescription, newItem.customSpeedDescription) &&
Objects.equals(oldItem.damageImmunities, newItem.damageImmunities) &&
Objects.equals(oldItem.damageResistances, newItem.damageResistances) &&
Objects.equals(oldItem.damageVulnerabilities, newItem.damageVulnerabilities) &&
Objects.equals(oldItem.dexteritySavingThrowAdvantage, newItem.dexteritySavingThrowAdvantage) &&
Objects.equals(oldItem.dexteritySavingThrowProficiency, newItem.dexteritySavingThrowProficiency) &&
Objects.equals(oldItem.dexterityScore, newItem.dexterityScore) &&
Objects.equals(oldItem.flySpeed, newItem.flySpeed) &&
Objects.equals(oldItem.hasCustomHP, newItem.hasCustomHP) &&
Objects.equals(oldItem.hasCustomSpeed, newItem.hasCustomSpeed) &&
Objects.equals(oldItem.hitDice, newItem.hitDice) &&
Objects.equals(oldItem.intelligenceSavingThrowAdvantage, newItem.intelligenceSavingThrowAdvantage) &&
Objects.equals(oldItem.intelligenceSavingThrowProficiency, newItem.intelligenceSavingThrowProficiency) &&
Objects.equals(oldItem.intelligenceScore, newItem.intelligenceScore) &&
Objects.equals(oldItem.lairActions, newItem.lairActions) &&
Objects.equals(oldItem.languages, newItem.languages) &&
Objects.equals(oldItem.legendaryActions, newItem.legendaryActions) &&
Objects.equals(oldItem.name, newItem.name) &&
Objects.equals(oldItem.naturalArmorBonus, newItem.naturalArmorBonus) &&
Objects.equals(oldItem.otherArmorDescription, newItem.otherArmorDescription) &&
Objects.equals(oldItem.reactions, newItem.reactions) &&
Objects.equals(oldItem.regionalActions, newItem.regionalActions) &&
Objects.equals(oldItem.senses, newItem.senses) &&
Objects.equals(oldItem.shieldBonus, newItem.shieldBonus) &&
Objects.equals(oldItem.size, newItem.size) &&
Objects.equals(oldItem.skills, newItem.skills) &&
Objects.equals(oldItem.strengthSavingThrowAdvantage, newItem.strengthSavingThrowAdvantage) &&
Objects.equals(oldItem.strengthSavingThrowProficiency, newItem.strengthSavingThrowProficiency) &&
Objects.equals(oldItem.strengthScore, newItem.strengthScore) &&
Objects.equals(oldItem.subtype, newItem.subtype) &&
Objects.equals(oldItem.swimSpeed, newItem.swimSpeed) &&
Objects.equals(oldItem.telepathyRange, newItem.telepathyRange) &&
Objects.equals(oldItem.type, newItem.type) &&
Objects.equals(oldItem.understandsButDescription, newItem.understandsButDescription) &&
Objects.equals(oldItem.wisdomSavingThrowAdvantage, newItem.wisdomSavingThrowAdvantage) &&
Objects.equals(oldItem.wisdomSavingThrowProficiency, newItem.wisdomSavingThrowProficiency) &&
Objects.equals(oldItem.wisdomScore, newItem.wisdomScore) &&
Objects.equals(oldItem.walkSpeed, newItem.walkSpeed);
}
public String getMeta() {
@@ -425,7 +370,7 @@ public class Monster {
return sb.toString();
}
public int getAbilityScore(@NonNull AbilityScore abilityScore) {
public int getAbilityScore(@NotNull AbilityScore abilityScore) {
switch (abilityScore) {
case STRENGTH:
return strengthScore;
@@ -444,10 +389,7 @@ public class Monster {
}
}
public int getAbilityModifier(@Nullable AbilityScore abilityScore) {
if (abilityScore == null) {
return 0;
}
public int getAbilityModifier(@NotNull AbilityScore abilityScore) {
switch (abilityScore) {
case STRENGTH:
return getStrengthModifier();
@@ -466,7 +408,7 @@ public class Monster {
}
}
public AdvantageType getSavingThrowAdvantageType(@NonNull AbilityScore abilityScore) {
public AdvantageType getSavingThrowAdvantageType(@NotNull AbilityScore abilityScore) {
switch (abilityScore) {
case STRENGTH:
return strengthSavingThrowAdvantage;
@@ -485,7 +427,7 @@ public class Monster {
}
}
public ProficiencyType getSavingThrowProficiencyType(@NonNull AbilityScore abilityScore) {
public ProficiencyType getSavingThrowProficiencyType(@NotNull AbilityScore abilityScore) {
switch (abilityScore) {
case STRENGTH:
return strengthSavingThrowProficiency;
@@ -731,7 +673,7 @@ public class Monster {
}
}
public int getProficiencyBonus(@NonNull ProficiencyType proficiencyType) {
public int getProficiencyBonus(@NotNull ProficiencyType proficiencyType) {
switch (proficiencyType) {
case PROFICIENT:
return getProficiencyBonus();
@@ -875,7 +817,7 @@ public class Monster {
return abilityDescriptions;
}
public String getPlaceholderReplacedText(@NonNull String rawText) {
public String getPlaceholderReplacedText(@NotNull String rawText) {
return rawText
.replaceAll("\\[STR SAVE]", String.format("%+d", getSpellSaveDC(AbilityScore.STRENGTH)))
.replaceAll("\\[STR ATK]", String.format("%+d", getAttackBonus(AbilityScore.STRENGTH)))
@@ -907,22 +849,6 @@ public class Monster {
return actionDescriptions;
}
public List<String> getBonusActionDescriptions() {
ArrayList<String> actionDescriptions = new ArrayList<>();
for (Trait action : bonusActions) {
actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
}
return actionDescriptions;
}
public List<String> getMythicActionDescriptions() {
ArrayList<String> actionDescriptions = new ArrayList<>();
for (Trait action : mythicActions) {
actionDescriptions.add(getPlaceholderReplacedText(String.format("__%s__ %s", action.name, action.description)));
}
return actionDescriptions;
}
public List<String> getReactionDescriptions() {
ArrayList<String> actionDescriptions = new ArrayList<>();
for (Trait action : reactions) {
@@ -956,7 +882,7 @@ public class Monster {
}
@Override
public boolean equals(@Nullable Object obj) {
public boolean equals(@Nullable @org.jetbrains.annotations.Nullable Object obj) {
if (obj == null) {
return false;
}

View File

@@ -1,32 +0,0 @@
package com.majinnaibu.monstercards.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SearchResultItem {
public enum Type {
MONSTER,
COLLECTION
}
@NonNull
public final Type type;
@Nullable
public final Monster monster;
@Nullable
public final Collection collection;
public SearchResultItem(@NonNull Monster monster) {
this.type = Type.MONSTER;
this.monster = monster;
this.collection = null;
}
public SearchResultItem(@NonNull Collection collection) {
this.type = Type.COLLECTION;
this.monster = null;
this.collection = collection;
}
}

View File

@@ -2,14 +2,11 @@ package com.majinnaibu.monstercards.models;
import android.annotation.SuppressLint;
import androidx.annotation.Nullable;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import java.util.Comparator;
import java.util.Objects;
@SuppressLint("DefaultLocale")
public class Skill implements Comparator<Skill>, Comparable<Skill> {
@@ -34,25 +31,13 @@ public class Skill implements Comparator<Skill>, Comparable<Skill> {
this.proficiencyType = proficiencyType;
}
public AbilityScore getAbilityScore() {
if (abilityScore != null) {
return abilityScore;
}
if (name != null) {
return AbilityScore.valueOfString(name);
}
return AbilityScore.STRENGTH;
}
public int getSkillBonus(Monster monster) {
AbilityScore score = getAbilityScore();
int modifier = monster != null ? monster.getAbilityModifier(score) : 0;
ProficiencyType prof = proficiencyType != null ? proficiencyType : ProficiencyType.PROFICIENT;
switch (prof) {
int modifier = monster.getAbilityModifier(abilityScore);
switch (proficiencyType) {
case PROFICIENT:
return modifier + (monster != null ? monster.getProficiencyBonus() : 0);
return modifier + monster.getProficiencyBonus();
case EXPERTISE:
return modifier + (monster != null ? monster.getProficiencyBonus() * 2 : 0);
return modifier + monster.getProficiencyBonus() * 2;
case NONE:
default:
return modifier;
@@ -64,7 +49,7 @@ public class Skill implements Comparator<Skill>, Comparable<Skill> {
return String.format(
"%s%s %+d%s",
name.charAt(0),
name.substring(0, 1),
name.substring(1),
bonus,
advantageType == AdvantageType.ADVANTAGE ? " A" : advantageType == AdvantageType.DISADVANTAGE ? " D" : ""
@@ -81,27 +66,4 @@ public class Skill implements Comparator<Skill>, Comparable<Skill> {
return o1.name.compareToIgnoreCase(o2.name);
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof Skill)) {
return false;
}
Skill otherSkill = (Skill) obj;
if (!Objects.equals(this.name, otherSkill.name)) {
return false;
}
if (this.abilityScore != otherSkill.abilityScore) {
return false;
}
if (this.advantageType != otherSkill.advantageType) {
return false;
}
if (this.proficiencyType != otherSkill.proficiencyType) {
return false;
}
return true;
}
}

View File

@@ -1,11 +1,6 @@
package com.majinnaibu.monstercards.models;
import androidx.annotation.Nullable;
import java.util.Comparator;
import java.util.Objects;
public class Trait implements Comparator<Trait>, Comparable<Trait> {
public class Trait {
public String name;
public String description;
@@ -14,36 +9,4 @@ public class Trait implements Comparator<Trait>, Comparable<Trait> {
this.name = name;
this.description = description;
}
@Override
public int compareTo(Trait o) {
return compare(this, o);
}
@Override
public int compare(Trait o1, Trait o2) {
int result = o1.name.compareToIgnoreCase(o2.name);
if (result != 0) {
return result;
}
return o1.description.compareToIgnoreCase(o2.description);
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof Trait)) {
return false;
}
Trait otherTrait = (Trait) obj;
if (!Objects.equals(this.name, otherTrait.name)) {
return false;
}
if (!Objects.equals(this.description, otherTrait.description)) {
return false;
}
return true;
}
}

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

@@ -1,282 +0,0 @@
package com.majinnaibu.monstercards.ui.collections;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
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.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.dashboard.DashboardRecyclerViewAdapter;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class CollectionDetailFragment extends MCFragment {
private CollectionDetailViewModel mViewModel;
private UUID mCollectionId;
private DashboardRecyclerViewAdapter mAdapter;
private final CompositeDisposable mDisposables = new CompositeDisposable();
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
setHasOptionsMenu(true);
View root = inflater.inflate(R.layout.fragment_collection_detail, container, false);
Bundle arguments = getArguments();
assert arguments != null;
mCollectionId = UUID.fromString(CollectionDetailFragmentArgs.fromBundle(arguments).getCollectionId());
mViewModel = new ViewModelProvider(this).get(CollectionDetailViewModel.class);
TextView nameView = root.findViewById(R.id.detail_collection_name);
TextView descriptionView = root.findViewById(R.id.detail_collection_description);
RecyclerView recyclerView = root.findViewById(R.id.collection_monster_list);
FloatingActionButton fab = root.findViewById(R.id.fab_add_monster_to_collection);
if (fab != null) {
fab.setOnClickListener(v -> showCollectionDetailFabOptionsDialog());
}
mViewModel.getCollection().observe(getViewLifecycleOwner(), collection -> {
if (collection != null) {
nameView.setText(collection.name);
setTitle(collection.name);
if (!TextUtils.isEmpty(collection.description)) {
descriptionView.setVisibility(View.VISIBLE);
descriptionView.setText(collection.description);
} else {
descriptionView.setVisibility(View.GONE);
}
}
});
mViewModel.getMonsters().observe(getViewLifecycleOwner(), monsters -> {
if (mAdapter != null) {
List<DashboardMonsterWithMonster> items = new ArrayList<>();
if (monsters != null) {
for (int i = 0; i < monsters.size(); i++) {
DashboardMonster dm = new DashboardMonster(monsters.get(i).id, i);
dm.id = i + 1;
items.add(new DashboardMonsterWithMonster(dm, monsters.get(i)));
}
}
mAdapter.submitList(items);
}
});
setupRecyclerView(recyclerView);
loadCollectionData();
return root;
}
private void loadCollectionData() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getCollection(mCollectionId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(collection -> {
if (collection != null) {
mViewModel.setCollection(collection);
}
}, Logger::logError));
mDisposables.add(repository.getMonstersForCollection(mCollectionId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError));
}
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);
mAdapter = new DashboardRecyclerViewAdapter(item -> {
if (item != null && item.monster != null) {
navigateToMonsterDetail(item.monster.id);
} else {
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
}
});
recyclerView.setAdapter(mAdapter);
}
private void showCollectionDetailFabOptionsDialog() {
String[] options = new String[]{
getString(R.string.action_add_monster),
getString(R.string.action_create_monster),
getString(R.string.action_import)
};
new AlertDialog.Builder(requireContext())
.setTitle(R.string.title_collection_actions)
.setItems(options, (dialog, which) -> {
if (which == 0) {
showAddMonsterDialog();
} else if (which == 1) {
createNewMonster();
} else if (which == 2) {
showImportDialog();
}
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}
private void showAddMonsterDialog() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getMonsters()
.firstOrError()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> {
if (monsters.isEmpty()) {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster));
}
return;
}
String[] monsterNames = new String[monsters.size()];
for (int i = 0; i < monsters.size(); i++) {
monsterNames[i] = monsters.get(i).name;
}
new AlertDialog.Builder(requireContext())
.setTitle(R.string.action_add_monster)
.setItems(monsterNames, (dialog, which) -> {
Monster selectedMonster = monsters.get(which);
addMonsterToCollection(selectedMonster);
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}, Logger::logError));
}
private void addMonsterToCollection(Monster monster) {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.addMonsterToCollection(mCollectionId, monster.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
Collection collection = mViewModel.getCollection().getValue();
String collectionName = collection != null ? collection.name : "";
SnackbarHelper.showLong(view, getString(R.string.snackbar_monster_added_to_collection, monster.name, collectionName));
}
}, Logger::logError));
}
private void navigateToMonsterDetail(@NonNull UUID monsterId) {
NavDirections action = CollectionDetailFragmentDirections.actionCollectionDetailFragmentToNavigationMonster(monsterId.toString());
Navigation.findNavController(requireView()).navigate(action);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.collection_detail_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) {
addCollectionToDashboard();
return true;
} else if (item.getItemId() == R.id.menu_action_export_collection) {
exportCollection();
return true;
} else if (item.getItemId() == R.id.menu_action_remove_all_monsters_from_collection) {
showRemoveAllMonstersConfirmationDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
private void showRemoveAllMonstersConfirmationDialog() {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_remove_all_from_collection_title)
.setMessage(R.string.dialog_remove_all_from_collection_message)
.setPositiveButton(R.string.action_remove_all, (dialog, which) -> {
mDisposables.add(getMonsterRepository().removeAllMonstersFromCollection(mCollectionId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, R.string.snackbar_collection_cleared);
}
}, throwable -> Logger.logError("Failed to remove all monsters from collection", throwable)));
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}
private void exportCollection() {
Collection collection = mViewModel.getCollection().getValue();
List<Monster> monsters = mViewModel.getMonsters().getValue();
if (collection != null) {
String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder(
collection.name,
monsters != null ? monsters : new java.util.ArrayList<>()
);
String collectionName = (collection.name != null && !collection.name.trim().isEmpty())
? collection.name.trim()
: "collection";
exportToFile(collectionName + ".binder", json);
}
}
private void addCollectionToDashboard() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.addCollectionToDashboard(mCollectionId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
Collection collection = mViewModel.getCollection().getValue();
String collectionName = collection != null ? collection.name : "";
SnackbarHelper.showLong(view, getString(R.string.snackbar_collection_added_to_dashboard, collectionName));
}
}, Logger::logError));
}
@Override
public void onDestroyView() {
super.onDestroyView();
mDisposables.clear();
}
}

View File

@@ -1,36 +0,0 @@
package com.majinnaibu.monstercards.ui.collections;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.Monster;
import java.util.ArrayList;
import java.util.List;
public class CollectionDetailViewModel extends ViewModel {
private final MutableLiveData<Collection> mCollection = new MutableLiveData<>();
private final MutableLiveData<List<Monster>> mMonsters = new MutableLiveData<>(new ArrayList<>());
public CollectionDetailViewModel() {
}
public LiveData<Collection> getCollection() {
return mCollection;
}
public void setCollection(Collection collection) {
mCollection.setValue(collection);
}
public LiveData<List<Monster>> getMonsters() {
return mMonsters;
}
public void setMonsters(List<Monster> monsters) {
mMonsters.setValue(monsters);
}
}

View File

@@ -1,247 +1,34 @@
package com.majinnaibu.monstercards.ui.collections;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
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.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
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 androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Collection;
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.SnackbarHelper;
import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class CollectionsFragment extends MCFragment {
private final CompositeDisposable mDisposables = new CompositeDisposable();
@Nullable
@Override
private CollectionsViewModel collectionsViewModel;
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
setHasOptionsMenu(true);
ViewGroup container, Bundle savedInstanceState) {
collectionsViewModel = new ViewModelProvider(this).get(CollectionsViewModel.class);
View root = inflater.inflate(R.layout.fragment_collections, container, false);
FloatingActionButton fab = root.findViewById(R.id.fab_add_collection);
if (fab != null) {
fab.setOnClickListener(v -> showCollectionFabOptionsDialog());
}
View emptyState = root.findViewById(R.id.empty_state);
View emptyStateButton = root.findViewById(R.id.empty_state_button);
if (emptyStateButton != null) {
emptyStateButton.setOnClickListener(v -> showCollectionFabOptionsDialog());
}
RecyclerView recyclerView = root.findViewById(R.id.collection_list);
if (recyclerView != null) {
setupRecyclerView(recyclerView, emptyState);
}
return root;
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView, @Nullable View emptyState) {
Context context = requireContext();
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getCollectionsWithCount()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(collections -> {
boolean isEmpty = (collections == null || collections.isEmpty());
if (emptyState != null) {
emptyState.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
}
recyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
}, Logger::logError));
CollectionsRecyclerViewAdapter adapter = new CollectionsRecyclerViewAdapter(
context,
repository.getCollectionsWithCount(),
collection -> navigateToCollectionDetail(collection.id),
collection -> repository.deleteCollection(collection)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
final TextView textView = root.findViewById(R.id.text_collections);
collectionsViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
@Override
public void onComplete() {
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError(e);
}
})
);
recyclerView.setAdapter(adapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> adapter.deleteItem(position), null));
itemTouchHelper.attachToRecyclerView(recyclerView);
}
private void showCollectionFabOptionsDialog() {
String[] options = new String[]{
getString(R.string.title_new_collection),
getString(R.string.action_import_collection)
};
new AlertDialog.Builder(requireContext())
.setTitle(R.string.title_collection_actions)
.setItems(options, (dialog, which) -> {
if (which == 0) {
showCreateCollectionDialog();
} else if (which == 1) {
importCollectionFromFile();
}
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}
private void showCreateCollectionDialog() {
Context context = requireContext();
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
int padding = (int) (16 * getResources().getDisplayMetrics().density);
layout.setPadding(padding, padding, padding, padding);
final EditText nameInput = new EditText(context);
nameInput.setHint(R.string.label_collection_name);
layout.addView(nameInput);
final EditText descInput = new EditText(context);
descInput.setHint(R.string.label_collection_description);
layout.addView(descInput);
new AlertDialog.Builder(context)
.setTitle(R.string.title_new_collection)
.setView(layout)
.setPositiveButton(R.string.dialog_create, (dialog, which) -> {
String name = nameInput.getText().toString().trim();
String description = descInput.getText().toString().trim();
if (TextUtils.isEmpty(name)) {
name = getString(R.string.title_new_collection);
}
createCollection(name, description);
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}
private void createCollection(@NonNull String name, @NonNull String description) {
Collection collection = new Collection(name, description);
MonsterRepository repository = getMonsterRepository();
repository.saveCollection(collection)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
View view = getView();
if (view != null) {
SnackbarHelper.makeLong(view, getString(R.string.snackbar_collection_created, collection.name))
.setAction("View", v -> navigateToCollectionDetail(collection.id))
.show();
}
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error creating collection", e);
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_collection));
}
public void onChanged(@Nullable String s) {
textView.setText(s);
}
});
}
private void navigateToCollectionDetail(@NonNull UUID collectionId) {
NavDirections action = CollectionsFragmentDirections.actionNavigationCollectionsToCollectionDetailFragment(collectionId.toString());
Navigation.findNavController(requireView()).navigate(action);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.collections_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.menu_action_export_collections) {
exportCollections();
return true;
} else if (item.getItemId() == R.id.menu_action_remove_all_collections) {
showRemoveAllCollectionsConfirmationDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
private void exportCollections() {
mDisposables.add(getMonsterRepository().exportCollections()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(json -> exportToFile("collections.binder", json),
throwable -> Logger.logError("Failed to export collections", throwable)));
}
private void showRemoveAllCollectionsConfirmationDialog() {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_remove_all_collections_title)
.setMessage(R.string.dialog_remove_all_collections_message)
.setPositiveButton(R.string.action_remove_all, (dialog, which) -> {
mDisposables.add(getMonsterRepository().removeAllCollections()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, R.string.snackbar_all_collections_removed);
}
}, throwable -> Logger.logError("Failed to remove all collections", throwable)));
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mDisposables.clear();
return root;
}
}

View File

@@ -1,137 +0,0 @@
package com.majinnaibu.monstercards.ui.collections;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionWithCount;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class CollectionsRecyclerViewAdapter extends RecyclerView.Adapter<CollectionsRecyclerViewAdapter.ViewHolder> {
private final Context mContext;
private final CollectionCallback mOnClick;
private final CollectionCallback mOnDelete;
private final Flowable<List<CollectionWithCount>> mItemsObservable;
private List<CollectionWithCount> mValues;
private Disposable mDisposable;
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(@NonNull View view) {
Collection collection = (Collection) view.getTag();
if (mOnClick != null) {
mOnClick.onCallback(collection);
}
}
};
public CollectionsRecyclerViewAdapter(Context context,
Flowable<List<CollectionWithCount>> itemsObservable,
CollectionCallback onClick,
CollectionCallback onDelete) {
mContext = context;
mItemsObservable = itemsObservable;
mOnClick = onClick;
mOnDelete = onDelete;
mValues = new ArrayList<>();
mDisposable = null;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.collection_list_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CollectionWithCount item = mValues.get(position);
Collection collection = item.collection;
holder.mNameView.setText(collection.name);
String countText;
if (item.monsterCount == 1) {
countText = mContext.getString(R.string.format_monster_count_one);
} else {
countText = mContext.getString(R.string.format_monster_count_other, item.monsterCount);
}
holder.mCountView.setText(countText);
holder.mCountView.setVisibility(View.VISIBLE);
if (!TextUtils.isEmpty(collection.description)) {
holder.mDescriptionView.setVisibility(View.VISIBLE);
holder.mDescriptionView.setText(collection.description);
} else {
holder.mDescriptionView.setVisibility(View.GONE);
}
holder.itemView.setTag(collection);
holder.itemView.setOnClickListener(mOnClickListener);
}
@Override
public int getItemCount() {
return mValues.size();
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mDisposable = mItemsObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(collections -> {
mValues = collections;
notifyDataSetChanged();
});
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
if (mDisposable != null) {
mDisposable.dispose();
}
}
public void deleteItem(int position) {
if (mOnDelete != null && position >= 0 && position < mValues.size()) {
Collection collection = mValues.get(position).collection;
mOnDelete.onCallback(collection);
}
}
public interface CollectionCallback {
void onCallback(Collection collection);
}
static class ViewHolder extends RecyclerView.ViewHolder {
final TextView mNameView;
final TextView mDescriptionView;
final TextView mCountView;
ViewHolder(View view) {
super(view);
mNameView = view.findViewById(R.id.collection_name);
mDescriptionView = view.findViewById(R.id.collection_description);
mCountView = view.findViewById(R.id.collection_count);
}
}
}

View File

@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel;
public class CollectionsViewModel extends ViewModel {
private final MutableLiveData<String> mText;
private MutableLiveData<String> mText;
public CollectionsViewModel() {
mText = new MutableLiveData<>();

View File

@@ -35,12 +35,11 @@ public class AbilityScorePicker extends LinearLayout {
// TODO: use this as default but allow setting via attribute
mLabel = "Ability Score";
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AbilityScorePicker, 0, 0);
String label = a.getString(R.styleable.AbilityScorePicker_label);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Stepper, 0, 0);
String label = a.getString(R.styleable.Stepper_label);
if (label != null) {
mLabel = label;
}
a.recycle();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View root = inflater.inflate(R.layout.component_ability_score_picker, this, true);
@@ -70,17 +69,18 @@ public class AbilityScorePicker extends LinearLayout {
mHolder.spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
setValue((AbilityScore) parent.getItemAtPosition(position));
mSelectedValue = (AbilityScore) parent.getItemAtPosition(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
setValue(mSelectedValue = AbilityScore.STRENGTH);
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) {
@@ -93,8 +93,7 @@ public class AbilityScorePicker extends LinearLayout {
public void setValue(AbilityScore value) {
if (value != mSelectedValue) {
mSelectedValue = value;
mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), value));
mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), mSelectedValue));
if (mOnValueChangedListener != null) {
mOnValueChangedListener.onValueChanged(value);
}
@@ -125,7 +124,7 @@ public class AbilityScorePicker extends LinearLayout {
private final Spinner spinner;
private final TextView label;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
spinner = root.findViewById(R.id.spinner);
label = root.findViewById(R.id.label);
}

View File

@@ -33,9 +33,9 @@ public class AdvantagePicker extends ConstraintLayout {
setValue(AdvantageType.NONE);
mHolder.group.setOnCheckedChangeListener((group, checkedId) -> {
if (R.id.hasAdvantage == checkedId) {
if (R.id.advantage == checkedId) {
setValue(AdvantageType.ADVANTAGE);
} else if (R.id.hasDisadvantage == checkedId) {
} else if (R.id.disadvantage == checkedId) {
setValue(AdvantageType.DISADVANTAGE);
} else {
setValue(AdvantageType.NONE);
@@ -60,11 +60,11 @@ public class AdvantagePicker extends ConstraintLayout {
}
final int checkedId = mHolder.group.getCheckedRadioButtonId();
if (mSelectedValue == AdvantageType.ADVANTAGE) {
if (checkedId != R.id.hasAdvantage) {
if (checkedId != R.id.advantage) {
mHolder.advantage.setChecked(true);
}
} else if (mSelectedValue == AdvantageType.DISADVANTAGE) {
if (checkedId != R.id.hasDisadvantage) {
if (checkedId != R.id.disadvantage) {
mHolder.disadvantage.setChecked(true);
}
} else {
@@ -88,11 +88,11 @@ public class AdvantagePicker extends ConstraintLayout {
final MaterialRadioButton advantage;
final MaterialRadioButton disadvantage;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
group = root.findViewById(R.id.group);
none = root.findViewById(R.id.hasNoAdvantage);
advantage = root.findViewById(R.id.hasAdvantage);
disadvantage = root.findViewById(R.id.hasDisadvantage);
none = root.findViewById(R.id.none);
advantage = root.findViewById(R.id.advantage);
disadvantage = root.findViewById(R.id.disadvantage);
}
}
}

View File

@@ -88,7 +88,7 @@ public class ProficiencyPicker extends ConstraintLayout {
final MaterialRadioButton proficient;
final MaterialRadioButton expertise;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
group = root.findViewById(R.id.group);
none = root.findViewById(R.id.none);
proficient = root.findViewById(R.id.proficient);

View File

@@ -13,11 +13,11 @@ import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.Objects;
@SuppressWarnings("unused")
public class Stepper extends ConstraintLayout {
private final ViewHolder mHolder;
private int mCurrentValue;
@@ -46,7 +46,6 @@ public class Stepper extends ConstraintLayout {
mHolder = new ViewHolder(root);
setValue(mCurrentValue);
updateDisplayedValue();
mHolder.increment.setOnClickListener(v -> setValue(mCurrentValue + mStep));
mHolder.decrement.setOnClickListener(v -> setValue(mCurrentValue - mStep));
@@ -75,22 +74,19 @@ public class Stepper extends ConstraintLayout {
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);
}
updateDisplayedValue();
}
}
private void updateDisplayedValue() {
if (mOnFormatValueCallback != null) {
mHolder.text.setText(mOnFormatValueCallback.onFormatValue(this.mCurrentValue));
} else {
mHolder.text.setText(String.valueOf(this.mCurrentValue));
}
}
}
public void setOnValueChangeListener(OnValueChangeListener listener) {
mOnValueChangeListener = listener;
@@ -98,7 +94,6 @@ public class Stepper extends ConstraintLayout {
public void setOnFormatValueCallback(OnFormatValueCallback callback) {
mOnFormatValueCallback = callback;
updateDisplayedValue();
}
public int getStep() {
@@ -139,7 +134,7 @@ public class Stepper extends ConstraintLayout {
final Button increment;
final Button decrement;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
text = root.findViewById(R.id.text);
label = root.findViewById(R.id.label);
increment = root.findViewById(R.id.increment);

View File

@@ -3,349 +3,71 @@ package com.majinnaibu.monstercards.ui.dashboard;
import android.content.Context;
import android.os.Bundle;
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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
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.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionWithCount;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class DashboardFragment extends MCFragment {
private static final String MODIFIER_FORMAT = "%+d";
private DashboardViewModel mViewModel;
private ViewHolder mHolder;
private DashboardRecyclerViewAdapter mAdapter;
private final CompositeDisposable mDisposables = new CompositeDisposable();
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
setHasOptionsMenu(true);
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);
FloatingActionButton fab = root.findViewById(R.id.fab_add_to_dashboard);
if (fab != null) {
fab.setOnClickListener(v -> showAddToDashboardOptionsDialog());
}
if (mHolder.emptyStateButton != null) {
mHolder.emptyStateButton.setOnClickListener(v -> showAddToDashboardOptionsDialog());
}
setupRecyclerView(mHolder.list);
loadDashboardMonsters();
getMonsterRepository()
.getMonsters()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> {
mViewModel.setMonsters(monsters);
});
return root;
}
private void loadDashboardMonsters() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getDashboardMonstersWithMonster()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError));
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 360);
Logger.logWTF(String.format(Locale.US, "Setting column count to %d", columnCount));
int columnCount = Math.max(1, (int) Math.floor(getResources().getConfiguration().screenWidthDp / 396));
Logger.logWTF(String.format("Setting column count to %d", columnCount));
Context context = requireContext();
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
recyclerView.setLayoutManager(layoutManager);
LiveData<List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster>> monsterData = mViewModel.getMonsters();
mAdapter = new DashboardRecyclerViewAdapter(
item -> {
if (item != null && item.monster != null) {
navigateToMonsterDetail(item.monster);
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");
}
},
item -> {
if (item != null) {
showCardOptionsMenu(item);
}
}
);
if (monsterData != null) {
monsterData.observe(getViewLifecycleOwner(), monsters -> {
mAdapter.submitList(monsters);
boolean isEmpty = (monsters == null || monsters.isEmpty());
if (mHolder.emptyState != null) {
mHolder.emptyState.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
}
if (mHolder.list != null) {
mHolder.list.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
}
});
if (monsterData != null) {
monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
}
recyclerView.setAdapter(mAdapter);
ItemTouchHelper.SimpleCallback touchHelperCallback = new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
) {
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int fromPos = viewHolder.getAdapterPosition();
int toPos = target.getAdapterPosition();
if (fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION) {
List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster> currentList = new ArrayList<>(mAdapter.getCurrentList());
if (fromPos < currentList.size() && toPos < currentList.size()) {
com.majinnaibu.monstercards.models.DashboardMonsterWithMonster moved = currentList.remove(fromPos);
currentList.add(toPos, moved);
mAdapter.submitList(currentList);
}
}
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster> currentList = mAdapter.getCurrentList();
if (position < currentList.size()) {
com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item = currentList.get(position);
removeDashboardMonsterEntry(item);
}
}
}
};
new ItemTouchHelper(touchHelperCallback).attachToRecyclerView(recyclerView);
}
private void showAddToDashboardOptionsDialog() {
String[] options = new String[]{
getString(R.string.action_add_single_monster),
getString(R.string.action_add_collection_option),
getString(R.string.action_create_monster),
getString(R.string.action_import)
};
new AlertDialog.Builder(requireContext())
.setTitle(R.string.title_dashboard_actions)
.setItems(options, (dialog, which) -> {
if (which == 0) {
showAddMonsterPicker();
} else if (which == 1) {
showAddCollectionPicker();
} else if (which == 2) {
createNewMonster();
} else if (which == 3) {
showImportDialog();
}
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}
private void showRemoveMonsterPicker() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getDashboardMonsters()
.firstOrError()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> {
if (monsters.isEmpty()) {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, getString(R.string.snackbar_dashboard_already_empty));
}
return;
}
String[] names = new String[monsters.size()];
for (int i = 0; i < monsters.size(); i++) {
names[i] = monsters.get(i).name;
}
new AlertDialog.Builder(requireContext())
.setTitle(R.string.action_remove_single_monster)
.setItems(names, (dialog, which) -> {
Monster selected = monsters.get(which);
removeMonsterFromDashboard(selected);
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}, Logger::logError));
}
private void showCardOptionsMenu(@NonNull com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item) {
String[] options = new String[]{
getString(R.string.action_view_details),
getString(R.string.action_remove_from_dashboard)
};
new AlertDialog.Builder(requireContext())
.setTitle(item.monster.name)
.setItems(options, (dialog, which) -> {
if (which == 0) {
navigateToMonsterDetail(item.monster);
} else if (which == 1) {
removeDashboardMonsterEntry(item);
}
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}
private void removeDashboardMonsterEntry(@NonNull com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item) {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.removeDashboardMonsterById(item.dashboardEntry.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
SnackbarHelper.makeLong(view, getString(R.string.snackbar_removed_from_dashboard, item.monster.name))
.setAction(R.string.action_undo, v -> addMonsterToDashboard(item.monster))
.show();
}
}, Logger::logError));
}
private void showAddMonsterPicker() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getMonsters()
.firstOrError()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> {
if (monsters.isEmpty()) {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, getString(R.string.no_monsters_available));
}
return;
}
String[] names = new String[monsters.size()];
for (int i = 0; i < monsters.size(); i++) {
names[i] = monsters.get(i).name;
}
new AlertDialog.Builder(requireContext())
.setTitle(R.string.action_add_single_monster)
.setItems(names, (dialog, which) -> {
Monster selected = monsters.get(which);
addMonsterToDashboard(selected);
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}, Logger::logError));
}
private void showAddCollectionPicker() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getCollectionsWithCount()
.firstOrError()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(collections -> {
if (collections.isEmpty()) {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, getString(R.string.no_collections_available));
}
return;
}
String[] names = new String[collections.size()];
for (int i = 0; i < collections.size(); i++) {
CollectionWithCount item = collections.get(i);
int count = item.monsterCount;
String countText = count == 1 ? "1 monster" : count + " monsters";
String colName = item.collection != null && item.collection.name != null ? item.collection.name : "";
names[i] = colName + " (" + countText + ")";
}
new AlertDialog.Builder(requireContext())
.setTitle(R.string.action_add_collection_option)
.setItems(names, (dialog, which) -> {
Collection selected = collections.get(which).collection;
addCollectionToDashboard(selected);
})
.setNegativeButton(R.string.dialog_cancel, null)
.show();
}, Logger::logError));
}
private void addMonsterToDashboard(@NonNull Monster monster) {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.addMonsterToDashboard(monster.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, getString(R.string.snackbar_added_to_dashboard, monster.name));
}
}, Logger::logError));
}
private void addCollectionToDashboard(@NonNull Collection collection) {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.addCollectionToDashboard(collection.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, getString(R.string.snackbar_collection_added_to_dashboard, collection.name));
}
}, Logger::logError));
}
private void removeMonsterFromDashboard(@NonNull Monster monster) {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.removeMonsterFromDashboard(monster.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
SnackbarHelper.makeLong(view, getString(R.string.snackbar_removed_from_dashboard, monster.name))
.setAction(R.string.action_undo, v -> addMonsterToDashboard(monster))
.show();
}
}, Logger::logError));
}
private void clearDashboard() {
MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.clearDashboard()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
View view = getView();
if (view != null) {
SnackbarHelper.showLong(view, getString(R.string.snackbar_dashboard_cleared));
}
}, Logger::logError));
}
private void navigateToMonsterDetail(Monster monster) {
@@ -353,57 +75,12 @@ public class DashboardFragment extends MCFragment {
Navigation.findNavController(requireView()).navigate(action);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.dashboard_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.menu_action_add_single_monster) {
showAddMonsterPicker();
return true;
} else if (item.getItemId() == R.id.menu_action_add_collection_option) {
showAddCollectionPicker();
return true;
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
clearDashboard();
return true;
} else if (item.getItemId() == R.id.menu_action_export_dashboard) {
exportDashboard();
return true;
}
return super.onOptionsItemSelected(item);
}
private void exportDashboard() {
mDisposables.add(getMonsterRepository().getDashboardMonsters()
.firstOrError()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> {
String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder("", monsters);
String fileName = getString(R.string.default_filename_dashboard) + ".binder";
exportToFile(fileName, json);
}, Logger::logError));
}
@Override
public void onDestroyView() {
super.onDestroyView();
mDisposables.clear();
}
private static class ViewHolder {
final RecyclerView list;
final View emptyState;
final View emptyStateButton;
ViewHolder(View root) {
list = root.findViewById(R.id.list);
emptyState = root.findViewById(R.id.empty_state);
emptyStateButton = root.findViewById(R.id.empty_state_button);
}
}
}

View File

@@ -18,44 +18,29 @@ 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.DashboardMonsterWithMonster;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Trait;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.Locale;
public class DashboardRecyclerViewAdapter extends ListAdapter<DashboardMonsterWithMonster, DashboardRecyclerViewAdapter.ViewHolder> {
private static final DiffUtil.ItemCallback<DashboardMonsterWithMonster> DIFF_CALLBACK = new DiffUtil.ItemCallback<DashboardMonsterWithMonster>() {
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 DashboardMonsterWithMonster oldItem, @NonNull DashboardMonsterWithMonster newItem) {
if (oldItem.dashboardEntry != null && newItem.dashboardEntry != null) {
return oldItem.dashboardEntry.id == newItem.dashboardEntry.id;
}
return oldItem.monster.id.equals(newItem.monster.id);
public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Monster.areItemsTheSame(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(@NonNull DashboardMonsterWithMonster oldItem, @NonNull DashboardMonsterWithMonster newItem) {
if (oldItem.dashboardEntry != null && newItem.dashboardEntry != null) {
return oldItem.dashboardEntry.id == newItem.dashboardEntry.id
&& oldItem.dashboardEntry.ordinal == newItem.dashboardEntry.ordinal
&& (oldItem.monster != null && oldItem.monster.equals(newItem.monster));
}
return (oldItem.monster != null && oldItem.monster.equals(newItem.monster));
public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return Monster.areContentsTheSame(oldItem, newItem);
}
};
private final ItemCallback mOnClick;
private final ItemCallback mOnLongClick;
public DashboardRecyclerViewAdapter(ItemCallback onClick, ItemCallback onLongClick) {
protected DashboardRecyclerViewAdapter(ItemCallback onClick) {
super(DIFF_CALLBACK);
mOnClick = onClick;
mOnLongClick = onLongClick;
}
public DashboardRecyclerViewAdapter(ItemCallback onClick) {
this(onClick, null);
}
@NonNull
@@ -66,10 +51,7 @@ public class DashboardRecyclerViewAdapter extends ListAdapter<DashboardMonsterWi
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Logger.logUnimplementedMethod();
DashboardMonsterWithMonster item = getItem(position);
Monster monster = item.monster;
holder.item = item;
Monster monster = getItem(position);
holder.monster = monster;
holder.name.setText(monster.name);
holder.meta.setText(monster.getMeta());
@@ -137,25 +119,16 @@ public class DashboardRecyclerViewAdapter extends ListAdapter<DashboardMonsterWi
holder.itemView.setOnClickListener(v -> {
if (mOnClick != null) {
mOnClick.onItemCallback(holder.item);
mOnClick.onItemCallback(holder.monster);
}
});
holder.itemView.setOnLongClickListener(v -> {
if (mOnLongClick != null) {
mOnLongClick.onItemCallback(holder.item);
return true;
}
return false;
});
}
public interface ItemCallback {
void onItemCallback(DashboardMonsterWithMonster item);
void onItemCallback(Monster monster);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public DashboardMonsterWithMonster item;
public final TextView name;
public final TextView meta;
public final View action1Group;
@@ -268,7 +241,6 @@ public class DashboardRecyclerViewAdapter extends ListAdapter<DashboardMonsterWi
@NonNull
public static String getChallengeRatingAbbreviation(@NonNull ChallengeRating challengeRating) {
Logger.logUnimplementedMethod();
switch (challengeRating) {
case CUSTOM:
return "*";

View File

@@ -4,23 +4,23 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
import com.majinnaibu.monstercards.models.Monster;
import java.util.ArrayList;
import java.util.List;
public class DashboardViewModel extends ViewModel {
private final MutableLiveData<List<DashboardMonsterWithMonster>> mMonsters;
private final MutableLiveData<List<Monster>> mMonsters;
public DashboardViewModel() {
mMonsters = new MutableLiveData<>(new ArrayList<>());
}
public LiveData<List<DashboardMonsterWithMonster>> getMonsters() {
public LiveData<List<Monster>> getMonsters() {
return mMonsters;
}
public void setMonsters(List<DashboardMonsterWithMonster> monsters) {
public void setMonsters(List<Monster> monsters) {
mMonsters.setValue(monsters);
}
}

View File

@@ -5,7 +5,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
@@ -15,9 +15,7 @@ import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.ui.components.Stepper;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import java.util.Locale;
public class EditAbilityScoresFragment extends MCFragment {
public class EditAbilityScoresFragment extends Fragment {
private final String ABILITY_SCORE_FORMAT = "%d (%+d)";
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
@@ -27,37 +25,38 @@ public class EditAbilityScoresFragment extends MCFragment {
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
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(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
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(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
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(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
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(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
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(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
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(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
mHolder.charisma.setOnFormatValueCallback(value -> String.format(ABILITY_SCORE_FORMAT, value, getModifier(value)));
return root;
}
@@ -70,7 +69,7 @@ public class EditAbilityScoresFragment extends MCFragment {
final Stepper wisdom;
final Stepper charisma;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
strength = root.findViewById(R.id.strength);
dexterity = root.findViewById(R.id.dexterity);
constitution = root.findViewById(R.id.constitution);

View File

@@ -30,13 +30,14 @@ public class EditArmorFragment extends MCFragment {
private ViewHolder mHolder;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
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
@@ -93,7 +94,7 @@ public class EditArmorFragment extends MCFragment {
private final Stepper shieldBonus;
private final EditText customArmor;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
armorType = root.findViewById(R.id.armorType);
naturalArmorBonus = root.findViewById(R.id.naturalArmorBonus);
hasShield = root.findViewById(R.id.hasShield);

View File

@@ -6,7 +6,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
@@ -29,12 +28,13 @@ public class EditBasicInfoFragment extends MCFragment {
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
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());
@@ -74,7 +74,7 @@ public class EditBasicInfoFragment extends MCFragment {
private final Stepper hitDice;
private final SwitchMaterial hasCustomHitPoints;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
name = root.findViewById(R.id.name);
size = root.findViewById(R.id.size);
type = root.findViewById(R.id.type);

View File

@@ -28,13 +28,14 @@ public class EditChallengeRatingFragment extends MCFragment {
private ViewHolder mHolder;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
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
@@ -82,7 +83,7 @@ public class EditChallengeRatingFragment extends MCFragment {
final EditText customChallengeRatingDescription;
final EditText customProficiencyBonus;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
challengeRating = root.findViewById(R.id.challengeRating);
customChallengeRatingDescription = root.findViewById(R.id.customChallengeRatingDescription);
customProficiencyBonus = root.findViewById(R.id.customProficiencyBonus);

View File

@@ -44,12 +44,13 @@ public class EditLanguageFragment extends MCFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
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())));
@@ -80,7 +81,7 @@ public class EditLanguageFragment extends MCFragment {
EditText name;
SwitchCompat canSpeak;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
name = root.findViewById(R.id.name);
canSpeak = root.findViewById(R.id.canSpeak);
}

View File

@@ -1,6 +1,5 @@
package com.majinnaibu.monstercards.ui.editmonster;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import com.majinnaibu.monstercards.models.Language;
@@ -19,7 +18,7 @@ public class EditLanguageViewModel extends ChangeTrackedViewModel {
mLanguage = new ChangeTrackedLiveData<>(makeLanguage(), this::makeDirty);
}
public void copyFromLanguage(@NonNull Language language) {
public void copyFromLanguage(Language language) {
mName.resetValue(language.getName());
mCanSpeak.resetValue(language.getSpeaks());
makeClean();
@@ -59,7 +58,6 @@ public class EditLanguageViewModel extends ChangeTrackedViewModel {
return getCanSpeakValue(false);
}
@NonNull
private Language makeLanguage() {
Boolean boxedValue = mCanSpeak.getValue();
boolean canSpeak = boxedValue != null && boxedValue;

View File

@@ -30,19 +30,20 @@ public class EditLanguagesFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
private void navigateToEditLanguage(@NonNull Language language) {
private void navigateToEditLanguage(Language language) {
NavDirections action = EditLanguagesFragmentDirections.actionEditLanguagesFragmentToEditLanguageFragment(language.getName(), language.getSpeaks());
Navigation.findNavController(requireView()).navigate(action);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
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);
@@ -93,7 +94,7 @@ public class EditLanguagesFragment extends MCFragment {
RecyclerView list;
FloatingActionButton addLanguage;
ViewHolder(@NonNull View root) {
ViewHolder(View root) {
this.list = root.findViewById(R.id.list);
this.addLanguage = root.findViewById(R.id.add_language);
}

View File

@@ -6,7 +6,6 @@ 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;
@@ -14,8 +13,9 @@ import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListItemBind
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.ui.components.Stepper;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Locale;
public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<Language> mValues;
@@ -29,18 +29,18 @@ public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter<Recyc
private final int ITEM_VIEW_TYPE = 2;
private final String DISTANCE_IN_FEET_FORMAT = "%d ft.";
public EditLanguagesRecyclerViewAdapter(List<Language> items, ItemCallback onClick, int telepathyRange, Stepper.OnValueChangeListener telepathyRangeChangedListener, String understandsBut, TextWatcher understandsButChangedListener) {
public EditLanguagesRecyclerViewAdapter(List<Language> items, ItemCallback onClick, int telepathyRange, Stepper.OnValueChangeListener telepathyRangeChangedListener, String undderstandsBut, TextWatcher understandsButChangedListener) {
mValues = items;
mOnClick = onClick;
mTelepathyRange = telepathyRange;
mOnTelepathyRangeChanged = telepathyRangeChangedListener;
mUnderstandsBut = understandsBut;
mUnderstandsBut = undderstandsBut;
mOnUnderstandsButChanged = understandsButChangedListener;
}
@NonNull
@NotNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
public RecyclerView.ViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) {
if (viewType == HEADER_VIEW_TYPE) {
return new HeaderViewHolder(FragmentEditLanguagesListHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@@ -48,10 +48,10 @@ public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter<Recyc
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
public void onBindViewHolder(@NotNull 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.setOnFormatValueCallback(value -> String.format(DISTANCE_IN_FEET_FORMAT, value));
headerViewHolder.telepathy.setValue(mTelepathyRange);
headerViewHolder.telepathy.setOnValueChangeListener(mOnTelepathyRangeChanged);
headerViewHolder.understandsBut.setText(mUnderstandsBut);
@@ -89,7 +89,7 @@ public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter<Recyc
public final Stepper telepathy;
public final EditText understandsBut;
public HeaderViewHolder(@NonNull FragmentEditLanguagesListHeaderBinding binding) {
public HeaderViewHolder(FragmentEditLanguagesListHeaderBinding binding) {
super(binding.getRoot());
telepathy = binding.telepathy;
understandsBut = binding.understandsBut;
@@ -100,12 +100,12 @@ public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter<Recyc
public final TextView mContentView;
public Language mItem;
public ItemViewHolder(@NonNull FragmentEditLanguagesListItemBinding binding) {
public ItemViewHolder(FragmentEditLanguagesListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@NotNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";

View File

@@ -1,11 +1,9 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
@@ -16,19 +14,18 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
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 com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@@ -46,34 +43,21 @@ public class EditMonsterFragment extends MCFragment {
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);
MonsterRepository repository = getMonsterRepository();
Bundle arguments = getArguments();
if (arguments == null) {
Logger.logError("Arguments bundle is null in EditMonsterFragment");
return root;
}
String monsterIdStr = EditMonsterFragmentArgs.fromBundle(arguments).getMonsterId();
if (monsterIdStr == null || monsterIdStr.isEmpty()) {
Logger.logError("monsterId is null or empty in EditMonsterFragment");
return root;
}
UUID monsterId = UUID.fromString(monsterIdStr);
try {
NavController navController = NavHostFragment.findNavController(this);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
} catch (Exception e) {
mViewModel = new ViewModelProvider(this).get(EditMonsterViewModel.class);
}
setTitle(getString(R.string.title_editMonster_fmt, getString(R.string.default_monster_name)));
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()
@@ -83,12 +67,13 @@ public class EditMonsterFragment extends MCFragment {
mViewModel.setHasLoaded(true);
mViewModel.setHasError(false);
mViewModel.copyFromMonster(monster);
setTitle(getString(R.string.title_editMonster_fmt, monster.name));
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());
@@ -97,9 +82,101 @@ public class EditMonsterFragment extends MCFragment {
});
}
setupAccordions();
setupSummaries();
setupSubClickListeners();
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
@@ -110,6 +187,7 @@ public class EditMonsterFragment extends MCFragment {
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())
@@ -117,22 +195,29 @@ public class EditMonsterFragment extends MCFragment {
new DisposableCompletableObserver() {
@Override
public void onComplete() {
NavHostFragment.findNavController(EditMonsterFragment.this).navigateUp();
Navigation.findNavController(requireView()).navigateUp();
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error saving monster", e);
Logger.logError("Error creating monster", e);
assert view != null;
SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster));
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) -> NavHostFragment.findNavController(EditMonsterFragment.this).navigateUp());
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "Cancel", (dialog, id) -> {});
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 {
NavHostFragment.findNavController(EditMonsterFragment.this).navigateUp();
// No changes so we can safely leave.
Navigation.findNavController(requireView()).navigateUp();
}
}
});
@@ -140,344 +225,42 @@ public class EditMonsterFragment extends MCFragment {
return root;
}
private void setupAccordions() {
if (mHolder.headerBasicInfo != null && mHolder.bodyBasicInfo != null && mHolder.chevronBasicInfo != null) {
setupAccordion(mHolder.headerBasicInfo, mHolder.bodyBasicInfo, mHolder.chevronBasicInfo);
}
if (mHolder.headerCombatAbilities != null && mHolder.bodyCombatAbilities != null && mHolder.chevronCombatAbilities != null) {
setupAccordion(mHolder.headerCombatAbilities, mHolder.bodyCombatAbilities, mHolder.chevronCombatAbilities);
}
if (mHolder.headerSavesSkillsLanguages != null && mHolder.bodySavesSkillsLanguages != null && mHolder.chevronSavesSkillsLanguages != null) {
setupAccordion(mHolder.headerSavesSkillsLanguages, mHolder.bodySavesSkillsLanguages, mHolder.chevronSavesSkillsLanguages);
}
if (mHolder.headerDefenses != null && mHolder.bodyDefenses != null && mHolder.chevronDefenses != null) {
setupAccordion(mHolder.headerDefenses, mHolder.bodyDefenses, mHolder.chevronDefenses);
}
if (mHolder.headerTraitsActions != null && mHolder.bodyTraitsActions != null && mHolder.chevronTraitsActions != null) {
setupAccordion(mHolder.headerTraitsActions, mHolder.bodyTraitsActions, mHolder.chevronTraitsActions);
}
}
private void setupAccordion(View header, View body, ImageView chevron) {
header.setOnClickListener(v -> {
boolean isExpanded = body.getVisibility() == View.VISIBLE;
body.setVisibility(isExpanded ? View.GONE : View.VISIBLE);
chevron.setImageResource(isExpanded ? R.drawable.ic_expand_more_24 : R.drawable.ic_expand_less_24);
});
}
private void setupSummaries() {
mViewModel.getName().observe(getViewLifecycleOwner(), v -> updateBasicInfoSummary());
mViewModel.getSize().observe(getViewLifecycleOwner(), v -> updateBasicInfoSummary());
mViewModel.getType().observe(getViewLifecycleOwner(), v -> updateBasicInfoSummary());
mViewModel.getChallengeRating().observe(getViewLifecycleOwner(), v -> updateBasicInfoSummary());
mViewModel.getStrength().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
mViewModel.getDexterity().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
mViewModel.getConstitution().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
mViewModel.getIntelligence().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
mViewModel.getWisdom().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
mViewModel.getCharisma().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
mViewModel.getSkills().observe(getViewLifecycleOwner(), v -> updateSavesSkillsSummary());
mViewModel.getLanguages().observe(getViewLifecycleOwner(), v -> updateSavesSkillsSummary());
mViewModel.getConditionImmunities().observe(getViewLifecycleOwner(), v -> updateDefensesSummary());
mViewModel.getDamageImmunities().observe(getViewLifecycleOwner(), v -> updateDefensesSummary());
mViewModel.getDamageResistances().observe(getViewLifecycleOwner(), v -> updateDefensesSummary());
mViewModel.getDamageVulnerabilities().observe(getViewLifecycleOwner(), v -> updateDefensesSummary());
mViewModel.getAbilities().observe(getViewLifecycleOwner(), v -> updateTraitsActionsSummary());
mViewModel.getActions().observe(getViewLifecycleOwner(), v -> updateTraitsActionsSummary());
mViewModel.getReactions().observe(getViewLifecycleOwner(), v -> updateTraitsActionsSummary());
}
private void updateBasicInfoSummary() {
if (mHolder.summaryBasicInfo == null) return;
String name = mViewModel.getName().getValue();
String size = mViewModel.getSize().getValue();
String type = mViewModel.getType().getValue();
List<String> parts = new ArrayList<>();
if (!TextUtils.isEmpty(name)) parts.add(name);
if (!TextUtils.isEmpty(size)) parts.add(size);
if (!TextUtils.isEmpty(type)) parts.add(type);
mHolder.summaryBasicInfo.setText(parts.isEmpty() ? "Tap to edit basic info" : TextUtils.join(", ", parts));
}
private void updateCombatAbilitiesSummary() {
if (mHolder.summaryCombatAbilities == null) return;
Integer str = mViewModel.getStrength().getValue();
Integer dex = mViewModel.getDexterity().getValue();
Integer con = mViewModel.getConstitution().getValue();
Integer intel = mViewModel.getIntelligence().getValue();
Integer wis = mViewModel.getWisdom().getValue();
Integer cha = mViewModel.getCharisma().getValue();
String summary = String.format("STR %d | DEX %d | CON %d | INT %d | WIS %d | CHA %d",
str != null ? str : 10,
dex != null ? dex : 10,
con != null ? con : 10,
intel != null ? intel : 10,
wis != null ? wis : 10,
cha != null ? cha : 10);
mHolder.summaryCombatAbilities.setText(summary);
}
private void updateSavesSkillsSummary() {
if (mHolder.summarySavesSkillsLanguages == null) return;
int skillsCount = mViewModel.getSkills().getValue() != null ? mViewModel.getSkills().getValue().size() : 0;
int languagesCount = mViewModel.getLanguages().getValue() != null ? mViewModel.getLanguages().getValue().size() : 0;
mHolder.summarySavesSkillsLanguages.setText(String.format("%d Skills, %d Languages", skillsCount, languagesCount));
}
private void updateDefensesSummary() {
if (mHolder.summaryDefenses == null) return;
List<String> defenses = new ArrayList<>();
if (mViewModel.getConditionImmunities().getValue() != null && !mViewModel.getConditionImmunities().getValue().isEmpty()) {
defenses.add(mViewModel.getConditionImmunities().getValue().size() + " Condition Immunities");
}
if (mViewModel.getDamageImmunities().getValue() != null && !mViewModel.getDamageImmunities().getValue().isEmpty()) {
defenses.add(mViewModel.getDamageImmunities().getValue().size() + " Damage Immunities");
}
if (mViewModel.getDamageResistances().getValue() != null && !mViewModel.getDamageResistances().getValue().isEmpty()) {
defenses.add(mViewModel.getDamageResistances().getValue().size() + " Resistances");
}
mHolder.summaryDefenses.setText(defenses.isEmpty() ? "None" : TextUtils.join(", ", defenses));
}
private void updateTraitsActionsSummary() {
if (mHolder.summaryTraitsActions == null) return;
int abilitiesCount = mViewModel.getAbilities().getValue() != null ? mViewModel.getAbilities().getValue().size() : 0;
int actionsCount = mViewModel.getActions().getValue() != null ? mViewModel.getActions().getValue().size() : 0;
int reactionsCount = mViewModel.getReactions().getValue() != null ? mViewModel.getReactions().getValue().size() : 0;
mHolder.summaryTraitsActions.setText(String.format("%d Abilities, %d Actions, %d Reactions", abilitiesCount, actionsCount, reactionsCount));
}
private void setupSubClickListeners() {
if (mHolder.basicInfoButton != null) {
mHolder.basicInfoButton.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment();
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.challengeRating != null) {
mHolder.challengeRating.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditChallengeRatingFragment();
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.armorButton != null) {
mHolder.armorButton.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditArmorFragment();
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.speedButton != null) {
mHolder.speedButton.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSpeedFragment();
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.abilityScoresButton != null) {
mHolder.abilityScoresButton.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditAbilityScoresFragment();
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.savingThrows != null) {
mHolder.savingThrows.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSavingThrowsFragment();
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.skills != null) {
mHolder.skills.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSkillsFragment();
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.senses != null) {
mHolder.senses.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.SENSE);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.languages != null) {
mHolder.languages.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditLanguagesFragment();
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.conditionImmunities != null) {
mHolder.conditionImmunities.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.CONDITION_IMMUNITY);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.damageImmunities != null) {
mHolder.damageImmunities.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_IMMUNITY);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.damageResistances != null) {
mHolder.damageResistances.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_RESISTANCE);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.damageVulnerabilities != null) {
mHolder.damageVulnerabilities.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_VULNERABILITY);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.abilities != null) {
mHolder.abilities.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ABILITY);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.actions != null) {
mHolder.actions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ACTION);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.reactions != null) {
mHolder.reactions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REACTIONS);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.legendaryActions != null) {
mHolder.legendaryActions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LEGENDARY_ACTION);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.lairActions != null) {
mHolder.lairActions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LAIR_ACTION);
NavHostFragment.findNavController(this).navigate(action);
});
}
if (mHolder.regionalActions != null) {
mHolder.regionalActions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REGIONAL_ACTION);
NavHostFragment.findNavController(this).navigate(action);
});
}
}
private static class ViewHolder {
final View headerBasicInfo;
final View bodyBasicInfo;
final ImageView chevronBasicInfo;
final TextView summaryBasicInfo;
final View headerCombatAbilities;
final View bodyCombatAbilities;
final ImageView chevronCombatAbilities;
final TextView summaryCombatAbilities;
final View headerSavesSkillsLanguages;
final View bodySavesSkillsLanguages;
final ImageView chevronSavesSkillsLanguages;
final TextView summarySavesSkillsLanguages;
final View headerDefenses;
final View bodyDefenses;
final ImageView chevronDefenses;
final TextView summaryDefenses;
final View headerTraitsActions;
final View bodyTraitsActions;
final ImageView chevronTraitsActions;
final TextView summaryTraitsActions;
final TextView basicInfoButton;
final TextView challengeRating;
final TextView armorButton;
final TextView speedButton;
final TextView abilityScoresButton;
final TextView savingThrows;
final TextView skills;
final TextView senses;
final TextView languages;
final TextView conditionImmunities;
final TextView damageImmunities;
final TextView damageResistances;
final TextView damageVulnerabilities;
final TextView abilities;
final TextView actions;
final TextView reactions;
final TextView legendaryActions;
final TextView lairActions;
final TextView regionalActions;
ViewHolder(@NonNull View root) {
headerBasicInfo = root.findViewById(R.id.headerBasicInfo);
bodyBasicInfo = root.findViewById(R.id.bodyBasicInfo);
chevronBasicInfo = root.findViewById(R.id.chevronBasicInfo);
summaryBasicInfo = root.findViewById(R.id.summaryBasicInfo);
headerCombatAbilities = root.findViewById(R.id.headerCombatAbilities);
bodyCombatAbilities = root.findViewById(R.id.bodyCombatAbilities);
chevronCombatAbilities = root.findViewById(R.id.chevronCombatAbilities);
summaryCombatAbilities = root.findViewById(R.id.summaryCombatAbilities);
headerSavesSkillsLanguages = root.findViewById(R.id.headerSavesSkillsLanguages);
bodySavesSkillsLanguages = root.findViewById(R.id.bodySavesSkillsLanguages);
chevronSavesSkillsLanguages = root.findViewById(R.id.chevronSavesSkillsLanguages);
summarySavesSkillsLanguages = root.findViewById(R.id.summarySavesSkillsLanguages);
headerDefenses = root.findViewById(R.id.headerDefenses);
bodyDefenses = root.findViewById(R.id.bodyDefenses);
chevronDefenses = root.findViewById(R.id.chevronDefenses);
summaryDefenses = root.findViewById(R.id.summaryDefenses);
headerTraitsActions = root.findViewById(R.id.headerTraitsActions);
bodyTraitsActions = root.findViewById(R.id.bodyTraitsActions);
chevronTraitsActions = root.findViewById(R.id.chevronTraitsActions);
summaryTraitsActions = root.findViewById(R.id.summaryTraitsActions);
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);
challengeRating = root.findViewById(R.id.challengeRating);
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);
senses = root.findViewById(R.id.senses);
languages = root.findViewById(R.id.languages);
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);

View File

@@ -28,7 +28,7 @@ import java.util.List;
import java.util.Objects;
import java.util.UUID;
@SuppressWarnings({"ConstantConditions"})
@SuppressWarnings({"ConstantConditions", "unused"})
public class EditMonsterViewModel extends ChangeTrackedViewModel {
private final ChangeTrackedLiveData<UUID> mMonsterId;
private final MutableLiveData<Boolean> mHasError;
@@ -158,7 +158,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mRegionalActions = new ChangeTrackedLiveData<>(new ArrayList<>(), this::makeDirty);
}
public void copyFromMonster(@NonNull Monster monster) {
public void copyFromMonster(Monster monster) {
mMonsterId.resetValue(monster.id);
mName.resetValue(monster.name);
mSize.resetValue(monster.size);
@@ -206,7 +206,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mUnderstandsButDescription.resetValue(monster.understandsButDescription);
ArrayList<Skill> skills = new ArrayList<>(monster.skills);
Collections.sort(skills, Skill::compareTo);
Collections.sort(skills, (skill1, skill2) -> skill1.name.compareToIgnoreCase(skill2.name));
mSkills.resetValue(skills);
ArrayList<String> senses = new ArrayList<>(monster.senses);
Collections.sort(senses, String::compareToIgnoreCase);
@@ -224,7 +224,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
Collections.sort(conditionImmunities, String::compareToIgnoreCase);
mConditionImmunities.resetValue(conditionImmunities);
ArrayList<Language> languages = new ArrayList<>(monster.languages);
Collections.sort(languages, Language::compareTo);
Collections.sort(languages, (lang1, lang2) -> lang1.getName().compareToIgnoreCase(lang2.getName()));
mLanguages.resetValue(languages);
mAbilities.resetValue(new ArrayList<>(monster.abilities));
mActions.resetValue(new ArrayList<>(monster.actions));
@@ -247,6 +247,10 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
return mMonsterId;
}
public LiveData<String> getErrorMessage() {
return mErrorMessage;
}
public void setErrorMessage(@NonNull String errorMessage) {
mErrorMessage.setValue(errorMessage);
}
@@ -315,22 +319,48 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mCustomHitPoints.setValue(customHitPoints);
}
public LiveData<Integer> getHitDice() {
return mHitDice;
}
public void setHitDice(int hitDice) {
mHitDice.setValue(hitDice);
}
public void setHitDice(String hitDice) {
Integer parsedHitDice = StringHelper.parseInt(hitDice);
this.setHitDice(parsedHitDice != null ? parsedHitDice : 0);
}
public int getHitDiceUnboxed() {
return Helpers.unboxInteger(mHitDice.getValue(), 1);
}
public String getHitDiceValueAsString() {
return mHitDice.getValue().toString();
}
public LiveData<Integer> getNaturalArmorBonus() {
return mNaturalArmorBonus;
}
public void setNaturalArmorBonus(int naturalArmorBonus) {
mNaturalArmorBonus.setValue(naturalArmorBonus);
}
public void setNaturalArmorBonus(String naturalArmorBonus) {
Integer parsedValue = StringHelper.parseInt(naturalArmorBonus);
this.setNaturalArmorBonus(parsedValue != null ? parsedValue : 0);
}
public int getNaturalArmorBonusUnboxed() {
return Helpers.unboxInteger(mNaturalArmorBonus.getValue(), 0);
}
public String getNaturalArmorBonusValueAsString() {
return mNaturalArmorBonus.getValue().toString();
}
public LiveData<Boolean> getHasCustomHitPoints() {
return mHasCustomHitPoints;
}
@@ -351,6 +381,10 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mArmorType.setValue(armorType);
}
public LiveData<Boolean> getHasShield() {
return mHasShield;
}
public void setHasShield(boolean hasShield) {
mHasShield.setValue(hasShield);
}
@@ -359,10 +393,19 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
return mHasShield.getValue();
}
public LiveData<Integer> getShieldBonus() {
return mShieldBonus;
}
public void setShieldBonus(int shieldBonus) {
mShieldBonus.setValue(shieldBonus);
}
public void setShieldBonus(String shieldBonus) {
Integer parsedValue = StringHelper.parseInt(shieldBonus);
this.setShieldBonus(parsedValue != null ? parsedValue : 0);
}
public int getShieldBonusUnboxed() {
return Helpers.unboxInteger(mShieldBonus.getValue(), 0);
}
@@ -387,6 +430,14 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mWalkSpeed.setValue(walkSpeed);
}
public void incrementWalkSpeed() {
setWalkSpeed(mWalkSpeed.getValue() + 5);
}
public void decrementWalkSpeed() {
setWalkSpeed(mWalkSpeed.getValue() - 5);
}
public LiveData<Integer> getBurrowSpeed() {
return mBurrowSpeed;
}
@@ -443,6 +494,38 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mCustomSpeed.setValue(customSpeed);
}
public void incrementBurrowSpeed() {
setBurrowSpeed(mBurrowSpeed.getValue() + 5);
}
public void decrementBurrowSpeed() {
setBurrowSpeed(mBurrowSpeed.getValue() - 5);
}
public void incrementClimbSpeed() {
setClimbSpeed(mClimbSpeed.getValue() + 5);
}
public void decrementClimbSpeed() {
setClimbSpeed(mClimbSpeed.getValue() - 5);
}
public void incrementFlySpeed() {
setFlySpeed(mFlySpeed.getValue() + 5);
}
public void decrementFlySpeed() {
setFlySpeed(mFlySpeed.getValue() - 5);
}
public void incrementSwimSpeed() {
setSwimSpeed(mSwimSpeed.getValue() + 5);
}
public void decrementSwimSpeed() {
setSwimSpeed(mSwimSpeed.getValue() - 5);
}
public LiveData<Integer> getStrength() {
return mStrength;
}
@@ -451,6 +534,14 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mStrength.setValue(strength);
}
public void incrementStrength() {
setStrength(mStrength.getValue() + 1);
}
public void decrementStrength() {
setStrength(mStrength.getValue() - 1);
}
public LiveData<Integer> getDexterity() {
return mDexterity;
}
@@ -459,6 +550,14 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mDexterity.setValue(dexterity);
}
public void incrementDexterity() {
setDexterity(mDexterity.getValue() + 1);
}
public void decrementDexterity() {
setDexterity(mDexterity.getValue() - 1);
}
public LiveData<Integer> getConstitution() {
return mConstitution;
}
@@ -467,6 +566,14 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mConstitution.setValue(constitution);
}
public void incrementConstitution() {
setConstitution(mConstitution.getValue() + 1);
}
public void decrementConstitution() {
setConstitution(mConstitution.getValue() - 1);
}
public LiveData<Integer> getIntelligence() {
return mIntelligence;
}
@@ -475,6 +582,14 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mIntelligence.setValue(intelligence);
}
public void incrementIntelligence() {
setIntelligence(mIntelligence.getValue() + 1);
}
public void decrementIntelligence() {
setIntelligence(mIntelligence.getValue() - 1);
}
public LiveData<Integer> getWisdom() {
return mWisdom;
}
@@ -483,6 +598,14 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mWisdom.setValue(wisdom);
}
public void incrementWisdom() {
setWisdom(mWisdom.getValue() + 1);
}
public void decrementWisdom() {
setWisdom(mWisdom.getValue() - 1);
}
public LiveData<Integer> getCharisma() {
return mCharisma;
}
@@ -491,6 +614,14 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
mCharisma.setValue(charisma);
}
public void incrementCharisma() {
setCharisma(mCharisma.getValue() + 1);
}
public void decrementCharisma() {
setCharisma(mCharisma.getValue() - 1);
}
public LiveData<ProficiencyType> getStrengthProficiency() {
return mStrengthProficiency;
}
@@ -690,6 +821,22 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
return mSenses;
}
public List<String> getSensesArray() {
return mSenses.getValue();
}
public String addNewSense() {
return Helpers.addItemToList(mSenses, "", String::compareToIgnoreCase);
}
public void removeSense(int position) {
Helpers.removeFromList(mSenses, position);
}
public void replaceSense(String oldSense, String newSense) {
Helpers.replaceItemInList(mSenses, oldSense, newSense, String::compareToIgnoreCase);
}
public LiveData<List<String>> getDamageImmunities() {
return mDamageImmunities;
}
@@ -698,40 +845,76 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
return mDamageImmunities.getValue();
}
public String addNewDamageImmunity() {
return Helpers.addStringToList("", mDamageImmunities);
}
public void removeDamageImmunity(int position) {
Helpers.removeFromList(mDamageImmunities, position);
}
public void replaceDamageImmunity(String oldDamageType, String newDamageType) {
Helpers.replaceItemInList(mDamageImmunities, oldDamageType, newDamageType, String::compareToIgnoreCase);
}
public LiveData<List<String>> getDamageResistances() {
return mDamageResistances;
}
public List<String> getDamageResistancesArray() {
return mDamageResistances.getValue();
}
public String addNewDamageResistance() {
return Helpers.addStringToList("", mDamageResistances);
}
public void removeDamageResistance(int position) {
Helpers.removeFromList(mDamageResistances, position);
}
public void replaceDamageResistance(String oldDamageType, String newDamageType) {
Helpers.replaceItemInList(mDamageResistances, oldDamageType, newDamageType, String::compareToIgnoreCase);
}
public LiveData<List<String>> getDamageVulnerabilities() {
return mDamageVulnerabilities;
}
public List<String> getDamageVulnerabilitiesArray() {
return mDamageVulnerabilities.getValue();
}
public String addNewDamageVulnerability() {
return Helpers.addStringToList("", mDamageVulnerabilities);
}
public void removeDamageVulnerability(int position) {
Helpers.removeFromList(mDamageVulnerabilities, position);
}
public void replaceDamageVulnerability(String oldDamageType, String newDamageType) {
Helpers.replaceItemInList(mDamageVulnerabilities, oldDamageType, newDamageType, String::compareToIgnoreCase);
}
public LiveData<List<String>> getConditionImmunities() {
return mConditionImmunities;
}
public LiveData<List<Trait>> getAbilities() {
return mAbilities;
public List<String> getConditionImmunitiesArray() {
return mConditionImmunities.getValue();
}
public LiveData<List<Trait>> getActions() {
return mActions;
public String addNewConditionImmunity() {
return Helpers.addStringToList("", mConditionImmunities);
}
public LiveData<List<Trait>> getReactions() {
return mReactions;
public void removeConditionImmunity(int position) {
Helpers.removeFromList(mConditionImmunities, position);
}
public LiveData<List<Trait>> getLairActions() {
return mLairActions;
}
public LiveData<List<Trait>> getLegendaryActions() {
return mLegendaryActions;
}
public LiveData<List<Trait>> getRegionalActions() {
return mRegionalActions;
public void replaceConditionImmunity(String oldDamageType, String newDamageType) {
Helpers.replaceItemInList(mConditionImmunities, oldDamageType, newDamageType, String::compareToIgnoreCase);
}
public LiveData<List<Language>> getLanguages() {
@@ -819,7 +1002,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
return monster;
}
public LiveData<List<Trait>> getTraits(@NonNull TraitType type) {
public LiveData<List<Trait>> getTraits(TraitType type) {
switch (type) {
case ABILITY:
return mAbilities;
@@ -839,7 +1022,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
}
}
public void removeTrait(@NonNull TraitType type, int position) {
public void removeTrait(TraitType type, int position) {
switch (type) {
case ABILITY:
Helpers.removeFromList(mAbilities, position);
@@ -865,7 +1048,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
}
}
public void replaceTrait(@NonNull TraitType type, Trait oldTrait, Trait newTrait) {
public void replaceTrait(TraitType type, Trait oldTrait, Trait newTrait) {
switch (type) {
case ABILITY:
Helpers.replaceItemInList(mAbilities, oldTrait, newTrait);
@@ -890,7 +1073,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
}
}
public Trait addNewTrait(@NonNull TraitType type) {
public Trait addNewTrait(TraitType type) {
Trait newAction = new Trait("", "");
switch (type) {
case ABILITY:
@@ -911,7 +1094,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
}
}
public LiveData<List<String>> getStrings(@NonNull StringType type) {
public LiveData<List<String>> getStrings(StringType type) {
switch (type) {
case CONDITION_IMMUNITY:
return mConditionImmunities;
@@ -929,7 +1112,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
}
}
public void removeString(@NonNull StringType type, int position) {
public void removeString(StringType type, int position) {
switch (type) {
case CONDITION_IMMUNITY:
Helpers.removeFromList(mConditionImmunities, position);
@@ -952,7 +1135,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
}
}
public String addNewString(@NonNull StringType type) {
public String addNewString(StringType type) {
String newString = "";
switch (type) {
case CONDITION_IMMUNITY:
@@ -971,7 +1154,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
}
}
public void replaceString(@NonNull StringType type, String oldValue, String newValue) {
public void replaceString(StringType type, String oldValue, String newValue) {
switch (type) {
case CONDITION_IMMUNITY:
Helpers.replaceItemInList(mConditionImmunities, oldValue, newValue);
@@ -993,7 +1176,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
}
}
public boolean moveTrait(@NonNull TraitType type, int from, int to) {
public boolean moveTrait(TraitType type, int from, int to) {
switch (type) {
case ABILITY:
return Helpers.moveItemInList(mAbilities, from, to);
@@ -1021,7 +1204,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
return addItemToList(listData, newItem, null);
}
static <T> T addItemToList(@NonNull MutableLiveData<List<T>> listData, T newItem, Comparator<? super T> comparator) {
static <T> T addItemToList(MutableLiveData<List<T>> listData, T newItem, Comparator<? super T> comparator) {
ArrayList<T> newList = new ArrayList<>(listData.getValue());
newList.add(newItem);
if (comparator != null) {
@@ -1031,14 +1214,14 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
return newItem;
}
static <T> void removeFromList(@NonNull MutableLiveData<List<T>> listData, int position) {
static <T> void removeFromList(MutableLiveData<List<T>> listData, int position) {
List<T> oldList = listData.getValue();
ArrayList<T> newList = new ArrayList<>(oldList);
newList.remove(position);
listData.setValue(newList);
}
static <T> void replaceItemInList(@NonNull MutableLiveData<List<T>> listData, int position, T newItem, Comparator<? super T> comparator) {
static <T> void replaceItemInList(MutableLiveData<List<T>> listData, int position, T newItem, Comparator<? super T> comparator) {
List<T> oldList = listData.getValue();
if (oldList == null) {
oldList = new ArrayList<>();
@@ -1068,7 +1251,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
replaceItemInList(listData, position, newItem, null);
}
static <T> void replaceItemInList(@NonNull MutableLiveData<List<T>> listData, T oldItem, T newItem, Comparator<? super T> comparator) {
static <T> void replaceItemInList(MutableLiveData<List<T>> listData, T oldItem, T newItem, Comparator<? super T> comparator) {
List<T> oldList = listData.getValue();
if (oldList == null) {
oldList = new ArrayList<>();
@@ -1103,7 +1286,7 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
return value;
}
static <T> boolean moveItemInList(@NonNull ChangeTrackedLiveData<List<T>> listData, int from, int to) {
static <T> boolean moveItemInList(ChangeTrackedLiveData<List<T>> listData, int from, int to) {
List<T> oldList = listData.getValue();
if (oldList == null) {
oldList = new ArrayList<>();

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